SyntaxGameServer/RCCService2020/ExtraContent/scripts/PlayerScripts/StarterPlayerScripts/CameraScript/RootCamera/OrbitalCamera.lua

451 lines
17 KiB
Lua

--[[
Orbital Camera 1.0.0
AllYourBlox
Derived from ClassicCamera, adds camera angle constraints, and represents position values in spherical
coordinates (azimuth, elevation, radius), instead of Cartesian coordinates (x, y, z). Azimuth is the
angle of rotation about the Y axis, with the zero-angle reference point corresponding to offsetting
the camera in the +Z direction, from where it will be looking in the -Z direction by default. Elevation
is the angle up from the XZ plane, where zero degrees is on the plane, and +90 degrees is on the +Y
axis. Distance is the camera-to-subject distance, the spherical coordinates radius, specified in studs.
--]]
-- Do not edit these values, they are not the developer-set limits, they are limits
-- to the values the camera system equations can correctly handle
local MIN_ALLOWED_ELEVATION_DEG = -80
local MAX_ALLOWED_ELEVATION_DEG = 80
local externalProperties = {}
externalProperties["InitialDistance"] = 25
externalProperties["MinDistance"] = 10
externalProperties["MaxDistance"] = 100
externalProperties["InitialElevation"] = 35
externalProperties["MinElevation"] = 35
externalProperties["MaxElevation"] = 35
externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
local refAzimuthRad
local curAzimuthRad
local minAzimuthAbsoluteRad
local maxAzimuthAbsoluteRad
local useAzimuthLimits
local curElevationRad
local minElevationRad
local maxElevationRad
local curDistance
local minDistance
local maxDistance
local UNIT_Z = Vector3.new(0,0,1)
local TAU = 2 * math.pi
local changedSignalConnections = {}
-- End of OrbitalCamera additions
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
local RootCameraCreator = require(script.Parent)
local UP_VECTOR = Vector3.new(0, 1, 0)
local XZ_VECTOR = Vector3.new(1, 0, 1)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local VR_PITCH_FRACTION = 0.25
local Vector3_new = Vector3.new
local CFrame_new = CFrame.new
local math_min = math.min
local math_max = math.max
local math_atan2 = math.atan2
local math_rad = math.rad
local math_abs = math.abs
--[[ Gamepad Support ]]--
local THUMBSTICK_DEADZONE = 0.2
local r3ButtonDown = false
local l3ButtonDown = false
local currentZoomSpeed = 1 -- Multiplier, so 1 == no zooming
local function clamp(value, minValue, maxValue)
if maxValue < minValue then
maxValue = minValue
end
return math.clamp(value, minValue, maxValue)
end
local function IsFinite(num)
return num == num and num ~= 1/0 and num ~= -1/0
end
local function IsFiniteVector3(vec3)
return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
end
-- May return NaN or inf or -inf
-- This is a way of finding the angle between the two vectors:
local function findAngleBetweenXZVectors(vec2, vec1)
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
end
local function GetValueObject(name, defaultValue)
local valueObj = script:FindFirstChild(name)
if valueObj then
return valueObj.Value
end
return defaultValue
end
local function LoadOrCreateNumberValueParameter(name, valueType, updateFunction)
local valueObj = script:FindFirstChild(name)
if valueObj and valueObj:isA(valueType) then
-- Value object exists and is the correct type, use its value
externalProperties[name] = valueObj.Value
elseif externalProperties[name] ~= nil then
-- Create missing (or replace incorrectly-typed) valueObject with default value
valueObj = Instance.new(valueType)
valueObj.Name = name
valueObj.Parent = script
valueObj.Value = externalProperties[name]
else
print("externalProperties table has no entry for ",name)
return
end
if updateFunction then
if changedSignalConnections[name] then
changedSignalConnections[name]:disconnect()
end
changedSignalConnections[name] = valueObj.Changed:connect(function(newValue)
externalProperties[name] = newValue
updateFunction()
end)
end
end
local function SetAndBoundsCheckAzimuthValues()
minAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(externalProperties["CWAzimuthTravel"]))
maxAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(externalProperties["CCWAzimuthTravel"]))
useAzimuthLimits = externalProperties["UseAzimuthLimits"]
if useAzimuthLimits then
curAzimuthRad = math.max(curAzimuthRad, minAzimuthAbsoluteRad)
curAzimuthRad = math.min(curAzimuthRad, maxAzimuthAbsoluteRad)
end
end
local function SetAndBoundsCheckElevationValues()
-- These degree values are the direct user input values. It is deliberate that they are
-- ranged checked only against the extremes, and not against each other. Any time one
-- is changed, both of the internal values in radians are recalculated. This allows for
-- A developer to change the values in any order and for the end results to be that the
-- internal values adjust to match intent as best as possible.
local minElevationDeg = math.max(externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG)
local maxElevationDeg = math.min(externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG)
-- Set internal values in radians
minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg))
maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg))
curElevationRad = math.max(curElevationRad, minElevationRad)
curElevationRad = math.min(curElevationRad, maxElevationRad)
end
local function SetAndBoundsCheckDistanceValues()
minDistance = externalProperties["MinDistance"]
maxDistance = externalProperties["MaxDistance"]
curDistance = math.max(curDistance, minDistance)
curDistance = math.min(curDistance, maxDistance)
end
-- This loads from, or lazily creates, NumberValue objects for exposed parameters
local function LoadNumberValueParameters()
-- These initial values do not require change listeners since they are read only once
LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
-- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", SetAndBoundsCheckAzimuthValues)
LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", SetAndBoundsCheckElevationValues)
LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", SetAndBoundsCheckElevationValues)
LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", SetAndBoundsCheckAzimuthValues)
-- Internal values set (in radians, from degrees), plus sanitization
curAzimuthRad = math.rad(externalProperties["ReferenceAzimuth"])
curElevationRad = math.rad(externalProperties["InitialElevation"])
curDistance = externalProperties["InitialDistance"]
SetAndBoundsCheckAzimuthValues()
SetAndBoundsCheckElevationValues()
SetAndBoundsCheckDistanceValues()
end
local function CreateOrbitalCamera()
local module = RootCameraCreator()
LoadNumberValueParameters()
module.DefaultZoom = curDistance
local tweenAcceleration = math_rad(220)
local tweenSpeed = math_rad(0)
local tweenMaxSpeed = math_rad(250)
local timeBeforeAutoRotate = 2
local lastUpdate = tick()
module.LastUserPanCamera = tick()
function module:Update()
module:ProcessTweens()
local now = tick()
local timeDelta = (now - lastUpdate)
local userPanningTheCamera = (self.UserPanningTheCamera == true)
local camera = workspace.CurrentCamera
local player = PlayersService.LocalPlayer
local humanoid = self:GetHumanoid()
local cameraSubject = camera and camera.CameraSubject
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastCameraTransform = nil
end
if lastUpdate then
local gamepadRotation = self:UpdateGamepad()
if self:ShouldUseVRRotation() then
self.RotateInput = self.RotateInput + self:GetVRRotationInput()
else
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
local delta = math_min(0.1, now - lastUpdate)
if gamepadRotation ~= ZERO_VECTOR2 then
userPanningTheCamera = true
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
end
local angle = 0
if not (isInVehicle or isOnASkateboard) then
angle = angle + (self.TurningLeft and -120 or 0)
angle = angle + (self.TurningRight and 120 or 0)
end
if angle ~= 0 then
self.RotateInput = self.RotateInput + Vector2.new(math_rad(angle * delta), 0)
userPanningTheCamera = true
end
end
end
-- Reset tween speed if user is panning
if userPanningTheCamera then
tweenSpeed = 0
module.LastUserPanCamera = tick()
end
local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local zoom = self:ZoomCamera(curDistance * currentZoomSpeed)
if not userPanningTheCamera and self.LastCameraTransform then
local isInFirstPerson = self:IsInFirstPerson()
if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
if isInFirstPerson then
if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
if IsFinite(y) then
self.RotateInput = self.RotateInput + Vector2.new(y, 0)
end
tweenSpeed = 0
end
elseif not userRecentlyPannedCamera then
local forwardVector = humanoid.Torso.CFrame.lookVector
if isOnASkateboard then
forwardVector = cameraSubject.CFrame.lookVector
end
tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
local percent = clamp(0, 1, tweenSpeed * timeDelta)
if self:IsInFirstPerson() then
percent = 1
end
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
if IsFinite(y) and math_abs(y) > 0.0001 then
self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
end
end
end
end
local VREnabled = VRService.VREnabled
camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition)
local cameraFocusP = camera.Focus.p
if VREnabled and not self:IsInFirstPerson() then
local cameraHeight = self:GetCameraHeight()
local vecToSubject = (subjectPosition - camera.CFrame.p)
local distToSubject = vecToSubject.magnitude
-- Only move the camera if it exceeded a maximum distance to the subject in VR
if distToSubject > zoom or self.RotateInput.x ~= 0 then
local desiredDist = math_min(distToSubject, zoom)
vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, Vector2.new(self.RotateInput.x, 0)) * desiredDist
local newPos = cameraFocusP - vecToSubject
local desiredLookDir = camera.CFrame.lookVector
if self.RotateInput.x ~= 0 then
desiredLookDir = vecToSubject
end
local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
self.RotateInput = ZERO_VECTOR2
camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0)
end
else
-- self.RotateInput is a Vector2 of mouse movement deltas since last update
curAzimuthRad = curAzimuthRad - self.RotateInput.x
if useAzimuthLimits then
curAzimuthRad = clamp(curAzimuthRad, minAzimuthAbsoluteRad, maxAzimuthAbsoluteRad)
else
curAzimuthRad = (curAzimuthRad ~= 0) and (math.sign(curAzimuthRad) * (math.abs(curAzimuthRad) % TAU)) or 0
end
curDistance = clamp(zoom, minDistance, maxDistance)
curElevationRad = clamp(curElevationRad + self.RotateInput.y, minElevationRad,maxElevationRad)
local cameraPosVector = curDistance * ( CFrame.fromEulerAnglesYXZ( -curElevationRad, curAzimuthRad, 0 ) * UNIT_Z )
local camPos = subjectPosition + cameraPosVector
camera.CFrame = CFrame.new(camPos, subjectPosition)
self.RotateInput = ZERO_VECTOR2
end
self.LastCameraTransform = camera.CFrame
self.LastCameraFocus = camera.Focus
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
self.LastSubjectCFrame = cameraSubject.CFrame
else
self.LastSubjectCFrame = nil
end
end
lastUpdate = now
end
function module:GetCameraZoom()
return curDistance
end
function module:ZoomCamera(desiredZoom)
local player = PlayersService.LocalPlayer
if player then
curDistance = clamp(desiredZoom, minDistance, maxDistance)
end
local isFirstPerson = self:GetCameraZoom() < 2
--ShiftLockController:SetIsInFirstPerson(isFirstPerson)
-- set mouse behavior
self:UpdateMouseBehavior()
return self:GetCameraZoom()
end
function module:ZoomCameraBy(zoomScale)
local newDist = curDistance
-- Can break into more steps to get more accurate integration
newDist = self:rk4Integrator(curDistance, zoomScale, 1)
self:ZoomCamera(newDist)
return self:GetCameraZoom()
end
function module:ZoomCameraFixedBy(zoomIncrement)
return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement)
end
function module:SetInitialOrientation(humanoid)
local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, self:GetCameraLook())
local vertShift = math.asin(self:GetCameraLook().y) - math.asin(newDesiredLook.y)
if not IsFinite(horizontalShift) then
horizontalShift = 0
end
if not IsFinite(vertShift) then
vertShift = 0
end
self.RotateInput = Vector2.new(horizontalShift, vertShift)
end
function module.getGamepadPan(name, state, input)
if input.UserInputType == module.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
if r3ButtonDown or l3ButtonDown then
-- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
if (input.Position.Y > THUMBSTICK_DEADZONE) then
currentZoomSpeed = 0.96
elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
currentZoomSpeed = 1.04
else
currentZoomSpeed = 1.00
end
else
if state == Enum.UserInputState.Cancel then
module.GamepadPanningCamera = ZERO_VECTOR2
return
end
local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
if inputVector.magnitude > THUMBSTICK_DEADZONE then
module.GamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
else
module.GamepadPanningCamera = ZERO_VECTOR2
end
end
end
end
function module.doGamepadZoom(name, state, input)
if input.UserInputType == module.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
if (state == Enum.UserInputState.Begin) then
r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
elseif (state == Enum.UserInputState.End) then
if (input.KeyCode == Enum.KeyCode.ButtonR3) then
r3ButtonDown = false
elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
l3ButtonDown = false
end
if (not r3ButtonDown) and (not l3ButtonDown) then
currentZoomSpeed = 1.00
end
end
end
end
function module:BindGamepadInputActions()
local ContextActionService = game:GetService('ContextActionService')
ContextActionService:BindAction("OrbitalCamGamepadPan", module.getGamepadPan, false, Enum.KeyCode.Thumbstick2)
ContextActionService:BindAction("OrbitalCamGamepadZoom", module.doGamepadZoom, false, Enum.KeyCode.ButtonR3)
ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", module.doGamepadZoom, false, Enum.KeyCode.ButtonL3)
end
return module
end
return CreateOrbitalCamera