Custom camera bug

Hello developers. I currently in the midst of making a wizard roleplaying type of game, and for the game, I and (by far mostly) NeonD00m have made a camera script. (Note: Me and NeonD00m are not affiliated elsewhere, he helped me with the third person camera system and not other parts of the game).

Anyways, the system works pretty great in terms of functionality, it’s toggle-able, mobile supported (working on the Xbox support), collisions, and the character moves with the camera. But there is one major issue, a bug, that occurs when you have the camera angle either on the max or min of the Y axis. Allow me to explain:

In order for the camera not flip out or being able to look backwards there is a limit on how far up and down you can look with your camera. To illustrate:


The red outline shows the area of how high and low you can adjust your camera. This can be adjusted using the mouse, or touch on the phone.

You can only move the camera on the Y axis (semi freely) without the character cha
ging direction too. Check out the videos below to get what I mean:

As you can see the camera kind of gets stuck in that position when reaching Y axis limits. It’s a little hard to explain, so your best bet is probably to download the Roblox Studio file linked below, and check it out.

This is how the scripts ancestry is:
image

So, the code is here below: (main camera script)

------------------------------------------------------------------
-- Scripted by NeonD00m, special thanks to oxazolone and Spathi --
------------------------------------------------------------------
-- SETTINGS
local faceInCameraDirection = true

local primaryOffset = CFrame.new(1.75, 1.75, 0)--affects raycast starting point
local mainOffset = CFrame.new(0, 0, 5)--affects camera end point

local senseX = 1 --sense gets divided by 100 for some devices
local senseY = 1


-- SERVICES/IMPORTANT STUFF
local Input = game:GetService("UserInputService")
	Input.MouseBehavior = Enum.MouseBehavior.LockCenter
local cam = workspace.CurrentCamera
	cam.CameraType = Enum.CameraType.Scriptable

-- INSTANCES
local plr = game.Players.LocalPlayer
local mouse = plr:GetMouse()
local char = plr.Character or plr.CharacterAdded:Wait()
local humanoid = char:WaitForChild("Humanoid")
local camroot = char:WaitForChild("HumanoidRootPart")
local moreInput = require(script:WaitForChild("moreInput"))

-- VARIABLES
local roll = 0
local lastCFrame = CFrame.new()
local lX = 0
local lY = 0
local newTouch
local deltaToSubtract = Vector3.new()

local check = function(cX, cY)
	local newX = cX
	local checked = false
	if newX > math.rad(22.5) then
		newX = math.rad(22.5)

		checked = true
	elseif newX < math.rad(-45) then
		newX = math.rad(-45)
		checked = true
	end
	if faceInCameraDirection then 
		print("Testing")
		return CFrame.fromEulerAnglesXYZ(newX, 0, 0), checked --error may be here? Forcing the other values to 0, and only account for the newX variable. Maybe?
	else
		return CFrame.fromEulerAnglesXYZ(0, cY or 0, 0) * CFrame.fromEulerAnglesXYZ(newX, 0, 0), checked
	end
end

-- thanks oxazolone for fixing this \/
local function rayCheck(startPoint, endPoint, rotation)
	local ray = Ray.new(startPoint.Position, (endPoint.Position-startPoint.Position).unit*5)
	local dontCare, hit = workspace:FindPartOnRay(ray, char)
	if hit == nil or (startPoint.Position - hit).Magnitude > (startPoint.Position - endPoint.Position).Magnitude then
		return endPoint
	else
		return CFrame.new(hit, startPoint.Position)
	end
end

--convert vector3's of input to vector2's
local function convert(v3)
	return Vector2.new(v3.X, v3.Y)
end

local function getDelta()
	if Input.MouseEnabled then
		return Input:GetMouseDelta() * -1 / 100
	elseif Input.TouchEnabled then
		local touch, start = moreInput:GetLastTouch()
		if newTouch ~= touch then
			deltaToSubtract = start
			newTouch = touch
			print("diff touch")
		else
			print("same touch")
		end
		if touch and touch.Position - deltaToSubtract ~= Vector3.new(0, 0, 0) then
			local lastDTS = deltaToSubtract
			deltaToSubtract = touch.Position
			return convert(touch.Position - lastDTS) * -1 / 100
		else
			return Vector2.new(0, 0)
		end
	elseif Input.GamepadEnabled then
		local stick = moreInput:GetLastStickInput()
		if stick and stick.Delta ~= Vector3.new(0, 0, 0) then
			return convert(stick.Delta) / 100
		else
			return Vector2.new(0, 0)
		end
	else
		return Vector2.new(0, 0)
	end
end

