3rd person camera similar to polyguns

To get right into it, I’m trying to make a 3rd person camera similar to “POLYGUNS”. I have a very rough start, but I’m not really sure where to go from here.

Here’s what I’m trying to do:

https://streamable.com/rqr4z

Here’s what I have:

https://streamable.com/0s3od

Pretty good, right? Well, there’s a pretty big problem. Here’s what happens when you press “S”:

https://streamable.com/4x7gu

Here’s my code:

There’s a little bit of code before and after, but shouldn’t really be relevant. sens is set to 80.

Another problem is that in the code you can see that I disabled left and right movement. This is because it does a similar spinning motion to pressing S.

If anyone could explain what I did wrong, and how to fix it, I’d really appreciate that. It’d also be helpful to add an explanation of what your code does because I’m really interested in learning about camera manipulation and would like to make some parts of the camera even more complex than the POLYGUNS camera.

Thanks!

2 Likes

Added this about why I disabled left and right movement.

I was actually in the same boat as you a couple months back, spending hours looking for code that was a hundred lines long just for one thing to be wrong. You can actually get the roblox shift lock camera behavior very easily in about 10 lines of Lua. Check out this article by @1waffle1 Custom center-locked mouse camera control toggle

I’m not really looking for shift lock, as you can see from the polyguns clip. I’d also like a lot more customizability to it, being able to zoom in and out, which basically makes this useless. Thanks for the suggestion though!

Its right in those last lines of code that you’re finding your trouble.
You set your Camera based off the HRP’s CFrame, however, you then also set the HRP’s CFrame.
What you should instead do is a simple change;
Camera.CFrame = CFrame.new(hrp.CFrame.p) * CF(V3(2,3,3) + humanoid.CameraOffset)…
This removes the HRP rotation from the equation, and leaves you with the exact position of the character. From there, everything should be fine.

1 Like

Pretty solid progress, but now the camera doesn’t rotate with the hrp rotation.

https://gyazo.com/1cf260d356493019b7082a39e07fcb64

Judging by your clip of polygons, the post I gave is exactly what you’re looking for. You don’t have to have the player press shift, you just toggle the functionality of shift lock off and on whenever you need to enable this camera.

Polyguns doesn’t use shift lock switch, and you can tell that because the player can move to look back at the camera with their camera still facing the same direction.

I think it’s also important to note specifically the behavior of the POLYGUNS camera where you can turn towards the camera and move backwards quite smoothly.

you need to store a constant X and Y angle for your character. Stop setting X = 0 every frame.

What exactly do you mean by this? I tried removing the x = 0 at the end of the script, and not much has changed.

Okay, so after a few back and forths over discord;

  • We looked at the existing code to identify problem areas
  • Some changes were proposed, these brought up some slightly more fundamental points of interest.
  • We created a quick checklist to make problems easier to identify
  • We went back over the fundamentals to understand these problems.
  • We created code that reached a milestone goal
  • We created a minor addition that fulfilled our mvp goal.
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.

Our final code
local UserInputService = game:GetService("UserInputService")

local RunService = game:GetService("RunService")

local Client = game.Players.LocalPlayer

local Camera = workspace.CurrentCamera

local Char = Client.Character or Client.CharacterAdded:Wait()

local hrp = Char:WaitForChild("HumanoidRootPart")

local sens = 80

local x = 0

local y = 0

local FollowMouse = 0

UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter

UserInputService.InputChanged:Connect(function(input)

if input.UserInputType == Enum.UserInputType.MouseMovement then

FollowMouse = 0

x = x + input.Delta.X/sens

y = math.clamp(y-(input.Delta.Y/sens), -1, 1)

end

end)

RunService:BindToRenderStep("CameraUpdate", Enum.RenderPriority.Camera.Value, function(dt)

UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter

local getCameraRot = CFrame.new(hrp.Position) * CFrame.Angles(0, -x, 0) * CFrame.Angles(y, 0, 0)

Camera.CFrame = getCameraRot * CFrame.new(2, 3, 5)

if FollowMouse <= .3 then

hrp.CFrame = CFrame.new(hrp.CFrame.p) * CFrame.fromEulerAnglesXYZ(0, -x, 0)

end

FollowMouse = math.clamp(FollowMouse + dt, 0, 1)

end)

https://i.gyazo.com/0fe1abf30d7107b2c7ff7d26a2b2510f.mp4

@EncodedLua actually had a really useful resource available here, it was simply a few tweaks away from being what was needed. Sometimes its useful to slow down, look at how things work, understand why it was presented and see how you can learn more about it.

Hope anyone who has come across this particular problem set and not known why things weren’t quite working as intended have gained something :slight_smile:

15 Likes