Character Jittering with Over The Shoulder Camera System

Hey! I am working on a weapon system that uses an over-the-shoulder (OTS) camera system. Arbeiters originally authored the OTS system, and I have since taken it and made changes to it to better fit my needs.

My current issue is that characters seem to be a tad jittery/glitchy when trying to follow the camera. I have attempted to mess with the formula, and to no avail, it’s still a problem. The camera update does use RenderStepped and the camera is quite fluid. It is solely the character alignment that is having issues.

I could use some help here from anyone familiar with camera systems. Thanks!

What the issue looks like:

The issue is more prominent when in the zoomed/aim state:

I have no issue providing the entirety of the script because it was originally open-source, and should stay that way:

local OTS_Cam = {}

-- ROBLOX Services
local PlayersService = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

assert(not RunService:IsServer(), "Error: OTS_Cam should only be run on the Client!")

-- CONSTANTS
OTS_Cam.Player = PlayersService.LocalPlayer or PlayersService.PlayerAdded:Wait()

local SHOULDER_DIRECTION = {RIGHT = 1, LEFT = -1}


OTS_Cam.settings = {
	Default = {
		FOV = 70,
		Offset = Vector3.new(2.5, 2.5, 8),
		VerticalAngleLimits = NumberRange.new(-45, 45),
		Sensitivity = 3,
		LerpSpeed = 0.5
	},
	
	Zoomed = {
		FOV = 40,
		Offset = Vector3.new(2, 2, 3),
		VerticalAngleLimits = NumberRange.new(-45, 45),
		Sensitivity = 1.5,
		LerpSpeed = 0.5
	}
}

OTS_Cam.SavedCameraSettings = nil
OTS_Cam.SavedMouseBehavior = nil
OTS_Cam.CurrentSettings = nil
OTS_Cam.CurrentMode = nil

OTS_Cam.HorizontalAngle = 0
OTS_Cam.VerticalAngle = 0
OTS_Cam.ShoulderDirection = SHOULDER_DIRECTION.RIGHT

OTS_Cam.IsCharacterAligned = false
OTS_Cam.IsMouseLocked = false
OTS_Cam.IsEnabled = false

local function Lerp(x, y, a)
	return x + (y - x) * a
end

function OTS_Cam.SetCameraMode(cameraMode: string)
	assert(cameraMode ~= nil, "Error: Argument 1 nil or missing")
	assert(typeof(cameraMode) == "string", "Error: string expected, got " .. typeof(cameraMode))
	assert(OTS_Cam.settings[cameraMode] ~= nil, "Error: Attempt set an unrecognized CameraMode " .. cameraMode)
	if OTS_Cam.IsEnabled == false then
		warn("Warning: Attempt to change CameraMode without enabling OTS Cam")
		return
	end
	
	OTS_Cam.CurrentMode = cameraMode
	OTS_Cam.CurrentSettings = OTS_Cam.settings[cameraMode]
end

function OTS_Cam.SetAlignCharacter(alignCharacter: boolean)
	assert(alignCharacter ~= nil, "Error: Argument 2 nil or missing")
	assert(typeof(alignCharacter) == "boolean", "Error: boolean expected, got " .. typeof(alignCharacter))
	OTS_Cam.IsCharacterAligned = alignCharacter
	
	-- Avoid awkward fighting between character movement rotation and camera alignment rotation
	local humanoid = OTS_Cam.Player.Character and OTS_Cam.Player.Character:FindFirstChild("Humanoid")
	if alignCharacter then
		if humanoid then
			humanoid.AutoRotate = false
		end
	else
		if humanoid then
			humanoid.AutoRotate = true
		end
	end
end

function OTS_Cam.SetMouseStep(steppedIn: boolean)
	assert(steppedIn ~= nil, "Error: Argument 1 nil or missing")
	assert(typeof(steppedIn) == "boolean", "Error: boolean expected, got " .. typeof(steppedIn))
	if OTS_Cam.IsEnabled == false then
		warn("Warning: Attempt to change MouseStep without enabling OTS Cam")
		return
	end
	
	OTS_Cam.IsMouseLocked = steppedIn
	if steppedIn then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
	else
		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
	end
end

function OTS_Cam.SetShoulderDirection()
	if OTS_Cam.ShoulderDirection == -1 then
		OTS_Cam.ShoulderDirection = 1
	else
		OTS_Cam.ShoulderDirection = -1
	end
end

function OTS_Cam.SaveDefaultCameraSettings()
	local currentCamera = workspace.CurrentCamera
	OTS_Cam.SavedCameraSettings = {
		FieldOfView = currentCamera.FieldOfView,
		CameraSubject = currentCamera.CameraSubject,
		CameraType = currentCamera.CameraType
	}
	OTS_Cam.SavedMouseBehavior = UserInputService.MouseBehavior
