Controllers
Overview
Controllers are singleton objects that run on the client and handle specific areas of client-side logic. For example, a game might have a CameraController for custom camera behavior or a UIController for managing interface elements.
A controller is the client-side equivalent of a service on the server.
This guide builds a CameraController step by step.
Creating a Controller
At minimum, a controller requires a Name field:
local CameraController = { Name = "CameraController" }
return CameraController
The Name must be unique across all controllers. Other controllers should access it via require() to preserve type safety and IntelliSense. While Knit.GetController() works, it returns an untyped reference.
Adding Methods
function CameraController:LockTo(part)
-- Lock camera to a part
end
function CameraController:Unlock()
-- Unlock camera
end
Adding Properties
CameraController.Distance = 20
CameraController.Locked = false
Adding Basic Behavior
function CameraController:LockTo(part)
local cam = workspace.CurrentCamera
self.Locked = true
cam.CameraType = Enum.CameraType.Scriptable
cam.CFrame = part.CFrame * CFrame.new(0, 0, self.Distance)
end
function CameraController:Unlock()
local cam = workspace.CurrentCamera
self.Locked = false
cam.CameraType = Enum.CameraType.Custom
end
Continuous Updates with RenderStep
To track a moving part, bind to RenderStep:
local RunService = game:GetService("RunService")
CameraController.RenderName = "CustomCamRender"
CameraController.Priority = Enum.RenderPriority.Camera.Value
function CameraController:LockTo(part)
if self.Locked then return end
local cam = workspace.CurrentCamera
self.Locked = true
cam.CameraType = Enum.CameraType.Scriptable
RunService:BindToRenderStep(self.RenderName, self.Priority, function()
cam.CFrame = part.CFrame * CFrame.new(0, 0, self.Distance)
end)
end
function CameraController:Unlock()
if not self.Locked then return end
local cam = workspace.CurrentCamera
self.Locked = false
cam.CameraType = Enum.CameraType.Custom
RunService:UnbindFromRenderStep(self.RenderName)
end
Events
Create internal events using the Signal utility:
local Signal = require(Knit.Util.Signal)
CameraController.LockedChanged = Signal.new()
function CameraController:LockTo(part)
-- Other code...
self.LockedChanged:Fire(true)
end
function CameraController:Unlock()
-- Other code...
self.LockedChanged:Fire(false)
end
Other client code can listen for this event by requiring the controller module directly:
local CameraController = require(path.to.CameraController)
CameraController.LockedChanged:Connect(function(isLocked)
print(if isLocked then "Camera is now locked" else "Camera was unlocked")
end)
Server Communication
Controllers access server-side services via Knit.GetService(). This returns a network proxy that mirrors the service's Client table -- methods, signals, and properties are all available. Note that because this is a runtime proxy, you will not get type information or IntelliSense for the service's API.
See the Services: Client Communication section for details on what services can expose.
function CameraController:KnitStart()
local SomeService = Knit.GetService("SomeService")
SomeService:DoSomething()
SomeService.SomeEvent:Connect(function(...) end)
SomeService.AnotherEvent:Fire("Some data")
end
Note: If a service does not define a
Clienttable, it operates in server-only mode and cannot be accessed from the client viaKnit.GetService.
KnitInit and KnitStart
These lifecycle methods work identically to their service counterparts. See the Execution Model for the full lifecycle.
function CameraController:KnitInit()
-- Set up internal state; require other controllers for type-safe references
end
function CameraController:KnitStart()
-- All controllers are initialized; safe to use other controllers and services
end