Creating Common Camera Effects

Introduction

I’m sure you’ve seen those games that have those cool camera effects. Like DOORS, of course, and plenty of other games that implement it.

This post will be a list of those aforementioned effects, and after this post, I’m confident you understand how to make them.

— prerequisites —

  • Basic Knowledge of CFrames, and generic CFrame/Vector3 Functions!
  • tick() function.

— links —

https://create.roblox.com/docs/reference/engine/classes/Camera
https://create.roblox.com/docs/reference/engine/classes/RunService
https://www.mathsisfun.com/sine-cosine-tangent.html

Table of Contents

This post will be split up into many effects, and later I will even add on to it.

  • Camera Bouncing / Breathing Effect
  • Idle Wobbling Effect
  • Mixing them all together!

Camera Bouncing Effect

Assuming you don’t already know what tick is, it’s a GLOBAL function that accurately measures the amount of seconds passed since the Unix Epoch.

https://en.wikipedia.org/wiki/Unix_time

Which means you can measure how much time has passed since a specific event, get a deltaTime, and with that, you can do extremely advanced and amazing things!

local part = game.Workspace.Part
local now = tick() -- returns number

part.Touched:Connect(function()
    local new_now = tick() -- these are two different numbers, tick() is always changing!

    print("it's been " .. tostring(new_now - now) .. " seconds since this script has started!")
end)

Another thing, I’m sure you’ve heard of the humanoid, if not, I better put that in the prerequisites.

There is a very interesting and useful property in the humanoid. It’s called CameraOffset, a Vector3. This property offsets the camera by any Vector3, and it’s all local!

Now that you understand these things, let’s get into it.


For this method, and for all camera effects, it’s usually just matter of mixing stuff up. That way, you can create interesting, unique, and random effects.

Since we are dealing with the camera, we will need to put this into a LocalScript.

First, let’s get the camera, and the player. We will also definitely need RunService, so let’s fetch that up, and create an Update() function so we can update the camera.

local RS = game:GetService("RunService")

local player = game:GetService("Players").LocalPlayer

-- this is how I do it, you can use game.Workspace.Camera if you'd like!
local camera = game.Workspace.CurrentCamera

local function update()
    -- the updating will go here!
end

Of course, we definitely don’t want to do camera effects, if the player can’t see them! Let’s wait for the character to load.

-- this line waits and yields for the ".CharacterAdded" event to fire, and then it gets the character. We will need it.
local character = player.CharacterAdded:Wait()

-- don't forget about the humanoid!
local humanoid = character:WaitForChild("Humanoid")

Now that we know the player is loaded, and we have the camera, we can start doing the effects. But, if we’re going to do some effects we definitely want it to be smooth. That’s why we’re using RunService!. Let’s bind a function to the .RenderStepped event. We will update the camera every time this event fires!

RS.RenderStepped:Connect(function(deltaTime) -- deltaTime! wow!
    update()
end)

-- or you could do:
RS.RenderStepped:Connect(update)

Now, you probably noticed that deltaTime parameter I included, because of the nature of THIS method, we won’t be using that, but I will most likely use it in future methods, or effects.


NOTE

It is NOT good practice to include parameters that you will never use. It actually wastes memory, although being small, if you’re not using it, don’t include it.


You’re probably thinking: “Den_vers, I get that but HOW are we going to get the camera to bounce!

Well, billy, we’re going to need some ma-

NO-

Math.

You heard me. If you aren’t familiar with trigonometric functions, you should really learn them. They are very useful for positional math, and very simple and fun! I’ve included some links about them if you didn’t already know.

Observe the sine function:

image

It’s a wave! A bump! And, it goes on forever. This is exactly what we need. We will plug in the current tick() to this, and if we keep updating fast enough, the camera will move up and down!


NOTE

sine also has a common counterpart, cosine!

and if you don’t already know, to get the sine function in Lua, it’s just math.sin().


Updating!

Let’s start creating the update function. We will need to get the tick() or the “now”, to figure out what position the cycle will need to be at. And we get the height, and update the humanoid’s CameraOffset property.

local function update()
    local now = tick()
    local height = math.sin(now)
    
    humanoid.CameraOffset = Vector3.new(0, height, 0)