end

function OTS_Cam.LoadDefaultCameraSettings()
	local currentCamera = workspace.CurrentCamera
	for setting, value in OTS_Cam.SavedCameraSettings do
		currentCamera[setting] = value
	end
	UserInputService.MouseBehavior = OTS_Cam.SavedMouseBehavior
end

function OTS_Cam.Update()
	local currentCamera = workspace.CurrentCamera
	local currentCameraSettings = OTS_Cam.CurrentSettings
	
	currentCamera.CameraType = Enum.CameraType.Scriptable
	
	if OTS_Cam.IsMouseLocked then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
	else
		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
	end
	
	local mouseDelta = UserInputService:GetMouseDelta() * currentCameraSettings.Sensitivity
	OTS_Cam.HorizontalAngle -= mouseDelta.X/currentCamera.ViewportSize.X
	OTS_Cam.VerticalAngle -= mouseDelta.Y/currentCamera.ViewportSize.Y
	OTS_Cam.VerticalAngle = math.rad(math.clamp(math.deg(OTS_Cam.VerticalAngle), currentCameraSettings.VerticalAngleLimits.Min, currentCameraSettings.VerticalAngleLimits.Max))
	
	local character = OTS_Cam.Player.Character
	local humanoidRootPart = (character ~= nil) and (character:FindFirstChild("HumanoidRootPart"))
	if humanoidRootPart then
		currentCamera.FieldOfView = Lerp(
			currentCamera.FieldOfView,
			currentCameraSettings.FOV,
			currentCameraSettings.LerpSpeed
		)
		
		local offset = currentCameraSettings.Offset
		offset = Vector3.new(offset.X * OTS_Cam.ShoulderDirection, offset.Y, offset.Z)
		
		local newCameraCFrame = CFrame.new(humanoidRootPart.Position) *
			CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0) *
			CFrame.Angles(OTS_Cam.VerticalAngle, 0, 0) *
			CFrame.new(offset)
		
		newCameraCFrame = currentCamera.CFrame:Lerp(newCameraCFrame, currentCameraSettings.LerpSpeed)
		
		-- Character alignment
		if OTS_Cam.IsCharacterAligned then
			local newHumanoidRootPartCFrame = CFrame.new(humanoidRootPart.Position) *
				CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0)
			humanoidRootPart.CFrame = humanoidRootPart.CFrame:Lerp(newHumanoidRootPartCFrame, currentCameraSettings.LerpSpeed)
		end
		
		currentCamera.CFrame = newCameraCFrame
	end
end

function OTS_Cam.ConfigureStateForEnabled()
	-- Initialize settings
	OTS_Cam.SaveDefaultCameraSettings()
	OTS_Cam.SetCameraMode("Default")
	if OTS_Cam.CurrentSettings.AlignCharacter then
		OTS_Cam.IsCharacterAligned = true
	end
	OTS_Cam.SetMouseStep(true)
	OTS_Cam.SetShoulderDirection(SHOULDER_DIRECTION.RIGHT)
	
	-- Calculate angles
	local cameraCFrame = workspace.CurrentCamera.CFrame
	local x, y, z = cameraCFrame:ToOrientation()
	
	OTS_Cam.HorizontalAngle = y
	OTS_Cam.VerticalAngle = x
end

function OTS_Cam.ConfigureStateForDisabled()
	OTS_Cam.LoadDefaultCameraSettings()
	OTS_Cam.SetCameraMode("Default")
	OTS_Cam.SetShoulderDirection(SHOULDER_DIRECTION.RIGHT)
	OTS_Cam.SetMouseStep(false)
	OTS_Cam.HorizontalAngle = 0
	OTS_Cam.VerticalAngle = 0
end

function OTS_Cam.Enable()
	assert(OTS_Cam.IsEnabled == false, "Error: Attempt to enable OTS Cam while already enabled")
	
	OTS_Cam.IsEnabled = true
	OTS_Cam.ConfigureStateForEnabled()
	
	-- Bind update to renderstep
	RunService:BindToRenderStep(
		"OTS_Cam",
		Enum.RenderPriority.Camera.Value - 10,
		function()
			if OTS_Cam.IsEnabled then
				OTS_Cam.Update()
			end
		end
	)
end

function OTS_Cam.Disable()
	assert(OTS_Cam.IsEnabled == true, "Error: Attempt to disable OTS Cam while already disabled")
	
	OTS_Cam.ConfigureStateForDisabled()
	OTS_Cam.IsEnabled = false
	
	-- Unbind update from renderstep
	RunService:UnbindFromRenderStep("OTS_Cam")
end

