How do I add head bobbing to cutscene?

I am trying to add head bobbing that mimics human movement to my cutscene as part of a horror game. How would I go about doing this?

Example:

Current cutscene code:

local function MoveCamera(StartPart, EndPart, Duration, EasingStyle, EasingDirection)
	Camera.CameraType = Enum.CameraType.Scriptable
	Camera.CFrame = StartPart.CFrame
	local Cutscene = TweenService:Create(Camera, TweenInfo.new(Duration, EasingStyle, EasingDirection), {CFrame = EndPart.CFrame})
	Cutscene:Play()
	wait(Duration)
end

local function Cutscene()
	local CameraNodes = game.ReplicatedStorage:WaitForChild("CameraNodes"):GetChildren()
	table.sort(CameraNodes, function(a, b)
		return a.Name < b.Name
	end)
	MoveCamera(CameraNodes[1], CameraNodes[2], 10, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	wait(3)
	MoveCamera(CameraNodes[3], CameraNodes[4], 10, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	
	MoveCamera(CameraNodes[5], CameraNodes[6], 10, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	MoveCamera(CameraNodes[6], CameraNodes[7], 10, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	
	for i = 7, 14 do
		local speed = 3
		local startCam = CameraNodes[i]
		local ii = i + 1
		local endCam = CameraNodes[ii]
		local startPosition = startCam:GetPivot().Position
		local endPosition = endCam.Position
		local duration = (startPosition - endPosition).Magnitude / speed 
		MoveCamera(startCam, endCam, duration, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
		if CameraNodes[i].Name == "CameraAN" then 
			break
		end
	end

Also I know the code is awful I’m using a modified version of a script I made when I was 13 because I cannot be asked to rewrite from scratch.

1 Like

Your tween is doing a long sweeping movement from one position to the next. As it’s targeting the CFrame, and CFrame includes the rotational element of the camera, I think it would be quite difficult to implement varying roll without spamming tweens for each movement of the camera.

There may be other tricks that you can do as I’ve not messed around with the camera in quite a while, but I would tween an invisible part or a CFrame value between the two coordinates to get the base CFrame (similar to what you already have), then in a RenderStepped bind I would set the camera’s CFrame to the tweening CFrame and multiply by a rotation value defined using some form of math.sin() function against time.

I’m sure there’s easier ways to do it, but I’m not sure.

1 Like

I would rewrite from scratch, this code is a mess, it uses tweening instead of lerping, which makes it less responsive, if you rewrite with your current knowledge it would probably be better.

I created a very basic example of what I was trying to explain:

local Player = game:GetService('Players').LocalPlayer;
local Camera = workspace.CurrentCamera;

repeat wait(); until Player.Character;

Camera.CameraType = Enum.CameraType.Scriptable;

local TweeningPart = workspace.CFramePart;

---
local Duration = 8;
local EasingStyle = Enum.EasingStyle.Quad;
local EasingDirection = Enum.EasingDirection.Out;
---

local StartPart = workspace.Start;
local EndPart = workspace.End;

function runCutscene()
	
	TweeningPart.CFrame = StartPart.CFrame;
	Camera.CFrame = TweeningPart.CFrame;
	
	local CutsceneTween = game:GetService('TweenService'):Create(TweeningPart, TweenInfo.new(Duration, EasingStyle, EasingDirection), {CFrame = EndPart.CFrame});
	
	local Run = game:GetService('RunService').RenderStepped:Connect(function()
		Camera.CFrame = TweeningPart.CFrame *CFrame.Angles(0, math.pi, 0.05*math.sin(2*tick()));
	end);
	
	CutsceneTween:Play();
	
	CutsceneTween.Completed:Connect(function()
		Run:Disconnect();
	end);
end

runCutscene();

I’ve used a part here to demonstrate, but you could lerp a CFrame or use a CFrame value as an alternative.

I’ve done some research on lerping this is the code I’ve got so far, the first camera movement is fine but every other camera movement happens instantaneously, when it should last the duration which I have left as 10 for testing. I know its probably a silly mistake but I haven’t coded in lua in ages so am a bit lost.

local RunService = game:GetService("RunService")
local Camera = game.Workspace.CurrentCamera
Camera.CameraType = Enum.CameraType.Scriptable
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StartVehicleMovementEvent = ReplicatedStorage:WaitForChild("StartVehicleMovement")

local CameraNodes = game.ReplicatedStorage:WaitForChild("CameraNodes"):GetChildren()
table.sort(CameraNodes, function(a, b)
	return a.Name < b.Name
end)

local function MoveCamera(StartPart, TargetPart, Duration)
	
	local startCFrame = StartPart.CFrame
	local targetCFrame = TargetPart.CFrame
	local runningTime = 0
	local lerp
	
	lerp = RunService.Heartbeat:Connect(function(deltaTime)
		runningTime += deltaTime
		local alpha = runningTime / Duration
		Camera.CFrame = startCFrame:Lerp(targetCFrame, alpha)
		if alpha >= 1 then
			lerp:Disconnect()
		end
	end)
end

local function Cutscene()
	MoveCamera(CameraNodes[1], CameraNodes[2], 10)
	MoveCamera(CameraNodes[3], CameraNodes[4], 10)
	for i = 5, 14 do
		local startCam = CameraNodes[i]
		local endCam = CameraNodes[i+1]
		MoveCamera(startCam, endCam, 10)
		task.wait()
	end
end

I think you forget to set the running time back to 0 when everything is done.
The cleanup should look something like this.

if alpha >= 1 then
   lerp:Disconnect()
   runningTime = 0
end

Doing that just freezes the camera after the first movement.

I managed to fix the issue

local RunService = game:GetService("RunService")
local Camera = game.Workspace.CurrentCamera
Camera.CameraType = Enum.CameraType.Scriptable
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StartVehicleMovementEvent = ReplicatedStorage:WaitForChild("StartVehicleMovement")
local CameraNodes = game.ReplicatedStorage:WaitForChild("CameraNodes"):GetChildren()
table.sort(CameraNodes, function(a, b)
    return a.Name < b.Name
end)

local function MoveCamera(StartPart, TargetPart, Duration)
    local startCFrame = StartPart.CFrame
    local targetCFrame = TargetPart.CFrame
    local runningTime = 0
    local alpha = 0
    Camera.CFrame = startCFrame
    local lerp
    
    local completed = false
    lerp = RunService.Heartbeat:Connect(function(deltaTime)
        runningTime += deltaTime
        alpha = runningTime / Duration
        Camera.CFrame = startCFrame:Lerp(targetCFrame, alpha)
        if alpha >= 1 then
            lerp:Disconnect()
            completed = true
        end
    end)
    
    while not completed do
        task.wait()
    end
end

local function Cutscene()
    MoveCamera(CameraNodes[1], CameraNodes[2], 10)
    MoveCamera(CameraNodes[3], CameraNodes[4], 10)

    for i = 5, 14 do
        local startCam = CameraNodes[i]
        local endCam = CameraNodes[i+1]
        MoveCamera(startCam, endCam, 10)
    end
end

GUI = script.Parent.Parent
Confirm = script.Parent
GUI.Visible = true

Confirm.MouseButton1Click:connect(function()
    StartVehicleMovementEvent:FireServer()
    Confirm.Visible = false
    task.wait(2)
    GUI.Visible = false
    Cutscene()
end)
1 Like

Aight Ima put in some annotations so I don’t forget what any of this does then I’ll try work out the head bobbing system and calibrating the speed of each camera movement.

This is actually worthy of getting its own studio extension I can’t lie

-- Roblox Service Imports
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Replicated Storage Imports
local StartVehicleMovementEvent = ReplicatedStorage:WaitForChild("StartVehicleMovement")
local CameraNodes = game.ReplicatedStorage:WaitForChild("CameraNodes"):GetChildren()

-- Detecting Camera and Enabling Scripting
local Camera = game.Workspace.CurrentCamera
Camera.CameraType = Enum.CameraType.Scriptable

-- Sorting Camera Nodes Into Correct Order
table.sort(CameraNodes, function(a, b)
	return a.Name < b.Name
end)

-- Camera Movement Function
local function MoveCamera(StartPart, TargetPart, speed)
	
	-- Set up variables for lerp
	local startCFrame = StartPart.CFrame
	local targetCFrame = TargetPart.CFrame
	local startPosition = StartPart.Position
	local endPosition = TargetPart.Position
	local duration = (startPosition - endPosition).Magnitude / speed -- Duration for each segment
	local runningTime = 0
	
	-- Initial Camera Position Setup
	Camera.CFrame = startCFrame
	local lerp
	
	-- Camera Movement
	local completed = false
	lerp = RunService.Heartbeat:Connect(function(deltaTime)
		
		-- Movement Time Calculations
		runningTime += deltaTime
		local alpha = runningTime / duration
		
		-- Camera Movement
		Camera.CFrame = startCFrame:Lerp(targetCFrame, alpha)
		
		-- Completed Movement Check
		if alpha >= 1 then
			lerp:Disconnect()
			completed = true
		end
	end)
	
	-- Wait Until Movement Is Completed
	while not completed do
		task.wait()
	end
end

-- Cutscene Editor 
local function Cutscene()
	-- Seperate Movements
	MoveCamera(CameraNodes[1], CameraNodes[2], 1.75)
	MoveCamera(CameraNodes[3], CameraNodes[4], 1.75)
	
	-- Combined Movements
	for i = 5, 14 do
		local startCam = CameraNodes[i]
		local endCam = CameraNodes[i+1]
		MoveCamera(startCam, endCam, 0.5)
	end
end

-- GUI Variables
GUI = script.Parent.Parent
Confirm = script.Parent
GUI.Visible = true

-- Run When Clicked
Confirm.MouseButton1Click:Connect(function()
	StartVehicleMovementEvent:FireServer()
	Confirm.Visible = false
	task.wait(2)
	GUI.Visible = false
	Cutscene()
end)

Made A template if anyone wants it, now I just need to add headbobbing which was the entire purpose of this forum post.

-- I recommend storing this script in a StarterGUI 
-- Create a folder called "CameraNodes" in Replicated Storage
-- Store all camera nodes in this folder
-- Use the naming scheme CameraNodeAAA, CameraNodeAAB, [...etc], CamerNodeABA, [etc]
--Using this naming scheme means Roblox can accurately sort the nodes, and gives you 17,576 possible cameras
-- If you ever require more cameras add an extra letter to the end of the naming scheme, this will give you 456,976 possible cameras
-- In the Cutscene editor the letters will be translated into numbers 
-- On line 74 you will find the cutscene editor
-- It has 2 examples of separated camera movements
-- And an example of combined camera movements
-- Call the Cutscene with Cutscene()
-- Any calls to the cutscene must be at the bottom of this script
-- This can be put in a RemoteEvent e.g. 
-- StartCutscene.OnServerEvent:Connect(Cutscene)
-- Or a GUI click detector e.g.
-- Confirm.MouseButton1Click:Connect(Cutscene)

-- Roblox Service Imports
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Replicated Storage Imports
local StartVehicleMovementEvent = ReplicatedStorage:WaitForChild("StartVehicleMovement")
local CameraNodes = game.ReplicatedStorage:WaitForChild("CameraNodes"):GetChildren()

-- Detecting Camera and Enabling Scripting
local Camera = game.Workspace.CurrentCamera
Camera.CameraType = Enum.CameraType.Scriptable

-- Sorting Camera Nodes Into Correct Order
table.sort(CameraNodes, function(a, b)
	return a.Name < b.Name
end)

-- Camera Movement Function
local function MoveCamera(StartPart, TargetPart, speed)
	
	-- Set up variables for lerp
	local startCFrame = StartPart.CFrame
	local targetCFrame = TargetPart.CFrame
	local startPosition = StartPart.Position
	local endPosition = TargetPart.Position
	local duration = (startPosition - endPosition).Magnitude / speed -- Duration for each segment
	local runningTime = 0
	
	-- Initial Camera Position Setup
	Camera.CFrame = startCFrame
	local lerp
	
	-- Camera Movement
	local completed = false
	lerp = RunService.Heartbeat:Connect(function(deltaTime)
		
		-- Movement Time Calculations
		runningTime += deltaTime
		local alpha = runningTime / duration
		
		-- Camera Movement
		Camera.CFrame = startCFrame:Lerp(targetCFrame, alpha)
		
		-- Completed Movement Check
		if alpha >= 1 then
			lerp:Disconnect()
			completed = true
		end
	end)
	
	-- Wait Until Movement Is Completed
	while not completed do
		task.wait()
	end
end

-- Cutscene Editor 
local function Cutscene()
	-- Seperate Movements
	
	-- First Camera, Second Camera, Speed(Highly Sensitive Use Decimals)
	MoveCamera(CameraNodes[1], CameraNodes[2], 1.75)
	MoveCamera(CameraNodes[3], CameraNodes[4], 1.75)
	
	-- Combined Movements
	local combinedmovementspeed = 0.5
	-- First Camera, Second To Last Camera
	for i = 5, 14 do
		local startCam = CameraNodes[i]
		local endCam = CameraNodes[i+1]
		MoveCamera(startCam, endCam, combinedmovementspeed)
	end
end

-- Call Cutscene() after this line




-- A script by OliAPerson

Model if anyone wants it