end

And then… well, that’s it! you’ve done it! That’s literally all we need for the simplest example.

Of course, though, if you tested it out, it would look goofy. Your camera would be bouncing all around too high, and then too low, and probably too slow. So we have to modify the function. We want it to breath faster (or slower if you prefer), and higher (or lower if you prefer).

The script so far:

local RS = game:GetService("RunService")

local player = game:GetService("Players").LocalPlayer

-- this is how I do it, you can use game.Workspace.Camera if you'd like!
local camera = game.Workspace.CurrentCamera

-- this line waits and yields for the ".CharacterAdded" event to fire, and then it gets the character. We will need it.
local character = player.CharacterAdded:Wait()

-- don't forget about the humanoid!
local humanoid = character:WaitForChild("Humanoid")

local function update()
     local now = tick()
     local height = math.sin(now)
    
     humanoid.CameraOffset = Vector3.new(0, height, 0)
end

RS.RenderStepped:Connect(update)

Modifications

To accomplish the higher or lower effect, it’s simple. Multiply the value by the desired height. Since the automatic or “default” height is 1, we can just multiply it by the height we want.

So let’s add a line to our script, and say we want the breathHeight to be .3:

local camera = game.Workspace.CurrentCamera

local breathHeight = .3

local character = player.CharacterAdded:Wait()

And then, we will multiply the height by that value.

local height = math.sin(now) * breathHeight

Great! It’s working. But, two things:

  • You’ve probably noticed that you’re still breathing while NOT being in first person, which looks kind of wonky.
  • And also, the breathing is still slow, I mean, what if this is a sprinting game?

Let’s work on the second one. If you’re good at math, you probably already know how to solve this, but if you’re not, I’ll explain it for you, don’t worry. :slight_smile:

This time, we can’t just multiply the height by some number. That will just make it go higher, or lower. We want to make it go faster. How do we do that? By speeding up time! What do I mean by that? We multiply tick(), instead!

This essentially, as I just mentioned, “speeds up time”, so we move through the math.sin() function faster. Let’s do it.

Of course, let’s create a variable so that it can be easily modified later. We’ll add it directly under breathHeight:

local breathHeight = .3
local breathSpeed = 3 -- this will, uh, make it breath 3 times per second ;)

Now, we have to multiply now by the value, so this is how our formula would look:

local height = math.sin(now * breathSpeed) * breathHeight

If everything was done right, it should work! Amazing! Now, we also have to make it so that it doesn’t breath when you’re not in first person.

If you’re especially observant, you’ll notice it doesn’t ACTUALLY cycle at three times per second

QUICK FIX: multiply by math.pi. Why? math.

To do this, we’ll use a little trick. Checking if the player is in first person by the head transparency.

if character.Head.LocalTransparencyModifier == 1 then
    -- player is in first person, we can breath! ahhh~
end

Simple as that! Let’s try it:

local function update()
    if character.Head.LocalTransparencyModifier == 1 then
        local now = tick()
        local height = math.sin(now * breathSpeed) * breathHeight

        humanoid.CameraOffset = Vector3.new(0, height, 0)
    end
end

Yay! It works! You probably do notice, though, that when you go in first person, it shifts to wherever, the tick() happened to be in the cycle. If you have a game where you’re forced in first person, this is no problemo, you don’t have to worry about this. But, for all the people that do have this problem, I will be adding a way to prevent in the future. There are actually ALOT of ways. Here’s a list:

  • Lerping to the desired position, this helps with smoothing it out, and also can change the sine function’s dynamic.
  • Creating a variable that restarts to zero whenever you enter first person, and calculating the now by the amount of time passed that since that time with tick()
  • Creating a thread that constantly updates the height through a variable, and then every frame if the player is in first person then start updating the actual camera height. In simple terms, waiting for the height to reach zero again.
  • Just forcing the player in first person!

These are all just ideas, and you can use any of them. Whichever fits your needs.


IMPORTANT NOTE

This script was designed to be placed in StarterPlayerScripts, but I highly recommend to place this script in StarterCharacterScripts, because if the player resets, you will be in trouble with A LOT of errors. To fix this, just use StarterCharacterScripts, and replace the line where character is defined with script.Parent.