UserInputService.InputBegan:Connect(function(inputObject, gameProcessedEvent)
	if gameProcessedEvent == false and OTS_Cam.IsEnabled then
		if inputObject.KeyCode == Enum.KeyCode.Z then
			OTS_Cam.SetShoulderDirection()
		end
		if inputObject.UserInputType == Enum.UserInputType.MouseButton2 then
			OTS_Cam.SetCameraMode("Zoomed")
		end
		if inputObject.KeyCode == Enum.KeyCode.LeftControl then
			if OTS_Cam.IsEnabled then
				OTS_Cam.SetMouseStep(not OTS_Cam.IsMouseLocked)
			end
		end
	end
end)

UserInputService.InputEnded:Connect(function(inputObject, gameProcessedEvent)
	if gameProcessedEvent == false and OTS_Cam.IsEnabled then
		if inputObject.UserInputType == Enum.UserInputType.MouseButton2 then
			OTS_Cam.SetCameraMode("Default")
		end
	end
end)

return OTS_Cam
2 Likes

I had the same problem with my camera system

I think I fixed it by trying all of the RunService functions like .Stepped and .Heartbeat to move the camera each frame

Another thing you can do is set the camera subject to a part and then weld the part
The camera will never jitter when it’s subject to a part

The only reason I haven’t used Heartbeat is because it fires after simulation has been completed. Whereas RenderStepped, which is what I use, is fired before the frame renders or simulates if I am not mistaken.

This is what creates a fluid camera. My camera is very fluid, it’s the character that is jittery. Something is wrong with the character alignment formula (line 160) and is causing the character to jitter as you see in the videos.

Removing this line (effectively removing any camera lerping) fixes the character jittering. Doing this creates a snap-like camera with no smoothness, which I do not want. I am still at a loss on how to continue to lerp the camera without causing the character to jitter. Although, since removing this line made the jittering disappear I can only assume it’s the camera lerp’s fault and not the character lerp’s fault.

Maybe try changing the renderstep priority value? Looking at the microprofiler with the script im seeing that the camera script runs after the control module. Try replacing -10 with Enum.RenderPriority.Camera.Value or Enum.RenderPriority.Camera.Value plus or minus 1?

Unfortunately, the issue still persists with plus/minus 1

This approach to smoothing is indeed the source of your problem, but it’s a little bit tricky to clearly explain. TLDR; is that Lerping the camera like this will only be smooth if your render framerate is perfectly constant. Any irregularity in frame durations (FPS drops) will jerk the camera backwards relative to the character and you’ll directly see the jitter of your unsteady FPS in the relative position of your camera to the character.

Simply put, “move halfway from last rendered position to currentdirectly couples velocity of the camera to frame duration.

Your renderstep binding is fine, the issue is that the character is moved on the physics step (Stepped) and the camera is moved on RenderStepped, and these two events are not in sync, Stepped can have a longer timestep then RenderStepped, and vice versa.

If you’re always positioning the camera relative to the character each frame (like when the Lerp is removed), your character will not jitter because they are locked in sync every frame. But, what your Lerp does every frame is move the camera to a point halfway between where it was last frame (last RenderStepped) and where the hard-sync would place it on the current frame. If your avatar is running at a nice constant FPS, but you have some irregularity in your render thread FPS, your interpolation rate inherits that irregularity. Anytime your renderstep is long due to FPS drop, your midpoint Lerp is going to make the camera cover half the distance since the last render frame.

Put another way, if your computer we to manage perfect 60fps physics step and 60fps render step, everything would be smooth and the camera would accelerate smoothly. But if your render framerate cannot keep up, everytime you have a longer than usual time between rendersteps, the Lerp will make the camera lag behind a lot more than expected, making it not smoothly recede from the character position and then catch up smoothly when the character stops.

The way to solve this is to use the time delta parameter you get on RenderStepped (passing it through to your Update() function) and do 1 of 2 things with it:

  1. You can accumulate and subdivide the actual elapse time into a fixed timestep and apply your lerp to the interpolated fixed-step positions of the character. Or,

  2. You can change the camera’s whole movement model to be in terms of a max velocity and acceleration, and do actual integration with the time delta, either Euler or Verlet would work just fine, so that the camera smoothly accelerates up to max velocity (presumably the character’s) and back to zero. Likewise for angular velocity.

1 Like

First of all, thank you for the very detailed explanation. That helped me comprehend why the jittering is happening.

However, I attempted solution 1 and somehow made it worse haha.

Here is the new Update() method:

local accumulatedTime = 0
local fixedStep = 1 / 60 -- Assuming 60 FPS as an example

