CameraController
A small object-oriented wrapper around a custom camera loop. A CameraController holds an
update function that runs once per frame while the controller is active (bound with
RunService:BindToRenderStep at Enum.RenderPriority.Input priority) and an optional reset
function that puts the camera back the way you want it when the controller is cleaned up.
Only one controller can be active at a time; the module tracks it so other scripts can find it
with GetCurrent or tear it down with DestroyCurrent.
Everything is tracked with a Trove that is
attached to workspace.CurrentCamera, so a controller is destroyed automatically if the camera
it drives is destroyed.
The module requires the client and errors when required on the server. It returns a table with
new, GetCurrent and DestroyCurrent; methods are called on the object returned by new.
local Players = game:GetService("Players")
local CameraController = require(path.to.CameraController)
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local root = character:WaitForChild("HumanoidRootPart") :: BasePart
-- Top-down camera that follows the character
local controller = CameraController.new(function(camera, dt)
camera.CameraType = Enum.CameraType.Scriptable
local target = root.Position
camera.CFrame = CFrame.lookAt(target + Vector3.new(0, 40, 20), target)
end, function(camera)
-- Runs when the controller is cleaned up: hand control back to Roblox
camera.CameraType = Enum.CameraType.Custom
end)
controller:Start()
-- Later, from anywhere on the client:
CameraController.DestroyCurrent()
Lifecycle
Start calls Stop first, and Stop cleans the whole Trove. The bookkeeping hook that new
registers (which calls ResetFunc, resets Active and clears the current controller) is part
of that Trove, so it runs during Start, before the update loop begins, and is not re-added.
After a Start, a later Stop or Destroy only unbinds the update function: ResetFunc is
not called again and Active stays true. When you are finished with a controller, prefer
CameraController.DestroyCurrent() (which also clears the tracked controller so a new one can
be started) and create a fresh controller with new rather than restarting the same object.
Credits: cleanup is handled by Trove, by sleitnick (sleitnick's RbxUtil). Wally installs it.
Installation and guide: CameraController package page.
Types
UpdateFunc
Runs every frame while the controller is active, at Enum.RenderPriority.Input priority (before
Roblox's own camera scripts). Receives the controller's Camera and the frame delta time. Set
it in new or with SetUpdate.
ResetFunc
Runs when the controller's Trove is cleaned (see the class description for exactly when that
happens). Use it to restore CameraType, field of view or anything else the update function
changed. Set it in new or with SetReset.
Properties
Camera
This item is read only and cannot be modified. Read OnlyCameraController.Camera: Camera
The camera this controller drives: workspace.CurrentCamera at the time new was called. It is
passed to UpdateFunc and ResetFunc. The Trove is attached to it, so destroying the camera
destroys the controller.
Trove
This item is read only and cannot be modified. Read OnlyCameraController.Trove: Trove
The Trove holding the render-step binding and
the controller's cleanup hook. Stop cleans it, Destroy destroys it. You may add your own
objects (connections, instances) so they are released together with the controller.
Active
This item is read only and cannot be modified. Read OnlyCameraController.Active: booleantrue once Start has bound the update function. Start errors while this is true.
Locked
This item is read only and cannot be modified. Read OnlyCameraController.Locked: boolean
true between Lock and Unlock. While locked, SetUpdate and SetReset error instead of
replacing the functions.
UpdateFunc
CameraController.UpdateFunc: UpdateFunc?The per-frame function. nil until set by new or SetUpdate; Start errors if it is nil.
ResetFunc
CameraController.ResetFunc: ResetFunc?The optional reset function called when the controller's Trove is cleaned.
Functions
new
Creates a controller for workspace.CurrentCamera. Nothing runs until you call Start.
Both functions are optional here and can be supplied later with SetUpdate / SetReset, but
Start requires an update function. The constructor also registers a cleanup hook in the
controller's Trove that calls resetFunc, resets Active and clears the current controller
when the Trove is cleaned, and attaches the Trove to the camera so the controller is destroyed
with it.
GetCurrent
Returns the controller most recently activated with Start, or nil if none has been started
or the current one was cleared (by its cleanup hook or by DestroyCurrent). Lets scripts that
did not create the controller inspect or destroy it.
local current = CameraController.GetCurrent()
if current and current.Active then
print("A custom camera is running")
end
DestroyCurrent
CameraController.DestroyCurrent() → ()Destroys the tracked current controller (if there is one) and clears the module's reference to it, so a new controller can be started without hitting "An existing CameraController is already active". Safe to call when nothing is active. This is the recommended way to end a custom camera mode.
Construct
Returns a shallow copy of this controller made with table.clone. The copy has the same
metatable and starts with the same UpdateFunc, ResetFunc, Active and Locked values, but
it shares the original's Trove and Camera: stopping or destroying either object cleans
the shared Trove. Treat it as a way to derive a variant with a different update function via
SetUpdate, not as an independent controller.
Stop
CameraController:Stop() → ()
Cleans the controller's Trove. This unbinds the render-step update added by Start, so the
update function stops running, and releases anything else you added to the Trove. Start
calls this itself before binding, which is when the cleanup hook from new (and therefore
ResetFunc) runs; see the class description for the consequences.
SetUpdate
Replaces the per-frame update function. Takes effect on the next frame if the controller is
already active, since the bound render step reads UpdateFunc each frame.
Errors
| Type | Description |
|---|---|
| "CameraController is locked" | `Lock` was called and `Unlock` has not been. |
SetReset
Replaces the reset function that the Trove cleanup hook calls with the controller's camera.
Errors
| Type | Description |
|---|---|
| "CameraController is locked" | `Lock` was called and `Unlock` has not been. |
Start
CameraController:Start() → ()
Activates the controller. After validating that an update function is set and that neither
this nor any other controller is active, it calls Stop (cleaning the Trove), binds the update
function with Trove:BindToRenderStep("CameraController", Enum.RenderPriority.Input.Value, ...),
sets Active to true and makes this the controller returned by GetCurrent.
Each frame the bound function calls UpdateFunc(Camera, dt); if UpdateFunc has somehow
become nil the controller stops itself instead.
Errors
| Type | Description |
|---|---|
| "There is no update function set in the CameraController" | `UpdateFunc` is `nil`. |
| "This CameraController is already active" | `Active` is already `true`. |
| "An existing CameraController is already active" | Another controller is current and active; destroy it (for example with `DestroyCurrent`) first. |
Lock
CameraController:Lock() → ()
Sets Locked to true, making SetUpdate and SetReset error until Unlock is called. Use
it to stop other scripts from hijacking a camera mode (a cutscene, for example) while it runs.
Locking does not affect Start, Stop or Destroy.
Unlock
CameraController:Unlock() → ()Sets Locked back to false so SetUpdate and SetReset work again.
Destroy
CameraController:Destroy() → ()
Destroys the controller's Trove, unbinding the update function and releasing everything the
Trove holds. The object should not be used afterwards. Note that this does not clear the
module's tracked current controller by itself; DestroyCurrent does both.