Configurable Head Bobbing Script

Configurable Head Bobbing Script

I have open-sourced my first person camera and head bobbing script for anyone to use. The bobbing is based on the clamped magnitude of your X and Z velocity and uses the sine function. Here is a video preview:

To use, place it as a LocalScript under StarterCharacterScripts. I have explained the constants with comments for easy adjustment.
You may wish to modify this script and turn it into a module to use with your current game’s code and may do so freely.

This script is free to use, redistribute and adapt under CC BY-SA 4.0

--[[
Created by @BuiltToWreck

Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
https://creativecommons.org/licenses/by-sa/4.0/

Place in StarterCharacterScripts
Adjust constants to your liking.

]]

-- CONSTANTS --
-- Camera position offset from HumanoidRootPart:
local OFFSET = Vector3.new(0, 3, 0)
-- Mouse sensitivity multiplier
local SENSITIVITY = 0.006
-- Verticle angle limits (how far you can look up and down):
local UPPER_ANGLE_LIMIT = math.rad(85)
local LOWER_ANGLE_LIMIT = math.rad(-85)
-- How many bobs per second horizontally and vertically:
local BOB_FREQUENCY_Y = 4
local BOB_FREQUENCY_X = 2
-- How intense the horizontal and vertical bobs are:
local BOB_AMPLITUDE_X = 0.6
local BOB_AMPLITUDE_Y = 0.4
-- The maximum velocity that affects the intensity of the bob:
local MAX_VELOCITY = 16
-- How fast it takes for bobbing to start/stop when you start/stop moving:
local RECENTER_SPEED = 4

-- SERVICES --
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")

-- REFERENCES --
local localPlayer = Players.LocalPlayer
local character = localPlayer.Character or localPlayer.CharacterAdded:Wait()
local currentCamera = workspace.CurrentCamera

-- VARIABLES --
local cameraAngle = Vector2.new(0, 0)
local currentTime = 0
local velocityMultiplier = 0

-- FUNCTIONS --
local function scalarLerp(a, b, c)
	c = math.clamp(c, 0, 1)
	return a + c *	(b - a)
end

local function renderStepped(deltaTime)
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	local mouseDelta = UserInputService:GetMouseDelta() * SENSITIVITY
	
	-- Calculate bob offsets
	currentTime += deltaTime
	local bobOffsetY = (math.sin(currentTime * math.pi * BOB_FREQUENCY_Y) - 0.5) * BOB_AMPLITUDE_Y
	local bobOffsetX = (math.sin(currentTime * math.pi * BOB_FREQUENCY_X) - 0.5) * BOB_AMPLITUDE_X
	
	-- Smooth between bobbing and neutral based on horizontal velocity
	local velocityMagnitude = (humanoidRootPart.Velocity * Vector3.new(1,0,1)).Magnitude
	local targetVelocityMultiplier = math.clamp(velocityMagnitude, 0, MAX_VELOCITY) / MAX_VELOCITY
	velocityMultiplier = scalarLerp(velocityMultiplier, targetVelocityMultiplier, RECENTER_SPEED * deltaTime)
	bobOffsetX *= velocityMultiplier
	bobOffsetY *= velocityMultiplier
	
	-- Update the camera angle, clamp it to angle limits
	cameraAngle -= mouseDelta
	cameraAngle = Vector2.new(
		cameraAngle.X, 
		math.clamp(cameraAngle.Y, LOWER_ANGLE_LIMIT, UPPER_ANGLE_LIMIT)
	) 
	
	-- Set the cframe of the camera
	currentCamera.CFrame = 
		CFrame.new(humanoidRootPart.Position + OFFSET + Vector3.new(0, bobOffsetY, 0)) * 
		CFrame.Angles(0, cameraAngle.X, 0) * 
		CFrame.Angles(cameraAngle.Y, 0, 0) *
		CFrame.new(bobOffsetX, 0, 0)
end

-- LOGIC/SETUP/CONNECTIONS --
localPlayer.CharacterAppearanceLoaded:Wait()
currentCamera.CameraType = Enum.CameraType.Scriptable
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter

-- Make character invisible
for _, desc in ipairs(character:GetDescendants()) do
	if desc:IsA("Part") then
		desc.Transparency = 1
	end
end

RunService.RenderStepped:Connect(renderStepped)

I hope this is of use to you :slightly_smiling_face:

59 Likes

This will really help alot of people keep up the great work! :happy3:

2 Likes

Honestly the head bobbing is just weird. Your view irl isn’t that slow

I think this is a really good idea on paper, but in practice I don’t think it will make a whole lot of sense. I think most people will find the movement to be awkward and obnoxious (and this will obviously vary depending on the game).