function OTS_Cam.Update(dt)
	accumulatedTime += dt
	while accumulatedTime >= fixedStep do
		local currentCamera = workspace.CurrentCamera
		local currentCameraSettings = OTS_Cam.CurrentSettings

		currentCamera.CameraType = Enum.CameraType.Scriptable

		if OTS_Cam.IsMouseLocked then
			UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
		else
			UserInputService.MouseBehavior = Enum.MouseBehavior.Default
		end

		local mouseDelta = UserInputService:GetMouseDelta() * currentCameraSettings.Sensitivity
		OTS_Cam.HorizontalAngle -= mouseDelta.X/currentCamera.ViewportSize.X
		OTS_Cam.VerticalAngle -= mouseDelta.Y/currentCamera.ViewportSize.Y
		OTS_Cam.VerticalAngle = math.rad(math.clamp(math.deg(OTS_Cam.VerticalAngle), currentCameraSettings.VerticalAngleLimits.Min, currentCameraSettings.VerticalAngleLimits.Max))

		local character = OTS_Cam.Player.Character
		local humanoidRootPart = (character ~= nil) and (character:FindFirstChild("HumanoidRootPart"))
		if humanoidRootPart then
			currentCamera.FieldOfView = Lerp(
				currentCamera.FieldOfView,
				currentCameraSettings.FOV,
				fixedStep * currentCameraSettings.LerpSpeed
			)

			local offset = currentCameraSettings.Offset
			offset = Vector3.new(offset.X * OTS_Cam.ShoulderDirection, offset.Y, offset.Z)

			local newCameraCFrame = CFrame.new(humanoidRootPart.Position) *
				CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0) *
				CFrame.Angles(OTS_Cam.VerticalAngle, 0, 0) *
				CFrame.new(offset)

			--// Raycast for obstructions //--
			local raycastParams = RaycastParams.new()
			raycastParams.FilterDescendantsInstances = {character}
			raycastParams.FilterType = Enum.RaycastFilterType.Exclude
			local raycastResult = workspace:Raycast(
				humanoidRootPart.Position,
				newCameraCFrame.p - humanoidRootPart.Position,
				raycastParams
			)
			----

			--// Address obstructions if any //--
			if (raycastResult ~= nil) then
				local obstructionDisplacement = (raycastResult.Position - humanoidRootPart.Position)
				local obstructionPosition = humanoidRootPart.Position + (obstructionDisplacement.Unit * (obstructionDisplacement.Magnitude - 0.1))
				local x,y,z,r00,r01,r02,r10,r11,r12,r20,r21,r22 = newCameraCFrame:components()
				newCameraCFrame = CFrame.new(obstructionPosition.x, obstructionPosition.y, obstructionPosition.z, r00, r01, r02, r10, r11, r12, r20, r21, r22)
			end
			----

			newCameraCFrame = currentCamera.CFrame:Lerp(newCameraCFrame, fixedStep * currentCameraSettings.LerpSpeed)

			-- Character alignment
			if OTS_Cam.IsCharacterAligned then
				local newHumanoidRootPartCFrame = CFrame.new(humanoidRootPart.Position) *
					CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0)
				humanoidRootPart.CFrame = humanoidRootPart.CFrame:Lerp(newHumanoidRootPartCFrame, fixedStep * currentCameraSettings.LerpSpeed/2)
			end

			currentCamera.CFrame = newCameraCFrame
		end

		accumulatedTime -= fixedStep
	end
end

There’s a little bit more to it, inside the fixed timestep loop where ever you use humanoidRootPart.CFrame or Position, you actually need to work with the interpolated version of that CFrame too. In other words, you need to use Lerp on the HRP CFrame between last renderframe’s value and current, toresample the path (and orientation) of the HRP at a fixed time interval.

I guess I am confused. Is that not accomplished by this line already? Sorry, not grasping this as well as I thought I was.

Right, sorry, that’s not what I meant. Ideally, you don’t want to be setting the HumanoidRootPart.CFrame at all from within RenderStepped, since it’s a physically-simulated part, unlike the camera. But that’s a different issue altogether.

I think it would probably be simplest just to compute the sample point as you were already doing, for the current RenderStep’s target position, and then just use the dt with simple Euler integration of the camera position so that you have totally framerate-independent control over the acceleration and max velocity of the camera. You can just Lerp the camera orientation proportional to the linear velocity. This is much easer IMO than trying to break up long rendersteps into fixed steps in order to apply the 50% Lerp, because that gets really messy; you can’t just rollover accumulated fractional frames into the next renderstep, or you just’ve just changed the source of the jitter. You have to actually do another proportional Lerp to handle fractional steps within the same renderstep.

Either way though, it’s best to not resample the HRP, since you don’t need to. You only need to work with the camera CFrame to do smoothing.

