How to make a Five Nights at Freddy's type camera movement


ATTENTION: I haven’t done a tutorial in a very long time, so I might not explain things well, or haven’t provided enough information. I will try to explain this as best as I can!

Hey y’all, BackSpaceCraft here, and today, I have a tutorial to show you that might interest people who are into the horror game Five Nights at Freddy’s. This tutorial will tell you how you can make a horizontal scrolling camera that can be used for FNaF fan-games, as well as other projects of yours!

The camera scrolling will include:

  • Customizable sensitivity
  • Limits for left and right side

Let’s get into it!

1. The essentials

For starters, what do we need to make this?
All you need in your workspace is a part to emulate the player’s camera, which we will be calling CamPart throughout this tutorial. Make sure your part is anchored.

With that being said, you can create a LocalScript in StarterPlayerScripts, and get to work.

2. The LocalScript

First of all, set things up.

Let’s create a variable for the player, the camera, the player’s mouse, and the RunService service. Let’s also not forget the CamPart.

local rs = game:GetService("RunService")

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local camera = workspace.CurrentCamera

local CameraPart = workspace:WaitForChild("CameraHolder")
local pivot = CameraPart.CFrame

You might have noticed I’ve put a pivot variable there. That’s what we’ll be using to determine the camera’s Rotation.

Next, you can put your custom variables down, such as the sensitivity, the limits (we’ll be only using one number to make this easier, but you’re more than welcome to put another variable), and last but not least, the currentPos. This will be EXTREMELY important for this tutorial, as this number will essentially tell us how far we are from the pivot’s original rotation.

local sensitivity = 0.1
local limit = 45
local currentPos = 0
local wholePos = 0 -- This is to round up the currentPos' value. Not necessary, but might help you keep track of things later on

Now, Let’s make the setCam() function.
So, we’ve made RunService a variable for a reason. You should either connect every RenderStep to the setCam() function, or bind it using the rs:BindToRenderStep() function. After all, it’s not really useful if it’s not happening every frame, right?


The function

I’d start off this function by making the camera’s ViewportSize a variable named vs.

local vs = camera.ViewportSize

camera.CameraType = Enum.CameraType.Scriptable
camera.CameraSubject = CameraPart

This is only because camera.ViewportSize is waaay too long. Don’t forget the camera’s type and subject either.

Anyhow, in order to rotate our camera, we need to use our mouse, right?
We could use the mouse’s position to subtract it from the half of the vs, both on the X and Y coordinates, to create the amount the camera’s rotation will increase/decrease.
Essentially this:

local centerPos, mousePos = Vector2.new(vs.X/2, vs.Y/2), Vector2.new(mouse.X, mouse.Y)
local xDir, yDir = centerPos.X - mousePos.X, centerPos.Y - mousePos.Y
local yRot = xDir -- we switched them because the Y axis isn't the same in 2D.

Great! Math is an essential part of programming, so get used to using it everywhere.
Anyway, the yRot variable is the raw format of the amount our camera will change. Let’s apply some of our settings onto it!


The sensitivity

The sensitivity should be pretty straightforward: You just need to multiply the yRot with the sensitivity we’ve set, Right? Well, not exactly.
You see, this would indeed work, but you have to realize that this sensitivity, while good on PC, might look horrendous on mobile. We have to apply scaling onto it. How do we do that? We just have to borrow the vs’ Y coordinate and multiply THAT with the sensitivity instead. and all of that can finally be divided with the yRot.

yRot = yRot/(vs.Y * sensitivity)

However, we forgot about our CurrentPos variable. We now need to add the yRot (that we hopefully applied sensitivity to) to the CurrentPos. All while clamping it between the limits we’ve set.

currentPos = math.clamp((currentPos + yRot), -limit, limit)
wholePos = math.round(currentPos) -- Again, not necessary.

We’re almost finished!

Finally, we should apply this currentPos to either the CamPart’s or the camera’s CFrame. I highly encourage setting the CamPart’s CFrame, as that’s the method I’ve worked with.

CameraPart.CFrame = pivot * CFrame.Angles(0, math.rad(currentPos), 0)
camera.CFrame = CameraPart.CFrame

Test time!

Hopefully everything works as intended. If not, feel free to comment under this post, so I can fix up some things with this poor excuse for a tutorial. :slight_smile:

9 Likes

is there a way to allow the player to look down and up, also with a limit?

Yes, there is a way to do it. There is actually a pretty detailed YouTube video about it (I actually upgraded that one to make a more “FNaF-ish” type camera movement)

actually, I don’t think I could link the video here because it contains mild swearing, but I think it explains the math pretty well so I’m keeping it. But I’ll just put the entire code here, just in case.

-- LOCATION: StarterPlayerScripts

local rs = game:GetService("RunService")

local camera = workspace.CurrentCamera
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local cameraPart = workspace:WaitForChild("CameraHolder")
local pivot = cameraPart.CFrame

local SENSITIVITY = 0.1
local LIMIT_Y = 20
local LIMIT_X = 40

local function bind()
	local vs = camera.ViewportSize
	camera.CameraType = Enum.CameraType.Scriptable
	camera.CameraSubject = cameraPart
	
	local centerPos, mousePos = Vector2.new(vs.X/2, vs.Y/2), Vector2.new(mouse.X, mouse.Y)
	local xDist, yDist = mousePos.X - centerPos.X, mousePos.Y - centerPos.Y
	local xRot, yRot = math.rad(yDist), math.rad(xDist)
	
	xRot, yRot = xRot/(vs.X * SENSITIVITY), yRot/(vs.Y * SENSITIVITY)
	xRot, yRot = math.clamp(xRot, -LIMIT_X, LIMIT_X), math.clamp(yRot, -LIMIT_Y, LIMIT_Y)
	xRot, yRot = -xRot, -yRot
	
	camera.CFrame = pivot * CFrame.Angles(xRot, yRot, 0)
end

rs:BindToRenderStep("Enable Camera Please :)", Enum.RenderPriority.Camera.Value, bind)

(https://youtu.be/qsL3OXnCGoE)

(Hint: It’s around the 8 minute mark if you’re only interested in the camera)

3 Likes