local function RenderCamera()
	cam.CameraType = Enum.CameraType.Scriptable
	Input.MouseBehavior = Enum.MouseBehavior.LockCenter
	
	local inputDelta = getDelta()
	print("delta: " .. tostring(inputDelta))
	
	local cX = senseY * inputDelta.Y + lX
	local cY = senseX * inputDelta.X + lY
	
	local newRootCF = CFrame.new(camroot.Position) * CFrame.fromEulerAnglesXYZ(0, cY, 0)
	
	if faceInCameraDirection then
		char.Humanoid.AutoRotate = false
		camroot.CFrame = newRootCF
	else
		char.Humanoid.AutoRotate = true
	end
	
	local headPos = faceInCameraDirection and camroot.CFrame or camroot.CFrame * primaryOffset
	
	headPos = faceInCameraDirection and headPos or CFrame.new(headPos.Position)
	
	if math.abs(inputDelta.X) > 0 or math.abs(inputDelta.Y) > 0 then
		local deltaCF, checked
		if faceInCameraDirection then
			deltaCF, checked = check(cX)
		else
			deltaCF, checked = check(cX, cY)
		end
		
		local startPoint = faceInCameraDirection and headPos * primaryOffset * deltaCF or headPos * deltaCF
		local endPoint = startPoint * mainOffset
		
		local endCFrame = rayCheck(startPoint, endPoint, deltaCF)
		
		cam.CFrame = endCFrame
		lastCFrame = deltaCF
		if not checked then
			lX = cX
			lY = cY
		end
	else
		local startPoint = faceInCameraDirection and headPos * primaryOffset * lastCFrame or headPos * lastCFrame
		local endPoint = startPoint * mainOffset
		
		local endCFrame = rayCheck(startPoint, endPoint, lastCFrame)
		
		cam.CFrame = endCFrame
	end
end

game:GetService("RunService"):BindToRenderStep("myCamera", Enum.RenderPriority.Camera.Value + 1, RenderCamera)

And the module handling input:
-----------------

-- BY NEOND00M --

-----------------

local module = {}

local input = game:GetService("UserInputService")

local lastStickInput

local lT

local lastTouch

local lTStart

local lastTouchStart

-- main functioning

input.TouchStarted:Connect(function(touch, gameEvent)

print(touch.Position.X .. "|" .. workspace.CurrentCamera.ViewportSize.X/2)

if not gameEvent and touch.Position.X >= workspace.CurrentCamera.ViewportSize.X/2 and touch.Position.Y <= (workspace.CurrentCamera.ViewportSize.Y-workspace.CurrentCamera.ViewportSize.Y/3) then

lT = touch

lTStart = touch.Position

end

end)

input.TouchMoved:Connect(function(touch, gameEvent)

if lT == touch and touch.Delta ~= Vector3.new() and not gameEvent then

lastTouch = touch

lastTouchStart = lTStart

end

end)

input.TouchEnded:Connect(function(touch, gameEvent)

if lastTouch == touch then

lastTouch = nil

end

end)

input.InputBegan:Connect(function(inputObject, gameEvent)

if inputObject.KeyCode == Enum.KeyCode.Thumbstick2 and not gameEvent then

if inputObject.Delta ~= Vector3.new() then

lastStickInput = inputObject

end

end

end)

input.InputEnded:Connect(function(inputObject, gameEvent)

if inputObject == lastStickInput then

lastStickInput = nil

end

end)

-- public functions

function module:GetLastTouch()

return lastTouch, lastTouchStart

end

function module:GetLastStickMovement()

return lastStickInput

end

return module

I made a comment on the code (the main camera code) of where I might believe the bug is. I might be wrong, I might be right. But I’d love to get your opinions on this and I would really appreciate it if you guys took the time to look at it the code - it really helps me a whole bunch!

But what I might think:
The part where it checks if the player has reached the maximum height or minimum height, it forces the player back to the max height limit - so if you try to go higher/or lower than the limit it will bring you back to the max/min. When doing that, it only accounts for the Y value, and not the X and Z value, hence, if you go a little upwards whilst moving to the side with the camera, it will force you 1. Back to the max height/min height and also back to the 0,0 position of the X and Z value, so there has to be some way to only account for the height, and then ignore or account with the side movement. I hope you get what I meant, hard to get this on paper.

As I said, I recommend test the code for yourself in studio, feel free to download:

ThirdPersonCameraBug.rbxl (27.2 KB)

If you need more info, please reply and I’ll answer! Thank you so much.

1 Like

I think you are overcomplicating the process needed for getting delta changed for your camera.

For getting the delta, all we really need is the .InputChanged event from UserInputService, which we can use to scale it down for sensitivity purposes. Once we scale it down, we can add them to our angles variables.

45 degrees means that we are looking straight down on the camera, and -45 degrees is the same but looking upwards. That’s all the limitations we need for solving the y angle in our camera.

Here, we get:

    if processed or not processed and input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
    angleX -= input.Delta.X / sensitivity
    angleY = math.clamp(angleY + (input.Delta.Y / sensitivity), -maxAngle, maxAngle)
end)