this is exactly why you need to use heartbeat. if you connect on renderstepped you first set the cframe of the camera, and then the character moves due to the physics calculations, and then the frame is drawn.

I gave this a shot. The camera seems smooth and I don’t think I’ve noticed character jittering, however, the interpolation is SUPER slow. I have tried multiplying the velocity by a scale factor to speed it up but it doesn’t seem to do anything other than glitch the camera out.

  • New Update() method
function OTS_Cam.Update(dt)
	local currentCamera = workspace.CurrentCamera
	local currentCameraSettings = OTS_Cam.CurrentSettings

	currentCamera.CameraType = Enum.CameraType.Scriptable

	if OTS_Cam.IsMouseLocked then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
	else
		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
	end

	local mouseDelta = UserInputService:GetMouseDelta() * currentCameraSettings.Sensitivity
	OTS_Cam.HorizontalAngle -= mouseDelta.X/currentCamera.ViewportSize.X
	OTS_Cam.VerticalAngle -= mouseDelta.Y/currentCamera.ViewportSize.Y
	OTS_Cam.VerticalAngle = math.rad(math.clamp(math.deg(OTS_Cam.VerticalAngle), currentCameraSettings.VerticalAngleLimits.Min, currentCameraSettings.VerticalAngleLimits.Max))

	local character = OTS_Cam.Player.Character
	local humanoidRootPart = (character ~= nil) and (character:FindFirstChild("HumanoidRootPart"))
	if humanoidRootPart then
		currentCamera.FieldOfView = Lerp(
			currentCamera.FieldOfView,
			currentCameraSettings.FOV,
			0.1
		)

		local offset = currentCameraSettings.Offset
		offset = Vector3.new(offset.X * OTS_Cam.ShoulderDirection, offset.Y, offset.Z)

		local newCameraCFrame = CFrame.new(humanoidRootPart.Position) *
			CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0) *
			CFrame.Angles(OTS_Cam.VerticalAngle, 0, 0) *
			CFrame.new(offset)

		-- Euler integration for the camera position
		local targetPosition = newCameraCFrame.p
		local currentPosition = currentCamera.CFrame.p
		local velocity = (targetPosition - currentPosition) * dt
		local newPosition = currentPosition + velocity

		-- Lerp the camera orientation proportional to the linear velocity
		local targetOrientation = newCameraCFrame - newCameraCFrame.p
		local currentOrientation = currentCamera.CFrame - currentCamera.CFrame.p
		local newOrientation = currentOrientation:Lerp(targetOrientation, velocity.Magnitude)

		newCameraCFrame = CFrame.new(newPosition) * newOrientation

		-- Character alignment
		if OTS_Cam.IsCharacterAligned then
			local newHumanoidRootPartCFrame = CFrame.new(humanoidRootPart.Position) *
				CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0)
			humanoidRootPart.CFrame = humanoidRootPart.CFrame:Lerp(newHumanoidRootPartCFrame, 0.5)
		end

		currentCamera.CFrame = newCameraCFrame
	end
end

I have tried both the PostSimulation and Heartbeat events provided by RunService, and neither solved the issue. From the digging I have done while trying to solve this issue, most people recommend camera manipulation be done with RenderStepped, and any part-simulation done with Heartbeat or Stepped (which I intend to move the HumanoidRootPart manipulation to once I’ve solved this camera problem).

I found a nice workaround solution to this. It no longer involves lerping the entirety of the camera’s CFrame based on the humanoidrootpart’s position. Instead, I have opted to use Nevermore’s Spring module to simulate lateral and zed movement of the camera and a Spring to smoothen the shoulder offset change. You can find the Spring API here. Both instances of the Spring class I created (shoulderSpring and movementSpring) act independently of the humanoidrootpart to avoid the jittering previously observed. Specifically, the movementSpring is influenced by player input (WASD) to simulate the camera “lagging” a short distance behind the player’s character movement.

I have also included a new method to disable this lateral and zed movement simulation called SetXZMovementSimulated(simulateMovement: boolean).

In addition, I moved the humanoidrootpart rotating change to update when Heartbeat is fired instead of wrapping it inside RenderStepped with the camera changes.

While this may not be a direct solution, it works quite well for me and I am content with moving on to the next parts of the project I am working on. Thank you to those of you who offered help.

Here is how the camera looks now (notice the jittering is nonexistent):

Below is the updated code for anyone who is struggling now or in the future with this issue:

local OTS_Cam = {}

-- ROBLOX Services
local PlayersService = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local SpringService = require(script.Spring)

assert(not RunService:IsServer(), "Error: OTS_Cam should only be run on the Client!")

-- CONSTANTS
OTS_Cam.Player = PlayersService.LocalPlayer or PlayersService.PlayerAdded:Wait()

