Tutorial · Gameplay

How to make a camera shake in Roblox

A short camera shake makes an explosion, a heavy hit or a boss landing feel heavy. Done badly it feels like a glitch: the camera snaps around randomly and keeps going too long. This guide builds a shake that moves smoothly, fades out on its own and adds up when several things happen at once, on top of Roblox's normal camera so the player keeps control. Then it lets the server start shakes for everyone or only for players near an explosion, and gives players a way to turn it off.

Updated 5 November 20267 min readIntermediateLuauWritten by

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
-- 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

Explorer: the module, required from LocalScripts
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
  1. Get the script
  2. How it works
  3. 1. Shake module
  4. 2. From the server
  5. 3. Off switch
  6. CameraOffset
  7. Common errors
  8. With RoCode
  9. Questions
  10. Sources

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.

The three ideas behind it
IdeaWhat it meansWhy
TraumaOne 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 squaredThe shake strength is trauma times trauma.Small hits barely shake, big ones shake hard, and the end of a shake tails off gently.
Noisemath.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
-- 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
-- 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
-- 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
-- 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
-- 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
-- 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.

You type

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.

RoCode does
  • 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

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.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.