I would personally suggest you try and look at a few games (Warzone, Battlefield, etc.) and try to mimic, in Roblox, a lot of the look and feel you find. For example, think of the parachute mechanics in Warzone; your FOV is altered, your screen shakes, all sorts of effects take place. I would definitely take a look at @sleitnick’s RbxCameraShaker module, which is a really fantastic port of Unity’s EZ Camera Shake asset.

Your code is otherwise written very well though! Glad to see high quality code on the DevForum every now and then.

5 Likes

You can adjust the settings to your liking

Some of you have mentioned the bob not looking natural, I encourage you to play with the constants, especially frequency and amplitude, to get the style and intensity of bob you like.

Here is an example of a bob with the following frequency and amplitude constants:

local BOB_FREQUENCY_Y = 4
local BOB_FREQUENCY_X = 2
local BOB_AMPLITUDE_X = 0.6
local BOB_AMPLITUDE_Y = 0.4

It gives an infinity shaped pattern that I like quite a lot. Since my amplitudes are fairly high, I would see this being used more for story/horror game type scenarios where camera bob doesn’t interfere with anything like aiming.

For a more tactical game like an fps, I would reduce the amplitude of the bob, or use the bob on the viewmodel instead of the camera, in order to reduce the potency of the camera bob.

Thank you for the support and your concerns. :slight_smile:

EDIT: I will update my post to use these adjusted settings as they seem to be better than the original

4 Likes

My camera aint shaking do you know why?

6 Likes

Sorry for reviving this but the previews for this look really good, but it doesn’t work for me. It’s in a LocalScript, and it’s in StarterCharacterScripts.
Edit: Never mind, I fixed it.

1 Like

how did u fix it mine doesnt move the screen whenn walking

nvm u have to be R6

R6 wasn’t the fix for me, I changed the layout of the RenderStepped function and that worked

1 Like

Here’s a modification of your script that detects the walkspeed and changes the headbob speed from that! You can still edit the base values to your liking, but this will automatically change the headbob speed when the walkspeed changes.

--[[
Created by @BuiltToWreck

Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
https://creativecommons.org/licenses/by-sa/4.0/

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material
  for any purpose, even commercially. 

Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the license, 
  and indicate if changes were made. You may do so in any reasonable manner, but 
  not in any way that suggests the licensor endorses you or your use.
- ShareAlike — If you remix, transform, or build upon the material, you must
  your contributions under the same license as the original.
- No additional restrictions — You may not apply legal terms or technological 
  measures that legally restrict others from doing anything the license permits.

Place in StarterCharacterScripts
Adjust constants to your liking.

]]

-- CONSTANTS --
-- Camera position offset from HumanoidRootPart:
local OFFSET = Vector3.new(0, 1.6, 0)
-- Mouse sensitivity multiplier
local SENSITIVITY = 0.006
-- Verticle angle limits (how far you can look up and down):
local UPPER_ANGLE_LIMIT = math.rad(85)
local LOWER_ANGLE_LIMIT = math.rad(-85)
-- How many bobs per second horizontally and vertically:
local BOB_FREQUENCY_Y = 4
local BOB_FREQUENCY_X = 2
-- How intense the horizontal and vertical bobs are:
local BOB_AMPLITUDE_X = 0.05
local BOB_AMPLITUDE_Y = 0.16
-- The maximum velocity that affects the intensity of the bob:
local MAX_VELOCITY = 16
-- How fast it takes for bobbing to start/stop when you start/stop moving:
local RECENTER_SPEED = 8

-- SERVICES --
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")

-- REFERENCES --
local localPlayer = Players.LocalPlayer
local character = localPlayer.Character or localPlayer.CharacterAdded:Wait()
local hum = character:WaitForChild("Humanoid")
local currentCamera = workspace.CurrentCamera

-- VARIABLES --
local cameraAngle = Vector2.new(0, 0)
local currentTime = 0
local velocityMultiplier = 0

local places = 3 -- the amount of decimal places

local mult = 10^places

local bfy
local bfx
local bax
local bay

-- FUNCTIONS --
local function scalarLerp(a, b, c)
	c = math.clamp(c, 0, 1)
	return a + c *	(b - a)
end