local SHOULDER_DIRECTION = {RIGHT = 1, LEFT = -1}

OTS_Cam.settings = {
	Default = {
		FOV = 70,
		Offset = Vector3.new(2.5, 2.5, 8),
		VerticalAngleLimits = NumberRange.new(-45, 45),
		Sensitivity = 3,
		LerpSpeed = 20
	},
	
	Zoomed = {
		FOV = 40,
		Offset = Vector3.new(2, 2, 3),
		VerticalAngleLimits = NumberRange.new(-45, 45),
		Sensitivity = 1.5,
		LerpSpeed = 20
	}
}

-- Springs to handle camera smoothness
OTS_Cam.shoulderSpring = SpringService.new(0)
OTS_Cam.movementSpring = SpringService.new(Vector3.zero)

OTS_Cam.SavedCameraSettings = nil
OTS_Cam.SavedMouseBehavior = nil
OTS_Cam.CurrentSettings = nil
OTS_Cam.CurrentMode = nil

OTS_Cam.HorizontalAngle = 0
OTS_Cam.VerticalAngle = 0
OTS_Cam.ShoulderDirection = SHOULDER_DIRECTION.RIGHT

OTS_Cam.IsCharacterAligned = false
OTS_Cam.IsMouseLocked = false
OTS_Cam.IsEnabled = false
OTS_Cam.IsXZMovementSimulated = true

local function Lerp(x, y, a)
	return x + (y - x) * a
end

function OTS_Cam.SetCameraMode(cameraMode: string)
	assert(OTS_Cam.IsEnabled == true, "Error: Attempt to set CameraMode while OTS_Cam is disabled")
	assert(cameraMode ~= nil, "Error: Argument 1 nil or missing")
	assert(typeof(cameraMode) == "string", "Error: string expected, got " .. typeof(cameraMode))
	assert(OTS_Cam.settings[cameraMode] ~= nil, "Error: Attempt set an unrecognized CameraMode " .. cameraMode)
	if OTS_Cam.IsEnabled == false then
		warn("Warning: Attempt to change CameraMode without enabling OTS Cam")
		return
	end
	
	OTS_Cam.CurrentMode = cameraMode
	OTS_Cam.CurrentSettings = OTS_Cam.settings[cameraMode]
	OTS_Cam.shoulderSpring.Speed = OTS_Cam.CurrentSettings.LerpSpeed
	OTS_Cam.movementSpring.Speed = OTS_Cam.CurrentSettings.LerpSpeed
end

function OTS_Cam.SetAlignCharacter(alignCharacter: boolean)
	assert(OTS_Cam.IsEnabled == true, "Error: Attempt to set AlignCharacter while OTS_Cam is disabled")
	assert(alignCharacter ~= nil, "Error: Argument 1 nil or missing")
	assert(typeof(alignCharacter) == "boolean", "Error: boolean expected, got " .. typeof(alignCharacter))
	OTS_Cam.IsCharacterAligned = alignCharacter
	
	-- Avoid awkward fighting between character movement rotation and camera alignment rotation
	local humanoid = OTS_Cam.Player.Character and OTS_Cam.Player.Character:FindFirstChild("Humanoid")
	if alignCharacter then
		if humanoid then
			humanoid.AutoRotate = false
		end
	else
		if humanoid then
			humanoid.AutoRotate = true
		end
	end
end

function OTS_Cam.SetMouseStep(steppedIn: boolean)
	assert(OTS_Cam.IsEnabled == true, "Error: Attempt to set MouseStep while OTS_Cam is disabled")
	assert(steppedIn ~= nil, "Error: Argument 1 nil or missing")
	assert(typeof(steppedIn) == "boolean", "Error: boolean expected, got " .. typeof(steppedIn))
	if OTS_Cam.IsEnabled == false then
		warn("Warning: Attempt to change MouseStep without enabling OTS Cam")
		return
	end
	
	OTS_Cam.IsMouseLocked = steppedIn
	if steppedIn then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
	else
		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
	end
end

function OTS_Cam.SetShoulderDirection()
	if OTS_Cam.ShoulderDirection == -1 then
		OTS_Cam.ShoulderDirection = 1
	else
		OTS_Cam.ShoulderDirection = -1
	end
end

function OTS_Cam.SetXZMovementSimulated(simulateMovement: boolean)
	assert(OTS_Cam.IsEnabled == true, "Error: Attempt to set XZMovementSimulated while OTS_Cam is disabled")
	assert(simulateMovement ~= nil, "Error: Argument 1 nil or missing")
	assert(typeof(simulateMovement) == "boolean", "Error: boolean expected, got " .. typeof(simulateMovement))
	OTS_Cam.IsXZMovementSimulated = simulateMovement
end