Idle Wobbling Effect

Now that you’ve gotten down the basics of the method we will be using to move the camera, this next one will be simple.

For this one, I’ll have to introduce you to a new concept about math and trigonometry. Wait! Don’t click off yet, it’ll be quick and simple.


math.sin + math.cos = circle!

If you don’t already know trigonometry, which I’m presuming you don’t, the essence of sine and cosine together can create circles. I won’t get into the depths. Take into consideration this code sample:

local part = Instance.new("Part")

part.Anchored = true

-- a very smart person recommended I use :GetService()!
part.Parent = game:GetService("Workspace")

while task.wait() do -- questionable, but for this example we'll leave it.
    part.Position = Vector3.new(math.sin(tick()), 0, math.cos(tick()))
end

Try it out, this code will make the part move in a circle, around the origin of the workspace. Pretty cool, huh?


Time Travel?

Remember when we multiplied tick() by 3 to “speed up time”? We can also “offset” or increment time by adding to tick()! Even slow down time by dividing. Any mathematical operation you do on tick(), can be used with effects.

Let’s try now and implement this in the camera.

local function update()
    if character.Head.LocalTransparencyModifier == 1 then
        local now = tick()

        humanoid.CameraOffset = Vector3.new(math.sin(now), 0, math.cos(now))
    end
end

Try it out! It will now move the camera in a circle. Cool, right? Now, remember we can ALWAYS manipulate it, by multiplying the offset, or dividing by any number.

But who says we only need one circle, let’s two, or even three! We can offset the positions of the circles to get different “random” offsets each time! Almost think of it as the path the moon takes about the sun (if the moon was the camera).

image

This is called a cycloid, and I’m not going to pretend I knew that already.

Let me represent the idea in code to help you understand better. We already know the moon orbits the earth, and orbits faster than the earth. It also orbits closer. Here’s the script for an example:

local function update()
    if character.Head.LocalTransparencyModifier == 1 then
        local now = tick()

        local orbit1 = Vector3.new(math.sin(now), 0, math.cos(now))

        -- multiply the now by 3 or speed up time and
        -- multiply the result by .1 to make the "orbit" smaller
        local orbit2 = Vector3.new(math.sin(now * 3), 0, math.sin(now * 3)) * .1

        humanoid.CameraOffset = orbit1 + orbit2 -- the sum of both orbits!
    end
end

Try it out! (you can always change the numbers to your liking, because I’m sure it may look pretty dramatic) You can even use three circles, for extra randomness.

Here’s the full script:

local RS = game:GetService("RunService")

local player = game.Players.LocalPlayer

-- this is how I do it, you can use game.Workspace.Camera if you'd like!
local camera = game.Workspace.CurrentCamera

-- this line waits and yields for the ".CharacterAdded" event to fire, and then it gets the character. We will need it.
local character = player.CharacterAdded:Wait()

local breathHeight = .3
local breathSpeed = 3

-- don't forget about the humanoid!
local humanoid = character:WaitForChild("Humanoid")

local function update()
	if character.Head.LocalTransparencyModifier == 1 then
	    local now = tick()

	    local orbit1 = Vector3.new(math.sin(now), 0, math.cos(now))

	    -- multiply the now by 3 or speed up time and
	    -- multiply the result by .1 to make the "orbit" smaller
	    local orbit2 = Vector3.new(math.sin(now * 3), 0, math.sin(now * 3)) * .1

	    humanoid.CameraOffset = orbit1 + orbit2 -- the sum of both orbits!
	end
end

RS.RenderStepped:Connect(update)

Now, if you tried out the effect, you may notice it looks sort of funky, only changing the x and z looks pretty weird, so you know what? Let’s merge the effects together!


Mixing them all together!

To mix the effects all together, it’s simple! Simply take the sum of all of the effects, this will achieve a very realistic effect. We can add the height to the two (or three (or four)) circles.

local function update()
	if character.Head.LocalTransparencyModifier == 1 then
	    local now = tick()

        local orbit1 = Vector3.new(math.sin(now), 0, math.cos(now)) * .3
        local orbit2 = Vector3.new(math.sin(now * 3), 0, math.sin(now * 3)) * .1
        local height = math.sin(now * math.pi) * .3

        humanoid.CameraOffset = Vector3.new(0, height, 0) + orbit1 + orbit2
	end
