Hey all, I’ve been working on a top-down camera system for a brawler/RPG I’m working on.
The camera is set to follow slightly ahead of where the player is going to give them a bit of a better view of what’s ahead. If they’ve stayed still, or within range of the last focus point, the camera will slowly pan back to the player after reaching its last goal.
Everything works pretty well so far, and y’all can use it for yourselves in whatever you’re working on, but I’m also looking for some input on a few things – namely my formatting and whether or not I’m handling my returns correctly (and anything else that might be helpful tbh).
Here’s the script (I added some comments to it, hopefully someone finds them helpful lol):
local Players = game:GetService("Players")
local RunSVC = game:GetService("RunService")
local ContextActionSVC = game:GetService("ContextActionService")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local humanoid, rootPart
local camNearPlayer = false
local minZoom = 30
local maxZoom = 75
local currentZoom = 50
local minCamSpeed = 0.001
local maxCamSpeed = 0.025
local focusPoint : CFrame -- point which the camera will look at
local camGoal : CFrame -- where the camera will be
local camDistFromGoal : number
local currentCamSpeed : number
camera.CameraType = Enum.CameraType.Scriptable
camera.FieldOfView = currentZoom
local function getDist(object, goal : Vector3) : number
return (object.CFrame.Position - goal).Magnitude
end
local function setCamSpeed() : number
return camNearPlayer and minCamSpeed
or math.clamp(0.002*camDistFromGoal, minCamSpeed, maxCamSpeed)
-- camera moves faster the further away it is from its goal,
-- staying within speed range.
-- slow pan when focusPoint is near/returning to player
end
local function setNewFocusPoint() : CFrame
return camDistFromGoal < 1.1 and CFrame.new(rootPart.CFrame.Position)
or humanoid.MoveDirection.Magnitude == 0 and focusPoint
or CFrame.new(rootPart.CFrame.Position + (humanoid.MoveDirection * 20))
-- camera set to follow slightly ahead of where the player is running
-- if the player is not moving,
-- the camera will continue toward its current destination
-- if the camera is very close to its goal,
-- it will change direction and pan back toward the player
end
local function setCamGoal() : CFrame
return CFrame.new(focusPoint.Position + Vector3.new(-30,60,-30), focusPoint.Position)
-- positions the camera overhead,
-- viewing the area surrounding the player at an angle
end
local function invalidState(state) : boolean
return state == Enum.UserInputState.End
or state == Enum.UserInputState.Cancel
or state == Enum.UserInputState.None
-- all InputStates to be ignored when zooming in/out
end
local function isAtMaxZoom(isZoomingOut) : boolean
return isZoomingOut and currentZoom >= maxZoom
or not isZoomingOut and currentZoom <= minZoom
-- makes sure the player doesn't zoom in/out past min/max values
end
local function isZoomingOut(input : InputObject) : boolean
return input.UserInputType == Enum.UserInputType.MouseWheel and input.Position.Z < 0
or input.KeyCode == Enum.KeyCode.Minus
-- when zooming, checks to see if player is zooming out specifically
-- if not, assume they're zooming in since that's the only other option anyway
end
local function camIsMoving() : boolean
return camDistFromGoal >= 1 and camDistFromGoal <= 10
-- a wider distance allows the player to stop the camera more quickly and easily
-- a shorter distance will make the camera more reactive to movement
end
local function setZoom(action, inputState, inputObject : InputObject?)
if invalidState(inputState) then
return Enum.ContextActionResult.Pass
end
local zoomingOut = isZoomingOut(inputObject)
currentZoom += isAtMaxZoom(zoomingOut) and 0 or zoomingOut and 5 or -5
camera.FieldOfView = currentZoom
end
local function OverheadCam()
camDistFromGoal = getDist(camera, camGoal.Position)
camera.CFrame = camera.CFrame:Lerp(camGoal, setCamSpeed())
if getDist(rootPart, focusPoint.Position) < 22 and camIsMoving() then return end
-- if the player is currently moving and not within range of the previous focusPoint,
-- update the point for the camera to lerp to
focusPoint = setNewFocusPoint()
camGoal = setCamGoal()
camNearPlayer = getDist(rootPart, focusPoint.Position) < 5
end
local function UnbindCamera() -- called when player character dies/is removed
humanoid, rootPart = nil
RunSVC:UnbindFromRenderStep("Camera")
ContextActionSVC:UnbindAction("Zoom")
-- stops the player from moving the camera after death
end
player.CharacterAdded:Connect(function(char)
humanoid = char:WaitForChild("Humanoid")
rootPart = char:WaitForChild("HumanoidRootPart")
focusPoint = rootPart.CFrame -- focus on player when spawning
camGoal = setCamGoal()
camera.CFrame = camGoal
RunSVC:BindToRenderStep("Camera", 0, OverheadCam)
-- camera begins tracking the player once their character spawns
ContextActionSVC:BindAction("Zoom", setZoom, false,
Enum.KeyCode.Equals,
Enum.KeyCode.Minus,
Enum.UserInputType.MouseWheel
)
-- once character spawns, let them zoom in/out using the given inputs
humanoid.Died:Connect(UnbindCamera)
end)
player.CharacterRemoving:Connect(UnbindCamera)