Since we don’t need to convert the numbers we solved for into degrees, we can just put math.rad(45) as our limitation.

    uis.MouseBehavior = Enum.MouseBehavior.LockCenter
    local origin = root.Position
    local cf = CFrame.new(origin.X, origin.Y, origin.Z) * CFrame.Angles(0, -angleX, 0)
    cf *= CFrame.Angles(angleY, 0, 0)
    cf *= offset
    workspace.CurrentCamera.CFrame = cf
end)

Here, all we’re really doing is cframe transformations. We take the origin (our camera’s focus or whatever object we are following), and then turn it into a cframe that’s rotated by angleX. We now have a cframe that can move left to right based on our mouse, but now we need to add Y rotation.

All we do is we multiply it again by CFrame.Angles, but this time with angleY. Here, we have the proper orientation that we need that follows the player. However, we’re inside now, so we need to offset it so we can see the player.

To do that, just multiply the cframe we have with an offset, CFrame.new(offsetX, offsetY, offsetZ). Just apply that to the camera.

I’m sure you can solve for camera collisions from here. Hope this helps.

2 Likes

Hi, thanks for your long and extensive answer. I’m sure I can do something with the code snippets you gave me. Anyways, as I waited for replies I changed a couple of variables in my script, this part:

local check = function(cX, cY)
	local newX = cX
	local checked = false
	if newX > math.rad(22.5) then
		newX = math.rad(22.5)

	checked = false --(changed from true to false)
elseif newX < math.rad(-45) then
	newX = math.rad(-45)
	checked = false --(changed from true to false)
end
if faceInCameraDirection then 
	print("Testing")
	return CFrame.fromEulerAnglesXYZ(newX, 0, 0), checked --error may be here? Forcing the other values to 0, and only account for the newX variable. Maybe?
else
	return CFrame.fromEulerAnglesXYZ(0, cY or 0, 0) * CFrame.fromEulerAnglesXYZ(newX, 0, 0), checked
	end
end

Now I do not have any camera jitter when reaching the min or max values, but it kind of snaps to the bottom or top. I can describe it more in detail (and with videos etc) if you are interested. Just reply and I’ll give you more information right away!

I’m confused as to why you need all that. Are you limiting your angles via those if statements from the check function? You can just solve for the angles and use math.clamp to make sure it doesn’t exceed the maximum rotation possible. At most, you should only have 3-10 lines for delta changed, you might need more if you’re including joystick action but all of that can be solved with just pure math.

It looks like you change a value for rotation based on how much the mouse moves, and clamp the shown rotation for up and down, but not the actual value for up and down. Do you get what I mean?

I think what @hboogy101 said is true. It seems like every time a player wants for example to move the camera away from upper limit, he/she has to also include all the mouse delta after reaching the limit. So if I were to reach the upper limit and try the move my mouse forward for 10cm/~=4inch, I would have to move it the same length but backwards before reaching the point where I can rotate the camera away from the border. I hope you understand this confusion. :slight_smile:

EDIT
I also agree with @VerySublime. Xbox input will require some modifications, but you can really simply use mouse movement and transform CFrames.
When you finish the code, you might want to remove all prints - print() - and use π instead of math.rad for better performance.

math.rad(22.5) → π / 8 → math.pi / 8
math.rad(-45) → - π / 4 → - (math.pi / 4)

1 Like

@hboogy101 @DevTestDummy @VerySublime

Hey, you guys just gave me the answer (kinda).

So, as you might’ve guessed, I am not the best at math, so the math parts here were basically not programmed by me, but a “colleague” of mine. Anyways, as I said in the previous reply I swtiched the “checked” variable from true to false. AND, it actually worked. But the bug I am encountering now, (I do not have any jitter etc, see vid) is that what @DevTestDummy said, that if I reach the max limit, and I continue to move my mouse forwards I have to move the mouse backwards the same distance (just read @DevTestDummy post, its exactly how he described). Now, how would I get the value for rotation “the right way”, not based on the mouse and how would I clamp that (like @hboogy101).

I know this seems kind of messy, but I would really appreciate you guys’ help, it really means a lot - and I really can’t do anything with the game before this is fixed. I will experiment a little, but I am really not sure how to do this unless someone could tell me. Thanks.

1 Like

I’ve given you the answer already. You’re doing too much in order to provide compatibility to various devices when you can just solve for the angles given by the .InputChanged event, making it difficult to debug.

I was able to achieve the same result as you, however in only 27 lines:

.InputChanged modifies the values needed for the camera, and the camera updater (from RenderStepped) just updates the camera based on those values. The RenderStepped does nothing special as it just solves for where your camera is supposed to be and how it’s rotated given those data. No logic, just cframes there. How you get that data is where your logic comes from.

You can see the same thing here where you can obtain delta via a single event: UserInputService.InputChanged

Transform what you get as delta from InputObject to the angles.