Heres the process for those interested in this topic
Lets begin with the code presented itself, disect it a bit and see where potential problems could be.
Disecting the Code
First things first, What is Client? We’re being presented with variables that we don’t have a reference to. For bug-fixing potential code, knowing what each variable is meant to represent is crucial to us. Thankfully, with a quick search we know that Client here is probably a Player object, because CameraMinZoomDistance is a valid property.
Moving on, we unbind move left and move right. Towards the end of the code, we realise that the player is being rotated by the mouse. While this concept isn’t itself a problem, the way it is implemented is. This is problem 1.
We then set our player’s MouseBehaviour to LockCenter. This is done so that the input type MouseMovement is triggered in a way we can reliably use, otherwise we might not be able to find the delta, or we could have rotation while just regularly moving our mouse. Yikes! Again, not a massive issue itself, but with some other code interfering this can become unreliable on loading, so we’ll mark it as problem 2.
Input is given and we look for our input type, we find MouseMovement and its delta. Thanks to our mouse being locked, we know we’re being given a reliable delta value. Lets use them!
x, another phantom variable that we’ll say is 0 by default, is now defined as the X value of the delta, divided by sens (a number variable we will say is 80.). For making incremental turns this is fine, but it is a symptom of a larger problem, so lets label it as Problem 3.
Now a large if and elseif statement, followed by yet another if/elseif. Nothing wrong, but could be cleaner. We’ll label it Problem 4.
Now for the meat and potatoes of a good camera script. Runservice. This is a service that will fire events every time a frame is drawn, its an incredibly powerful service and should have some time taken to familiarise with it. As you continue coding, you’ll find more and more ways to use it.
We’ll assume currentlyRunning is a bool variable set to true.
Here we take our camera and create its new CFrame for the current frame. To do this, we’ve taken our hrp (humanoid root part) CFrame and given it more coordinates, v3(2,3,3) + HumanoidCameraOffset, then applied an angle, Y first, then applied another translation. We then change our HRP’s CFrame to give it the same angle we just applied to the camera, and that brings us into Problem 5. Now we have our Camera’s CFrame and our HRP’s CFrame tied together, we cannot change one without changing the other!
So lets begin to look through them.
Problem 1 - unbinding actions
To achieve the desired camera, the player needs to have all of the possible angles of movement available to them. If we take away their ability to move left and right, we no longer fulfil this goal. Because we aren’t doing anything to re-introduce this behaviour we can fix this problemsimply by removing this portion of code.
Problem 2 - MouseBehaviour and LockCenter
This is an incredibly small problem, but its really easy to address and is just a matter of moving the line of code. In order to make sure the player is consistantly and reliably has their mouse locked to the center of the screen, we just move this code to inside our RunService function, after the CurrentlyRunning if statement. This gives us the ability to add a way to unlock the mouse later on.
Problem 4 - Tidying up the IF statements.
We don’t actually need to have any if statements here. We can instead clamp the values between -1 and 1. In order to do this we will use the math.clamp function. We will need 3 things:
- The value we are putting into it
- The smallest value we want out of it
- The largest value we want out of it
We can now change all of these if statements to;
y = math.clamp(y - (input.Delta.Y / sens), -1, 1)
Now, even if y delta is 0, we are able to easily solve this.
Problem 5 - Finding our CFrames and untying them.
CFrame is a really powerful property. In simple terms, its a position and rotation, stored in the same property. Theres a whole wealth of information available about CFrame, but it also can become quite difficult to work with if you’re unfamiliar. Lets have a quick look at what we’re doing, then recap why its a problem.
Setting the Camera’s CFrame: Take our HRP’s CFrame as our starting CFrame, translate this by a vector + vector, apply a rotation to this, then apply another vector.
Setting the HRP’s CFrame: Take our HRP’s CFrame as our starting CFrame, rotate this by the negative of our MouseMovement X delta.
We are using the Humanoid Root Part’s CFrame in both of these, but we’re also setting the HRP’s CFrame at the end! We’ve accidentally just created two ways of controlling the camera! O Noes! Why does this happen? Well, when the Camera’s CFrame is calculated, we’re taking the HRP’s CFrame to make it. The CFrame already has a rotation applied to it, meaning that if we try and move backwards, the character will turn backwards slightly, we’ll use this new angle to move our camera, we’ll then change the HRP’s CFrame so that its facing the direction of the camera so its still forward, and so on, and so on spinning us in circles.
The way to solve this is really simple.
Instead of taking the HRP’s CFrame with all its rotations, we just take the Vector (position) of the CFrame, by doing hrp.CFrame.p . Now we don’t have any rotations and we can use this how we’d like! Lets make it a CFrame again so that we can work with our other math operators.
Camera.CFrame = CFrame.new(hrp.CFrame.p)
Next we apply a transformation. This will move our camera’s focus point away from the center of our player, and slightly more to its right shoulder. If we want to move our camera to be able to rotate fully around the character, we need to apply this after our rotations. Because of this, we can now change to;
Camera.CFrame = CFrame.new(hrp.CFrame.p) * CFrame.fromEulerAnglesXYZ(y, 0, 0) * CFrame.fromEulerAnglesXYZ(0, -x, 0)
We’re now just rotating around the center of the player. If we were to try this now, we’d have the camera stuck inside the player itself. We can apply some transformation in order to move us where we’d like to be! In order to not update the camera too many times, we’ll just make a local variable to hold our math.
local GetCameraRotation = CFrame.new(hrp.CFrame.p) * CFrame.fromEulerAnglesXYZ(y, 0, 0) * CFrame.fromEulerAnglesXYZ(0, -x, 0)
Now we can set our Camera’s transformations really easily!
Camera.CFrame = GetCameraRotation * CFrame.new(3, 3, 5)
Something will still be off about this though. We’re rotating around the player’s center, not their head. To fix this, jump back to the GetCameraRotation line and change it to;
local GetCameraRotation = CFrame.new(hrp.CFrame.p) * CFrame.new(0, NEW_HEIGHT, 0) * CFrame.fromEulerAnglesXYZ(y, 0, 0) * CFrame.fromEulerAnglesXYZ(0, -x, 0)
NEW_HEIGHT can be changed out for any number, but it moves what we’re spinning around.
We can now change the HRP CFrame change using the same principles.
We take our HRP position, turn it into a CFrame, and then apply only our rotation around the Y axis (or our vertical axis).
hrp.CFrame = CFrame.new(hrp.CFrame.p) * CFrame.fromEulerAnglesXYZ(0, -x, 0)
This is great… but somethings still wrong. We can’t look side to side now!
Problem 3 - X is always new
We’re rotating around our vertical axis using the X delta, but at the end of each frame, we’re turning that to 0! In the old way of doing this, we were only applying changes based on the rotation that frame. That worked because our HRP stayed in that rotation; In our new version doing the same thing means we will always have the camera try and face foward. Instead, we need to do what we’re doing with the y delta, and do some addition.
Now instead of;
x = input.Delta.X/sens
we have;
x = x + input.Delta.X/sense
Now we can turn and turn and turn, and if we stop, we’ll end up facing the direction we’ve moved to face.
After the solving the problems we have listed, we have reached a reasonable version of what we want to have. With some tweaks, we can tidy up and even add some behaviour that is closer to the goal. This includes defining our variables, removing the CurrentlyRunning arg, added a quick FollowMouse argument that lets the player run around freely unless they’ve recently moved the mouse. For speed’s sake, I’ve just included these changes in the final code.
Hope anyone who has come across this particular problem set and not known why things weren’t quite working as intended have gained something