function OTS_Cam.SaveDefaultCameraSettings()
	local currentCamera = workspace.CurrentCamera
	OTS_Cam.SavedCameraSettings = {
		FieldOfView = currentCamera.FieldOfView,
		CameraSubject = currentCamera.CameraSubject,
		CameraType = currentCamera.CameraType
	}
	OTS_Cam.SavedMouseBehavior = UserInputService.MouseBehavior
end

function OTS_Cam.LoadDefaultCameraSettings()
	local currentCamera = workspace.CurrentCamera
	for setting, value in OTS_Cam.SavedCameraSettings do
		currentCamera[setting] = value
	end
	UserInputService.MouseBehavior = OTS_Cam.SavedMouseBehavior
end

function OTS_Cam.Update(dt)
	local currentCamera = workspace.CurrentCamera
	local currentCameraSettings = OTS_Cam.CurrentSettings
	
	currentCamera.CameraType = Enum.CameraType.Scriptable
	
	if OTS_Cam.IsMouseLocked then
		UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
	else
		UserInputService.MouseBehavior = Enum.MouseBehavior.Default
	end
	
	-- Check for character movement and adjust simulated movement values for the spring
	local movementVector = Vector3.zero
	if UserInputService:IsKeyDown(Enum.KeyCode.D) then
		movementVector += Vector3.new(-0.1, 0, 0)
	end
	if UserInputService:IsKeyDown(Enum.KeyCode.A) then
		movementVector += Vector3.new(0.1, 0, 0)
	end
	if UserInputService:IsKeyDown(Enum.KeyCode.W) then
		movementVector += Vector3.new(0, 0, 0.2)
	end
	if UserInputService:IsKeyDown(Enum.KeyCode.S) then
		movementVector += Vector3.new(0, 0, -0.2)
	end
	OTS_Cam.movementSpring.Target = movementVector
	if not OTS_Cam.IsXZMovementSimulated then
		OTS_Cam.movementSpring.Target = Vector3.zero
		OTS_Cam.movementSpring.Position = Vector3.zero
	end
	
	local mouseDelta = UserInputService:GetMouseDelta() * currentCameraSettings.Sensitivity
	OTS_Cam.HorizontalAngle -= mouseDelta.X/currentCamera.ViewportSize.X
	OTS_Cam.VerticalAngle -= mouseDelta.Y/currentCamera.ViewportSize.Y
	OTS_Cam.VerticalAngle = math.rad(math.clamp(math.deg(OTS_Cam.VerticalAngle), currentCameraSettings.VerticalAngleLimits.Min, currentCameraSettings.VerticalAngleLimits.Max))
	
	local character = OTS_Cam.Player.Character
	local humanoidRootPart = (character ~= nil) and (character:FindFirstChild("HumanoidRootPart"))
	if humanoidRootPart then
		currentCamera.FieldOfView = Lerp(
			currentCamera.FieldOfView,
			currentCameraSettings.FOV,
			dt * currentCameraSettings.LerpSpeed
		)
		
		OTS_Cam.shoulderSpring.Target = OTS_Cam.ShoulderDirection
		local springAdjustedShoulderDirection = OTS_Cam.shoulderSpring.Position
		local offset = currentCameraSettings.Offset
		offset = Vector3.new(offset.X * springAdjustedShoulderDirection, offset.Y, offset.Z)
		
		local springAdjustedMovementVector = OTS_Cam.movementSpring.Position
		local newCameraCFrame = CFrame.new(humanoidRootPart.Position) *
			CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0) *
			CFrame.Angles(OTS_Cam.VerticalAngle, 0, 0) *
			CFrame.new(offset) *
			CFrame.new(springAdjustedMovementVector)
		
		--// Raycast for obstructions //--
		local raycastParams = RaycastParams.new()
		raycastParams.FilterDescendantsInstances = {character}
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		local raycastResult = workspace:Raycast(
			humanoidRootPart.Position,
			newCameraCFrame.Position - humanoidRootPart.Position,
			raycastParams
		)
		----

		--// Address obstructions if any //--
		if (raycastResult ~= nil) then
			local obstructionDisplacement = (raycastResult.Position - humanoidRootPart.Position)
			local obstructionPosition = humanoidRootPart.Position + (obstructionDisplacement.Unit * (obstructionDisplacement.Magnitude - 0.1))
			local x,y,z,r00,r01,r02,r10,r11,r12,r20,r21,r22 = newCameraCFrame:components()
			newCameraCFrame = CFrame.new(obstructionPosition.x, obstructionPosition.y, obstructionPosition.z, r00, r01, r02, r10, r11, r12, r20, r21, r22)
		end
		----
		
		currentCamera.CFrame = newCameraCFrame
	end
end