end

Try it out! (I added my own modifications to look more efficient)

Here’s the entire script for reference:

local RS = game:GetService("RunService")

local player = game.Players.LocalPlayer

-- this is how I do it, you can use game.Workspace.Camera if you'd like!
local camera = game.Workspace.CurrentCamera

-- this line waits and yields for the ".CharacterAdded" event to fire, and then it gets the character. We will need it.
local character = player.CharacterAdded:Wait()

local breathHeight = .3
local breathSpeed = 3

-- don't forget about the humanoid!
local humanoid = character:WaitForChild("Humanoid")

local function update()
	if character.Head.LocalTransparencyModifier == 1 then
		local now = tick()

		local orbit1 = Vector3.new(math.sin(now), 0, math.cos(now)) * .3
		local orbit2 = Vector3.new(math.sin(now * 3), 0, math.sin(now * 3)) * .1
		local height = math.sin(now * math.pi) * .3

		humanoid.CameraOffset = Vector3.new(0, height, 0) + orbit1 + orbit2
	end
end

RS.RenderStepped:Connect(update)

Conclusion

Thank you very much for reading, I appreciate it. I put a lot effort into this post, and it was my first. Hope you liked it!

43 Likes

Really thanks for creating this! Tim sure this will help a lot of Devs (and also me) to create great effects!

2 Likes

Also something I noticed, for the Players service you do game.Players, but the way you should ALWAYS do it is game: GetService(“Players”)

4 Likes

This is really just preference, but I’ll change it if you think so.

There’s absolutely no difference between using :GetService() and indexing the DataObject directly.

2 Likes

Yea, but if I’m not wrong the Documentation says that it’s better using GetService() than ., but you’re right

1 Like

GetService is of course better, because it’s compatible with all top level services. It’s always better to use :GetService(), but I use . with Players, because it’s faster. You’re right though.

2 Likes

Ok, I just found out why using GetService is better than ., if a Service us renamed, you will always get the right Service and if a Service doesnt exist, it will be created

2 Likes

tick() is on the road to deprecation. I recommend using time(), or os.clock()

1 Like

I know, and I won’t be changing that. I was planning on adding it in the post later, though. Clearly, os.clock() is intended for benchmarking, so I’m not using os.clock(). I’m trying to keep the post simple, straightforward and not overwhelm newer developers with the math, technicality behind it all.

I’m keeping it as tick().

4 Likes

Very nice, but you should make this more performant by assigning a variable to the head and:

  • Changing game.Players to game:GetService("Players") (it’s apparrently faster than using the dot operator for some reason)
  • Changing tick() to workspace.DistributedGameTime, time(), os.time(), DateTime.UnixTimestampMillis, or os.clock().
  • Changing game.Workspace to workspace
  • Changing players.CharacterAdded:Wait() to script.Parent and placing the script into StarterCharacterScripts (will prevent the script from stopping on respawn).

However, everything else in the script was well executed and you’ve explained how the math works really well. Good job!


Oh, and I would also like to point out that you can add to the delta time yourself which may or may not be faster than any time function:

local timePassed = 0
RS.RenderStepped:Connect(function(deltaTime)
timePassed += deltaTime
end)

you can also apply some effects to the addition of the time yourself
I’m unsure of the method’s performance increases.

4 Likes

This is deprecated, use os.time() instead

1 Like

This could work, but for simplicity, I’m keeping it as tick. Same goes for every other recommendation you gave. I appreciate the help, though. I will be adding a section for relevant substitutions for tick().

4 Likes

os.time() only alters things once per second because it’s an integer.
DateTime.UnixTimestampMillis won’t do anything, but any form of DateTime (e.g DateTime.now().UnixTimestampMillis) will jitter the camera around like crazy (because the gap between milliseconds is huge!) unless divided by 1000, which is expensive.
time(), os.clock() and workspace.DistributedGameTime act exactly as tick() did.

time() is the easiest and safest replacement, for anyone finding this thread a year later, like me.

1 Like