§ How to · with Picoo
How to make a custom camera in Roblox
By Sametcan Tasgiran, Founder & Developer·Published ·Updated
A custom camera that snaps back to default every frame is not broken code — it is the default camera script still doing its job.
Cameras are client-only, and every reliable camera in Roblox comes down to three things: the right CameraType, the right update event, and framerate-independent smoothing. Picoo's camera_system recipe writes one 135-line LocalScript that gets all three right.
Scriptable camera control
CameraType is set to Scriptable and checked again every frame, so Roblox's default camera script cannot overwrite the CFrame.
Smooth follow
Each frame lerps toward the goal with alpha 1 - (1 - 0.18)^(dt × 60), so the follow feels the same at 30 and 240 FPS. The 0.18 is the smoothness setting.
Four camera modes
shoulder (the default: 2.5 studs right, 2 up, 8 back), first_person, top_down (40 studs above, looking down at the player) and fixed. Another LocalScript switches with _G.CameraController.setMode("top_down"), and setMode("default") hands the camera back to Roblox.
Mouse look
Mouse movement turns the shoulder and first-person cameras, with pitch clamped so the view cannot flip over. Field of view is 70 unless you ask for another value. Touch or gamepad look is a follow-up prompt.
Shake
_G.CameraController.shake(intensity, duration) adds a random offset on top of the follow that fades to zero over the duration. Camera shakes other Picoo recipes send through PicooFeedback are handed to it while it owns the camera.
Occlusion handling
A raycast from the player toward the camera, ignoring the player's own character, moves the camera to 0.6 studs in front of any wall in between.
Files Picoo ships for this prompt
1 file · 135 lines · one recipe call
StarterPlayer/StarterPlayerScripts/CameraController.lua
LocalScript with the modes, smoothing, mouse look, shake and occlusion raycast, bound to the render step at camera priority. Exposes _G.CameraController.setMode and .shake to your other LocalScripts.
135 lines
Sample output: StarterPlayer/StarterPlayerScripts/CameraController.lua
--!strict
-- Generated by Picoo recipe: camera_system
-- Cameras exist ONLY on the client. This must stay a LocalScript.
--------------------------------------------------------------------
-- Framerate independence
--------------------------------------------------------------------
-- A constant per-frame alpha converges FASTER when frames come faster, so the
-- same numbers feel different on a phone and a desktop. Raising to the power of
-- dt*60 normalises it to a 60 FPS baseline.
local function alphaFor(alpha: number, dt: number): number
return 1 - (1 - alpha) ^ (dt * 60)
end
-- (excerpt) shake, mouse look, modes and occlusion sit here; then:
function CameraController.setMode(newMode: string)
CameraController.Mode = newMode
if newMode == "default" then
-- Handing control BACK is just as important: leaving it Scriptable after a
-- cutscene leaves the player unable to look around.
camera.CameraType = Enum.CameraType.Custom
else
camera.CameraType = Enum.CameraType.Scriptable
end
end
CameraController.setMode(CameraController.Mode)
-- Camera priority, not Heartbeat: on Heartbeat the camera is a frame behind the
-- render and the result reads as input lag.
RunService:BindToRenderStep("PicooCamera", Enum.RenderPriority.Camera.Value, function(dt)
if CameraController.Mode == "default" then return end
local hrp = root()
if not hrp then return end
camera = workspace.CurrentCamera -- re-read: it is replaced on respawn
if camera.CameraType ~= Enum.CameraType.Scriptable then
camera.CameraType = Enum.CameraType.Scriptable
end
local goal = deOcclude(hrp, goalFor(hrp))
camera.CFrame = camera.CFrame:Lerp(goal, alphaFor(SMOOTH, dt)) * shakeOffset(dt)
end)Building a custom camera in Roblox
Almost every custom camera problem in Roblox is one of three things, and none of them are about maths.
The first is control. Roblox ships a camera script that runs every frame and writes CurrentCamera.CFrame. If you set the CFrame yourself without claiming ownership, that script overwrites it before the frame is drawn, and the camera appears to ignore your code entirely. Setting CameraType to Scriptable is what makes it stop, and setting it back to Custom is how you return control when your cutscene ends.
The second is location. Cameras are per-player and exist only on the client. There is a CurrentCamera on the server, but it is not any player's view, and writing to it accomplishes nothing. Camera code lives in a LocalScript; when the server needs a camera change, it fires a remote and the client acts on it.
The third is timing, and it is the one that separates a camera that feels good from one that feels cheap. Bind to the render step at camera priority, so your update runs where the default camera's would have; on Heartbeat you end up a frame behind the render and get a jitter players notice without identifying. Then correct your interpolation for delta time. A constant lerp alpha converges faster when the frame rate is higher, so a follow that feels smooth on your desktop feels sluggish on a phone. Computing the alpha as 1 - (1 - alpha)^(dt × 60) makes the behaviour identical everywhere.
Once those three are right, everything else is layering, and that is how Picoo's camera_system recipe is built. One LocalScript in StarterPlayerScripts binds at camera priority and corrects its lerp for delta time. On top of the follow it adds a random shake offset that fades out over the duration passed to shake(), and a raycast that pulls the camera in front of any wall between it and the player. The modes are branches of one goal function (shoulder, first person, top-down, fixed), setMode switches between them at runtime, and setMode("default") sets CameraType back to Custom. Zoom limits, touch-screen look or a scripted cutscene path are follow-up prompts on top of it.
See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.
Frequently asked
Why does my camera snap back to normal?+
CameraType is not Scriptable. Roblox's built-in camera module writes CurrentCamera.CFrame every frame, so unless you take control by setting CameraType to Scriptable, your value is overwritten before it is ever drawn. Setting it back to Custom is how you hand control back. Picoo's controller also re-checks CameraType every frame and sets it back to Scriptable if something else changed it.
Can I control the camera from a Script on the server?+
No. Each player has their own camera and it exists only on their client; workspace.CurrentCamera on the server is not it. Camera code belongs in StarterPlayerScripts or another LocalScript, which is where the recipe puts CameraController. If the server needs to trigger a camera change, fire a RemoteEvent and let the client do it.
RenderStepped or Heartbeat?+
BindToRenderStep with Enum.RenderPriority.Camera.Value, so your update runs at the same point in the frame the default camera would have. Updating on Heartbeat puts the camera a frame behind the render and produces a subtle jitter that people describe as "the camera feels laggy" without being able to say why.
Why does my camera feel different on other devices?+
A fixed lerp alpha per frame is framerate-dependent: it converges faster when frames come faster. Correct for delta time (1 - (1 - alpha) ^ (dt * 60)) and the same code feels the same at 30 and 240 FPS. The recipe uses exactly this formula.
How do I lock first person?+
Two ways. The recipe's first_person mode is a Scriptable camera 1.5 studs above the HumanoidRootPart, turned by the mouse. If you would rather keep Roblox's default camera and only stop players zooming out, set Player.CameraMode to Enum.CameraMode.LockFirstPerson, which is a one-line follow-up prompt.
Related Picoo prompts
Roblox FPS
Example build: 14 files · 420 lines, three weapons (rifle, shotgun, pistol). The files and numbers you get follow your prompt.
How to set up lighting and atmosphere in Roblox
It is also full of settings that silently cancel each other out: an Atmosphere that makes your fog values do nothing, a Technology mode where your torches cast no shadow. Picoo's cinematic_atmosphere recipe writes one 62-line server Script that sets the whole look as one theme.
Roblox combat system
3 scripts · 1,181 lines · one recipe call. Press Play and fight with M1, E, F, Q, R and Shift.