function OTS_Cam.Heartbeat(dt: number)
	local currentCamera = workspace.CurrentCamera
	local currentCameraSettings = OTS_Cam.CurrentSettings
	
	local humanoidRootPart = OTS_Cam.Player.Character and OTS_Cam.Player.Character:FindFirstChild("HumanoidRootPart")
	-- Character alignment
	if OTS_Cam.IsCharacterAligned then
		local newHumanoidRootPartCFrame = CFrame.new(humanoidRootPart.Position) *
			CFrame.Angles(0, OTS_Cam.HorizontalAngle, 0)
		humanoidRootPart.CFrame = humanoidRootPart.CFrame:Lerp(newHumanoidRootPartCFrame, dt * currentCameraSettings.LerpSpeed/1.5)
	end
end

function OTS_Cam.ConfigureStateForEnabled()
	-- Initialize settings
	OTS_Cam.SaveDefaultCameraSettings()
	OTS_Cam.SetCameraMode("Default")
	if OTS_Cam.CurrentSettings.AlignCharacter then
		OTS_Cam.IsCharacterAligned = true
	end
	OTS_Cam.SetMouseStep(true)
	
	-- Calculate angles
	local cameraCFrame = workspace.CurrentCamera.CFrame
	local x, y, z = cameraCFrame:ToOrientation()
	
	OTS_Cam.HorizontalAngle = y
	OTS_Cam.VerticalAngle = x
	
	-- Set initial spring values
	OTS_Cam.shoulderSpring.Target = OTS_Cam.ShoulderDirection
	OTS_Cam.shoulderSpring.Position = OTS_Cam.ShoulderDirection
	OTS_Cam.movementSpring.Target = Vector3.zero
	OTS_Cam.movementSpring.Position = Vector3.zero
end

function OTS_Cam.ConfigureStateForDisabled()
	OTS_Cam.LoadDefaultCameraSettings()
	OTS_Cam.SetCameraMode("Default")
	OTS_Cam.SetMouseStep(false)
	OTS_Cam.SetAlignCharacter(false)
	OTS_Cam.HorizontalAngle = 0
	OTS_Cam.VerticalAngle = 0
	
	-- Reset spring values
	OTS_Cam.shoulderSpring.Target = 0
	OTS_Cam.shoulderSpring.Position = 0
	OTS_Cam.movementSpring.Target = Vector3.zero
	OTS_Cam.movementSpring.Position = Vector3.zero
end

function OTS_Cam.Enable()
	assert(OTS_Cam.IsEnabled == false, "Error: Attempt to enable OTS Cam while already enabled")
	
	OTS_Cam.IsEnabled = true
	OTS_Cam.ConfigureStateForEnabled()
	
	-- Bind update to renderstep
	RunService:BindToRenderStep(
		"OTS_Cam",
		Enum.RenderPriority.Camera.Value - 1,
		function(dt)
			if OTS_Cam.IsEnabled then
				OTS_Cam.Update(dt)
			end
		end
	)
	RunService.Heartbeat:Connect(function(dt)
		OTS_Cam.Heartbeat(dt)
	end)
end

function OTS_Cam.Disable()
	assert(OTS_Cam.IsEnabled == true, "Error: Attempt to disable OTS Cam while already disabled")
	
	OTS_Cam.ConfigureStateForDisabled()
	OTS_Cam.IsEnabled = false
	
	-- Re-enable auto rotate
	local humanoid = OTS_Cam.Player.Character and OTS_Cam.Player.Character:FindFirstChild("Humanoid")
	if humanoid then
		humanoid.AutoRotate = true
	end
	
	-- Unbind update from renderstep
	RunService:UnbindFromRenderStep("OTS_Cam")
end

UserInputService.InputBegan:Connect(function(inputObject, gameProcessedEvent)
	if gameProcessedEvent == false and OTS_Cam.IsEnabled then
		if inputObject.KeyCode == Enum.KeyCode.Z then
			OTS_Cam.SetShoulderDirection()
		end
		if inputObject.UserInputType == Enum.UserInputType.MouseButton2 then
			OTS_Cam.SetCameraMode("Zoomed")
		end
		if inputObject.KeyCode == Enum.KeyCode.LeftControl then
			if OTS_Cam.IsEnabled then
				OTS_Cam.SetMouseStep(not OTS_Cam.IsMouseLocked)
			end
		end
	end
end)

UserInputService.InputEnded:Connect(function(inputObject, gameProcessedEvent)
	if gameProcessedEvent == false and OTS_Cam.IsEnabled then
		if inputObject.UserInputType == Enum.UserInputType.MouseButton2 then
			OTS_Cam.SetCameraMode("Default")
		end
	end
end)

return OTS_Cam

its because ur updating that on the next step

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