Smooth Angled Camera Tweening (CFrame)

I’m trying to make a script that leans the character left or right and tilts their camera a little bit based off of which key they press (E or Q) but I can’t figure out how to tween the camera CFrame for the tilt smoothly.

I’ve tried looking for solutions on the forums but I can’t find anything that works or I can understand well enough to implement myself.

My Current Script
local character = script.Parent
local humanoid = character:WaitForChild("Humanoid")

local UIS = game:GetService("UserInputService")
local tweenService = game:GetService("TweenService")

local camera = game.Workspace.Camera

local tweenInfo = TweenInfo.new(
	0.25,
	Enum.EasingStyle.Linear	
)

UIS.InputBegan:Connect(function(key, isTyping)
	if key.KeyCode == Enum.KeyCode.E then
		tweenService:Create(humanoid, tweenInfo, {CameraOffset = Vector3.new(2, 0, 0)}):Play()
		game:GetService("RunService").RenderStepped:Connect(function()
			camera.CFrame = camera.CFrame * CFrame.Angles(0, 0, -.075)
		end)
	end
	
	if key.KeyCode == Enum.KeyCode.Q then
		tweenService:Create(humanoid, tweenInfo, {CameraOffset = Vector3.new(-2, 0, 0)}):Play()
		game:GetService("RunService").RenderStepped:Connect(function()
			camera.CFrame = camera.CFrame * CFrame.Angles(0,0,.075)
		end)
	end
end)

UIS.InputEnded:Connect(function(key)
	if key.KeyCode == Enum.KeyCode.E then
		tweenService:Create(humanoid, tweenInfo, {CameraOffset = Vector3.new(0, 0, 0)}):Play()
		game:GetService("RunService").RenderStepped:Connect(function()
			camera.CFrame = camera.CFrame * CFrame.Angles(0, 0, .075)
		end)
	elseif key.KeyCode == Enum.KeyCode.Q then
		tweenService:Create(humanoid, tweenInfo, {CameraOffset = Vector3.new(0, 0, 0)}):Play()
		game:GetService("RunService").RenderStepped:Connect(function()
			camera.CFrame = camera.CFrame * CFrame.Angles(0, 0, -.075)
		end)
	end
end)

Any help is greatly appreciated. Thanks so much in advance!

1 Like

Hello @Secerne! I took your script and tried to solve your problem. Below, you can see what I did. I hope this is what you want.

local character = script.Parent
local humanoid = character:WaitForChild("Humanoid")

local UIS = game:GetService("UserInputService")
local tweenService = game:GetService("TweenService")

local camera = game.Workspace.CurrentCamera

local tweenInfo = TweenInfo.new(
    0.25,
    Enum.EasingStyle.Linear
)

local cameraTilt = 0

local function UpdateCameraTilt(angle)
    local targetCFrame = camera.CFrame * CFrame.Angles(0, 0, angle)
    tweenService:Create(camera, tweenInfo, {CFrame = targetCFrame}):Play()
end

UIS.InputBegan:Connect(function(input, gameProcessedEvent)
    if gameProcessedEvent then
        return
    end

    if input.KeyCode == Enum.KeyCode.E then
        UpdateCameraTilt(-math.rad(10))
    elseif input.KeyCode == Enum.KeyCode.Q then
        UpdateCameraTilt(math.rad(10))
    end
end)

UIS.InputEnded:Connect(function(input)
    if input.KeyCode == Enum.KeyCode.E or input.KeyCode == Enum.KeyCode.Q then
        UpdateCameraTilt(0)
    end
end)

If you found this answer useful, do not forget to mark it as solved :white_check_mark:.

1 Like

One thing that I noticed in your script is that you’re not cleaning up RenderStepped connections. This is a memory leak. To prevent this, you have to disconnect “unbound” connections. That seems like a lot of work, right? Not really. Connections get disconnected once the instance they are connected to gets destroyed. You can’t destroy the RunService, so you have to keep track of these kinds of connections. Store them somewhere and disconnect them with the :Disconnect function once you don’t need them anymore. I’m not too sure how Roblox’s garbage collector works, but I once saw it disconnect all UserInputService connections in a script when it got destroyed. This didn’t happen when I used a module script, so I’m not yet sure.

Back to the topic. Here’s the script. Place it into StarterPlayerScripts:

local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")

local Camera = workspace.CurrentCamera

local TiltLength = 0.5
local TiltAngle = 30

local TiltDirection = Enum.EasingDirection.Out
local TiltStyle = Enum.EasingStyle.Cubic

local LeftConnections = {}
local RightConnections = {}

local LeftOffset = CFrame.identity
local RightOffset = CFrame.identity

local function CleanupConnections(list)
	for _, Connection in ipairs(list) do
		Connection:Disconnect()
	end

	table.clear(list)
end

local function LerpOffset(offset, left, cleanup)
	local OffsetCF = left and LeftOffset or RightOffset
	local Elapsed = 0

	local Connection
	Connection = RunService.RenderStepped:Connect(function(deltaTime)
		if Elapsed >= TiltLength and cleanup then
			Connection:Disconnect()
		end

		local Normal = math.clamp(Elapsed / TiltLength, 0, 1)
		Normal = TweenService:GetValue(Normal, TiltStyle, TiltDirection)

		local CF = OffsetCF:Lerp(
			offset,
			Normal
		)

		if left then
			LeftOffset = CF
		else
			RightOffset = CF
		end

		Camera.CFrame *= CF
		Elapsed += deltaTime
	end)

	return Connection
end

UserInputService.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then
		return
	end

	if input.KeyCode == Enum.KeyCode.Q then
		CleanupConnections(LeftConnections)

		local Connection = LerpOffset(
			CFrame.Angles(0, 0, math.rad(TiltAngle)),
			true
		)

		table.insert(LeftConnections, Connection)
	elseif input.KeyCode == Enum.KeyCode.E then
		CleanupConnections(RightConnections)

		local Connection = LerpOffset(
			CFrame.Angles(0, 0, math.rad(-TiltAngle)),
			false
		)

		table.insert(RightConnections, Connection)
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.Q then
		CleanupConnections(LeftConnections)

		local Connection = LerpOffset(
			CFrame.identity,
			true,
			true
		)

		table.insert(LeftConnections, Connection)
	elseif input.KeyCode == Enum.KeyCode.E then
		CleanupConnections(RightConnections)

		local Connection = LerpOffset(
			CFrame.identity,
			false,
			true
		)

		table.insert(RightConnections, Connection)
	end
end)

Here’s a demo:

There are 4 configuration variables. Tweak them to your liking.

I know this category is not for making entire scripts, but regardless. I wouldn’t say it looks the best, but it works well. Let me know if you have a question or want me to explain how it works.

2 Likes

Thank you so much for the help! I was kind of able to figure out what you did as well and I got it to work great with the camera moving too!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.