local function renderStepped(deltaTime)
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	local mouseDelta = UserInputService:GetMouseDelta() * SENSITIVITY
	bfy = math.floor((BOB_FREQUENCY_Y * (hum.WalkSpeed / 10))*mult)/mult
	bfx = math.floor((BOB_FREQUENCY_X * (hum.WalkSpeed / 10))*mult)/mult
	bax = math.floor((BOB_AMPLITUDE_X * (hum.WalkSpeed / 12))*mult)/mult
	bay = math.floor((BOB_AMPLITUDE_Y * (hum.WalkSpeed / 12))*mult)/mult

	-- Calculate bob offsets
	currentTime += deltaTime
	local bobOffsetY = (math.sin(currentTime * math.pi * bfy) - 0.5) * bay
	local bobOffsetX = (math.sin(currentTime * math.pi * bfx) - 0.5) * bax

	-- Smooth between bobbing and neutral based on horizontal velocity
	local velocityMagnitude = (humanoidRootPart.Velocity * Vector3.new(1,0,1)).Magnitude
	local targetVelocityMultiplier = math.clamp(velocityMagnitude, 0, MAX_VELOCITY) / MAX_VELOCITY
	velocityMultiplier = scalarLerp(velocityMultiplier, targetVelocityMultiplier, RECENTER_SPEED * deltaTime)
	bobOffsetX *= velocityMultiplier
	bobOffsetY *= velocityMultiplier

	-- Update the camera angle, clamp it to angle limits
	cameraAngle -= mouseDelta
	cameraAngle = Vector2.new(
		cameraAngle.X, 
		math.clamp(cameraAngle.Y, LOWER_ANGLE_LIMIT, UPPER_ANGLE_LIMIT)
	) 

	-- Set the cframe of the camera
	currentCamera.CFrame = 
		CFrame.new(humanoidRootPart.Position + OFFSET + Vector3.new(0, bobOffsetY, 0)) * 
		CFrame.Angles(0, cameraAngle.X, 0) * 
		CFrame.Angles(cameraAngle.Y, 0, 0) *
		CFrame.new(bobOffsetX, 0, 0)
end

-- LOGIC/SETUP/CONNECTIONS --
localPlayer.CharacterAppearanceLoaded:Wait()
currentCamera.CameraType = Enum.CameraType.Scriptable
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter

-- Make character invisible
for _, desc in ipairs(character:GetDescendants()) do
	if desc:IsA("Part") then
		desc.Transparency = 1
	end
end

RunService.RenderStepped:Connect(renderStepped)

This is pretty buggy if you tween the walkspeed though, so just change it in one line instead of a tween for this to work properly. If you tween it, the sine wave will keep restarting from the original position and act weird.

EDIT: The base values are designed for 10 walkspeed.

4 Likes

Just use this:

local plr = game:GetService("Players").LocalPlayer
local humanoid = plr.Character:WaitForChild("Humanoid")
local mouse = plr:GetMouse()
local camera = workspace.CurrentCamera
local UserInputService = game:GetService("UserInputService")
local touchEnabled = UserInputService.TouchEnabled
local function lerp(v1, v2, t)
    return v1 + (v2 - v1) * t
end
UserInputService.MouseIconEnabled = false
local angleX = 0
local x = 0
local y = 0
local tilt = 0
local vX = 0
local vY = 0
local sX = 10
local sY = 10
game:GetService("RunService").RenderStepped:Connect(function(dt)
    dt *= 60
    local vel = Vector3.new(humanoid.RootPart.Velocity.X, 0, humanoid.RootPart.Velocity.Z).Magnitude
    if dt > 2 then
        vX = 0
        vY = 0
    else
        vX = lerp(vX, math.cos(tick() * 0.5 * math.random(10, 15)) * (math.random(5, 20) / 200) * dt, 0.05 * dt)
        vY = lerp(vY, math.cos(tick() * 0.5 * math.random(5, 10)) * (math.random(2, 10) / 200) * dt, 0.05 * dt)
    end
    camera.CFrame *= CFrame.fromEulerAnglesXYZ(0, 0, math.rad(angleX))
        * CFrame.fromEulerAnglesXYZ(math.rad(math.clamp(x * dt, -0.15, 0.15)), math.rad(math.clamp(y * dt, -0.5, 0.5)), tilt)
        * CFrame.fromEulerAnglesXYZ(math.rad(vX), math.rad(vY), math.rad(vY * 10))
    tilt = math.clamp(lerp(tilt, -camera.CFrame:VectorToObjectSpace((humanoid.RootPart and humanoid.RootPart.Velocity or Vector3.new())
        / math.max(humanoid.WalkSpeed, 0.01)).X * 0.05, 0.1 * dt), -0.05, 0.05)
    if not touchEnabled and dt < 2 then
        angleX = lerp(angleX, math.clamp(UserInputService:GetMouseDelta().X / dt * 0.15, -2.5, 2.5), 0.25 * dt)
    end
    x = lerp(x, math.sin(tick() * sX) / 5 * math.min(1, sY / 10), 0.25 * dt)
    if vel > 1 then
        y = lerp(y, math.cos(tick() * 0.5 * math.floor(sX)) * (sX / 200), 0.25 * dt)
    else
        y = lerp(y, 0, 0.05 * dt)
    end
    if vel > 12 then
        sX = 20
        sY = 18
    elseif vel > 0.1 then
        sX = 12
        sY = 14
    else
        sY = 0
    end
end)
5 Likes

this is so badly written, god damn