In short
In a client ModuleScript, keep a "trauma" number between 0 and 1. Every frame, in a function bound with RunService:BindToRenderStep at a priority just after Enum.RenderPriority.Camera, multiply workspace.CurrentCamera.CFrame by a small rotation built from math.noise and scaled by trauma squared, then lower trauma a little. Just before the camera priority, take last frame's shake back out so it never builds up. Calling shake(0.5) adds trauma. The server fires a RemoteEvent to start a shake on the clients that should feel it.
- Shake on the client: the camera only exists on each player's device.
- Bind after the camera priority, so Roblox's camera script has already placed the camera for this frame and your shake sits on top of it.
- Use noise, not
math.random, so the shake is smooth instead of jittery.
Get the script
Camera shake module for explosions and hits
A client ModuleScript with shake(amount): a smooth camera shake that fades out by itself and adds up from several hits, on top of the default camera.
-- ModuleScript in ReplicatedStorage named CameraShake. Use it from LocalScripts only.
-- CameraShake.shake(amount) adds a shake (0 to 1); it fades out by itself.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local MAX_ANGLE = 6 -- degrees of tilt at full shake
local MAX_OFFSET = 0.4 -- studs of movement at full shake
local FREQUENCY = 18 -- how fast it wobbles
local DECAY_PER_SECOND = 1.2 -- how fast trauma falls
local CameraShake = {}
CameraShake.enabled = true
local trauma = 0
local seed = math.random() * 1000
local function noise(offset: number, t: number): number
return math.noise(seed + offset, t * FREQUENCY) -- about -1 to 1
end
function CameraShake.shake(amount: number)
trauma = math.clamp(trauma + amount, 0, 1)
end
local lastShake = CFrame.identity -- the offset added last frame
if RunService:IsClient() and Players.LocalPlayer then
-- Before the camera script runs: take last frame's shake back out, because the
-- camera script reads the camera's current direction and would keep it otherwise
RunService:BindToRenderStep("CameraShakeUndo", Enum.RenderPriority.Camera.Value - 1, function()
if lastShake ~= CFrame.identity then
local camera = workspace.CurrentCamera
camera.CFrame = camera.CFrame * lastShake:Inverse()
lastShake = CFrame.identity
end
end)
-- After the camera script runs: add this frame's shake on top
RunService:BindToRenderStep("CameraShake", Enum.RenderPriority.Camera.Value + 1, function(deltaTime: number)
if trauma <= 0 then
return
end
trauma = math.max(trauma - DECAY_PER_SECOND * deltaTime, 0)
if not CameraShake.enabled then
return
end
local strength = trauma * trauma
local t = os.clock()
local tilt = CFrame.Angles(
math.rad(MAX_ANGLE * strength * noise(1, t)),
math.rad(MAX_ANGLE * strength * noise(2, t)),
math.rad(MAX_ANGLE * strength * noise(3, t))
)
local move = Vector3.new(noise(4, t), noise(5, t), 0) * MAX_OFFSET * strength
lastShake = CFrame.new(move) * tilt
local camera = workspace.CurrentCamera
camera.CFrame = camera.CFrame * lastShake
end)
end
return CameraShake
RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts
Where to put it
ReplicatedStorage
CameraShake ModuleScript new
Settings you can change
MAX_ANGLEIn the script- The most the camera can tilt, in degrees, at full shake. Default:
6. DECAY_PER_SECONDIn the script- How fast a shake fades. Higher is shorter. Default:
1.2.
Require it from a LocalScript and call CameraShake.shake(0.4). Step 2 starts shakes from the server.
Type-checked with luau-lsp strict against Roblox's API definitions on 5 November 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.
Prompt for RoCode
Add camera shake: a client module with shake(amount) that tilts the camera smoothly with noise and fades out, plus a ShakeCamera RemoteEvent so my explosion script can shake players within 40 studs, stronger when closer, and a setting to turn shake off.
On this page
How a good camera shake works
Roblox's default camera script sets the camera's position every frame from where the player is looking. A shake has to add a small extra turn after that, each frame, or the camera script simply puts the camera back. RunService:BindToRenderStep runs a function at a chosen priority before every frame is drawn; the camera runs at Enum.RenderPriority.Camera (200), so a shake bound at 201 runs right after it.
| Idea | What it means | Why |
|---|---|---|
| Trauma | One number from 0 to 1. Hits add to it; it falls back to 0 over time. | Several hits in a row add up, and every shake fades out on its own. |
| Trauma squared | The shake strength is trauma times trauma. | Small hits barely shake, big ones shake hard, and the end of a shake tails off gently. |
| Noise | math.noise gives values that change smoothly over time. | The camera rolls and wobbles instead of jumping to a new random angle each frame. |
Step 1: The camera shake module
Insert a ModuleScript in ReplicatedStorage named CameraShake and paste this. It only does anything when a LocalScript requires it.
-- ModuleScript in ReplicatedStorage named CameraShake. Use it from LocalScripts only.
-- CameraShake.shake(amount) adds a shake (0 to 1); it fades out by itself.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local MAX_ANGLE = 6 -- degrees of tilt at full shake
local MAX_OFFSET = 0.4 -- studs of movement at full shake
local FREQUENCY = 18 -- how fast it wobbles
local DECAY_PER_SECOND = 1.2 -- how fast trauma falls
local CameraShake = {}
CameraShake.enabled = true
local trauma = 0
local seed = math.random() * 1000
local function noise(offset: number, t: number): number
return math.noise(seed + offset, t * FREQUENCY) -- about -1 to 1
end
function CameraShake.shake(amount: number)
trauma = math.clamp(trauma + amount, 0, 1)
end
local lastShake = CFrame.identity -- the offset added last frame
if RunService:IsClient() and Players.LocalPlayer then
-- Before the camera script runs: take last frame's shake back out, because the
-- camera script reads the camera's current direction and would keep it otherwise
RunService:BindToRenderStep("CameraShakeUndo", Enum.RenderPriority.Camera.Value - 1, function()
if lastShake ~= CFrame.identity then
local camera = workspace.CurrentCamera
camera.CFrame = camera.CFrame * lastShake:Inverse()
lastShake = CFrame.identity
end
end)
-- After the camera script runs: add this frame's shake on top
RunService:BindToRenderStep("CameraShake", Enum.RenderPriority.Camera.Value + 1, function(deltaTime: number)
if trauma <= 0 then
return
end
trauma = math.max(trauma - DECAY_PER_SECOND * deltaTime, 0)
if not CameraShake.enabled then
return
end
local strength = trauma * trauma
local t = os.clock()
local tilt = CFrame.Angles(
math.rad(MAX_ANGLE * strength * noise(1, t)),
math.rad(MAX_ANGLE * strength * noise(2, t)),
math.rad(MAX_ANGLE * strength * noise(3, t))
)
local move = Vector3.new(noise(4, t), noise(5, t), 0) * MAX_OFFSET * strength
lastShake = CFrame.new(move) * tilt
local camera = workspace.CurrentCamera
camera.CFrame = camera.CFrame * lastShake
end)
end
return CameraShake
Test it with a LocalScript in StarterPlayerScripts that shakes when you press K:
-- LocalScript in StarterPlayerScripts named ShakeTest.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
type CameraShakeModule = { shake: (number) -> (), enabled: boolean }
local CameraShake = require(ReplicatedStorage:WaitForChild("CameraShake")) :: CameraShakeModule
UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
if not gameProcessedEvent and input.KeyCode == Enum.KeyCode.K then
CameraShake.shake(0.5)
end
end)
Press K once for a small bump and several times fast for a big one. Good starting amounts: 0.2 for footsteps of a giant, 0.4 for a hit taken, 0.7 for a nearby explosion, 1 for an earthquake. Each value in noise(1, t) to noise(5, t) uses a different offset, so the three angles and two directions do not move in step. The module binds twice: once just before Roblox's camera to take the previous frame's shake back out, and once just after it to add the new one. Without the first step, the camera script would read the shaken direction as where the player is looking, and the view would slowly drift.
Step 2: Shake from the server
Explosions, bosses and quakes are decided on the server, which has no camera. It tells clients to shake through a RemoteEvent. Insert a RemoteEvent in ReplicatedStorage named ShakeCamera, and a LocalScript that listens for it:
-- LocalScript in StarterPlayerScripts named ShakeListener.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
type CameraShakeModule = { shake: (number) -> (), enabled: boolean }
local CameraShake = require(ReplicatedStorage:WaitForChild("CameraShake")) :: CameraShakeModule
local shakeCamera = ReplicatedStorage:WaitForChild("ShakeCamera") :: RemoteEvent
shakeCamera.OnClientEvent:Connect(function(amount: unknown)
if typeof(amount) == "number" then
CameraShake.shake(math.clamp(amount, 0, 1))
end
end)
On the server, a small helper shakes everyone near a point, stronger the closer they are. Call it from your explosion, landing or attack code:
-- Script in ServerScriptService named Quakes.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shakeCamera = ReplicatedStorage:WaitForChild("ShakeCamera") :: RemoteEvent
-- Shake every player within radius of position; full strength at the centre.
local function shakeNear(position: Vector3, radius: number, strength: number)
for _, player in Players:GetPlayers() do
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if root and root:IsA("BasePart") then
local distance = (root.Position - position).Magnitude
if distance < radius then
shakeCamera:FireClient(player, strength * (1 - distance / radius))
end
end
end
end
-- Example: an explosion every 10 seconds at the Bomb part
local bomb = workspace:WaitForChild("Bomb") :: BasePart
while true do
task.wait(10)
local explosion = Instance.new("Explosion")
explosion.Position = bomb.Position
explosion.BlastPressure = 0 -- visual only, for this example
explosion.DestroyJointRadiusPercent = 0
explosion.Parent = workspace
shakeNear(bomb.Position, 40, 0.8)
end
For something everyone should feel, such as an earthquake event, use shakeCamera:FireAllClients(0.6) instead. Shakes only change each player's own camera, so a cheater blocking them gains nothing, and nothing needs checking on the way in.
Step 3: Let players turn it off
Camera shake can make some players feel sick. A setting that turns it off is a small kindness. Set CameraShake.enabled = false from your settings menu; the module still lets shakes fade so turning it back on does not start one mid-way. The settings menu guide builds toggles that save, and a toggle named CameraShake there can set this value.
-- LocalScript in StarterPlayerScripts named ShakeSetting.
-- Follows a "CameraShake" attribute on the player, set by your settings menu.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
type CameraShakeModule = { shake: (number) -> (), enabled: boolean }
local CameraShake = require(ReplicatedStorage:WaitForChild("CameraShake")) :: CameraShakeModule
local player = Players.LocalPlayer
local function apply()
CameraShake.enabled = player:GetAttribute("CameraShake") ~= false
end
player:GetAttributeChangedSignal("CameraShake"):Connect(apply)
apply()
Every LocalScript that requires the module gets the same table, because a ModuleScript runs once per device and returns the same value to each require. That is why setting enabled here affects shakes started by ShakeListener.
The simpler way: Humanoid.CameraOffset
Humanoid.CameraOffset moves the camera by a Vector3 relative to the character, and the default camera respects it. Setting it to a few random small offsets for a fraction of a second is a quick shake with no render step at all.
-- LocalScript in StarterCharacterScripts named QuickShake.
local character = script.Parent :: Model
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
local function quickShake(duration: number, size: number)
local stop = os.clock() + duration
while os.clock() < stop do
humanoid.CameraOffset = Vector3.new((math.random() - 0.5) * size, (math.random() - 0.5) * size, 0)
task.wait()
end
humanoid.CameraOffset = Vector3.zero
end
humanoid.HealthChanged:Connect(function(health: number)
if health < humanoid.MaxHealth then
quickShake(0.15, 0.6)
end
end)
It only moves the camera, never tilts it, and it jitters rather than rolls, but for a short hit reaction that is often enough. Use the module from Step 1 when shakes need to feel smooth, stack, or come from the server.
Common errors and fixes
Open the Output window (Window menu, then Output) before you press Play.
Nothing shakes
The module must be required from a LocalScript: a server Script has no camera. Check that ShakeListener is in StarterPlayerScripts, the RemoteEvent is named ShakeCamera in ReplicatedStorage, and that the amount reaching the client is above 0.
The camera drifts and does not come back
The previous frame's shake is not being removed before the camera script runs, so it adds up frame after frame. Keep both bindings from Step 1: the undo at Enum.RenderPriority.Camera.Value - 1 and the shake at Value + 1.
It shakes during cutscenes in a strange way
With CameraType set to Scriptable, your cutscene script owns the camera. Either skip the shake while a cutscene plays, or bind your cutscene before the shake so the shake still sits on top. The cutscene guide sets the camera that way.
The shake is too violent or too long
Lower MAX_ANGLE and MAX_OFFSET for less movement, and raise DECAY_PER_SECOND for shorter shakes.
Build it with RoCode
If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts and instances in your Explorer, so there is no code to paste.
Add a camera shake when grenades explode in my game: players within 40 studs get a smooth shake that fades out, stronger when closer, started from my grenade server script through a RemoteEvent. Add a Camera Shake on and off option to my settings menu.
- Can read your existing grenade and settings scripts first with Search Scripts, so it hooks into them instead of duplicating them
- Writes the ModuleScript, the LocalScripts and the server change with Create Script, and adds the RemoteEvent
- Is told to compile-check each script it writes with Check Script
- Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time
A shake has to be judged by eye, so press Play and try it yourself after RoCode builds it. With plugin 3.9 or newer RoCode can run a short playtest and read the errors. How RoCode connects to Studio.
Questions
How do I make the camera shake in Roblox?
In a LocalScript, after Roblox's camera updates each frame, multiply workspace.CurrentCamera.CFrame by a small rotation that changes smoothly and fades out. Step 1 has a module that does it with BindToRenderStep and math.noise.
Can a server Script shake the camera?
Not directly; the camera only exists on each player's device. The server fires a RemoteEvent and a LocalScript shakes the camera, as in Step 2.
Why does my shake fight the camera?
Roblox's camera script sets the camera every frame. Apply the shake after it, with a render step priority above Enum.RenderPriority.Camera, so it adds on top instead of being overwritten.
How do I shake the camera only for players near an explosion?
On the server, measure each player's distance to the explosion and fire the RemoteEvent with a strength that falls off with distance, as shakeNear does.
Sources and further reading
- Roblox Creator Docs: RunService:BindToRenderStep. priorities and running after the camera
- Roblox Creator Docs: Enum.RenderPriority
- Roblox Creator Docs: Customize the camera. the default camera and scripting it
- Roblox Creator Docs: math.noise
- Roblox Creator Docs: Humanoid.CameraOffset
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 5 November 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.