CameraUtil - Your new tool for simple camera manipulation! (OPEN-SOURCED)

Introduction
Many developers utilize camera manipulation to improve the visual appeal and quality of their games. This could be in the form of cutscenes, camera panning, camera rotations, and even camera shaking, which make gameplay much more captivating. I created a module called “CameraUtil” which allows developers to quickly call functions in sequences to move and position the camera with ease. This module utilizes CutsceneService by Vaschex and CameraShaker by sleitnick, as well as custom functions I’ve included myself. This module was intended for personal practice with OOP, but I decided to make it open-sourced as it could be useful for developers!

Why use this? + Special Features

  • Various functions and camera actions to choose from
  • Applicable for all camera instances, including viewports
  • Calling a function will stop other functions before running
  • Will condense your code and make it more readable
  • Extremely easy to set up and use

Here’s the set-up, documentation, and source code for the utility!

Setting Up
  1. Get the module HERE

  2. Load it into studio and place it in ReplicatedStorage
    image

  3. Insert a local script in StarterPlayerScripts or StarterCharacterScripts or in a GUI
    image

  4. Initialize the CurrentCamera (Copy and Paste this code)

wait(1)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CameraUtil = require(ReplicatedStorage.CameraUtil)
local functions = CameraUtil.Functions
local shakePresets = CameraUtil.ShakePresets

local cameraInstance = workspace.CurrentCamera
local camera = CameraUtil.Init(cameraInstance)

--[[
	-- FUNCTIONS --
	DisableControls
	StartFromCurrentCamera 
	EndWithCurrentCamera
	EndWithDefaultCamera
	YieldAfterCutscene
	FreezeCharacter
	CustomCamera
	
	-- SHAKE PRESETS --
	Bump
	Explosion
	Earthquake
	BadTrip
	HandheldCamera
	Vibration
	RoughDriving
]]--

-- REST OF YOUR CODE GOES BELOW HERE!
  1. You’re ready to manipulate the camera!
Examples and Documentation

Here is a documentation of a list of functions in this utility module.
Parameters with * represents a required parameter for the function to work.

camera:MoveTo(target*, duration, style, direction)
while true do
	camera:MoveTo(workspace.Red.CFrame, 1) wait(1)
	camera:MoveTo(workspace.Blue, 1, Enum.EasingStyle.Quad, "InOut") wait(1)
	camera:MoveTo(workspace.Green.CFrame, 1, "Quad", Enum.EasingDirection.InOut) wait(1)
	camera:MoveTo(workspace.Yellow, 1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut) wait(1)
end
camera:PointTo(target*, duration, style, direction)
camera:MoveTo(workspace.Center)
while true do
	camera:PointTo(workspace.Red, 1) wait(1)
	camera:PointTo(workspace.Blue, 1.5) wait(1.5)
	camera:PointTo(workspace.Green, 2, "Bounce", Enum.EasingDirection.Out) wait(2)
	camera:PointTo(workspace.Yellow, 0.5) wait(0.5)
end
camera:Rotate(origin, speed, axes)
camera:Rotate(nil, 12, "Y") wait(3)
camera:Rotate(workspace.Center.CFrame, 10, "X")
camera:Orbit(origin, speed, horizontalOffset, verticalOffset, axes)
camera:Orbit(workspace.Center, 8, 8, 2, "Y") wait(3)
camera:Orbit(workspace.Center, 6, 10, -2, "y", "X")
camera:Follow(part*)
camera:Follow(Player.Character.HumanoidRootPart)
camera:Lock(part*)
camera:Lock(part) wait(3)
camera:Lock(game.Players.LocalPlayer.Character.Head)
camera:FocusOnPart(part*)

*part: BasePart

camera:FocusOnPart(workspace.Part)
camera:FocusOnPlayer(player*)

*player: PlayerInstance (if nil, will focus on local player)
I don’t need to put an example video here, this function is basically like spectating a player

camera:SetFOV(fov*, duration, style, direction)
camera:SetFOV(140, 3)
camera:SetFOV(40, 3)
camera:DisconnectAll()

Disconnects and cancels all RunService connections and tweens

camera:DisconnectAll()
camera:Reset()

Refocuses camera back onto player

camera:Reset()
camera:CreateCutscene(data*, duration*, style, direction, functions)
  • data: Table of CFrames or Folder of BaseParts numbered in cutscene order
  • duration: Number (if nil, module will use default)
  • style: String or Enum.EasingStyle (if nil, module will use default)
  • direction: String or Enum.EasingDirection (if nil, module will use default)
  • functions: *Arguments (special functions from CutsceneService)

This returns a cutscene object, and thus, has other functions.

local cutscene = camera:CreateCutscene(
	workspace.Cutscene1, 
	3,
	Enum.EasingStyle.Quart, 
	Enum.EasingDirection.InOut, 
	functions.StartFromCurrentCamera, 
	functions.EndWithDefaultCamera, 
	functions.DisableControls
)
cutscene:Play()

FOR MORE DETAILS GO HERE

camera:CreateShake()

Returns shake object, which consists of more functions:

local shake = camera:CreateShake()
shake:ShakeSustain(shakePresets.Explosion) wait(3)
shake:StopSustained(1)

FOR MORE INFO GO HERE

Source Code (If you REALLY want to look at it, it's in the module anyways)
--// CameraUtil
--// Awesom3_Eric
--// June 2, 2021

--[[
		DOCUMENTATION

		-- Initialization -- 
		wait(1) 
		local ReplicatedStorage = game:GetService("ReplicatedStorage")
		local CameraController = require(ReplicatedStorage.CameraController)
		local currentCamera = workspace.CurrentCamera
		local camera = CameraController.CreateCamera(currentCamera)
		
		
		-- Custom Functions --
		camera:MoveTo(target*, duration, style, direction) 						-- Moves camera to target
		camera:PointTo(target*, duration, style, direction) 					-- Faces camera towards target
		camera:Orbit(origin*, speed, horizontalOffset, verticalOffset, axes) 	-- Orbits camera around specific target
		camera:Rotate(origin*, speed, axis) 									-- Rotates camera on an axis	
		
		camera:Follow(part*) 													-- Camera consistently faces and follows target direction
		camera:Lock(part*) 														-- Camera attaches itself to a basepart
		camera:FocusOnPart(part*) 												-- Spectating a player, but for a part
		camera:FocusOnPlayer(player*) 											-- Acts like a spectate function
		
		camera:SetFOV(fov*, duration, style, direction)							-- Fade Camera FieldOfView
		
		camera:DisconnectAll() 													-- Cancels all cutscenes, updates, and tweens
		camera:Reset() 															-- Focuses camera back on player
		
		
		-- Derived Functions --
		camera:CreateCustcene(data*, duration*, style, direction, functions) -- Returns Cutscene Object
			cutsceneObject:Play()
			cutsceneObject:Pause(seconds)
			cutsceneObject:Resume()
			cutsceneObject:Cancel()
			cutsceneObject.Completed:Connect(function() end)
			
			SEE MORE INFO HERE: https://devforum.roblox.com/t/cutsceneservice-smooth-cutscenes-using-bezier-curves/718571/1
			
			
		camera:CreateShake() -- Returns shake object
			shakeObject:Start()
			shakeObject:Stop()
			shakeObject:Shake(shakePreset)
			shakeObject:ShakeSustain(shakePreset)
			shakeObject:StopSustained(fadeOutTime)
			shakeObject:ShakeOnce(magnitude, roughness, fadeInTime, fadeOutTime, posInfluence, rotInfluence)
			shakeObject:StartShake(magnitude, roughness, fadeInTime, posInfluence, rotInfluence)
			
			SEE MORE INFO HERE: https://devforum.roblox.com/t/ez-camera-shake-ported-to-roblox/98482
			OR GO TO CameraController > CameraShaker for example code, and CameraShaker > CameraShakePresets for shakePresets
]]--


local CameraUtil = {}
CameraUtil.__index = CameraUtil

--// Services and Variables \\--
local Player = game:GetService("Players").LocalPlayer
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local CutsceneService = require(script.CutsceneService)
local CameraShaker = require(script.CameraShaker)

-- References and defaults
CameraUtil.CameraCache = {}
CameraUtil.Functions = CutsceneService.Functions
CameraUtil.ShakePresets = CameraShaker.Presets
CameraUtil.DEFAULTS = {
	EASING_STYLE 		= Enum.EasingStyle.Quad,
	EASING_DIRECTION 	= Enum.EasingDirection.InOut,
	DURATION 			= 0
}



--// Local Functions \\--

--|| Returns target as CFrame from CFrame or BasePart ||--
local function getTarget(target)
	if not target then
		return nil
	end
	local targetType = typeof(target)
	if not (targetType == "Instance" or targetType == "CFrame") then
		return nil
	end
	return targetType == "Instance" and target.CFrame or target
end

--|| Returns Number of Seconds ||--
local function getDuration(duration)
	if not duration then
		return CameraUtil.DEFAULTS.DURATION
	end
	if typeof(duration) ~= "number" then
		return nil
	end
	return duration >= 0 and duration or duration * -1
end

--|| Returns EasingStyle or Default ||--
local function getEasingStyle(style)
	if not style then
		return CameraUtil.DEFAULTS.EASING_STYLE
	end
	local styleType = typeof(style)
	if not (styleType == "string" or styleType == "EnumItem") then
		return nil
	end
	return styleType == "string" and Enum.EasingStyle[style] or style
end

--|| Returns EasingDirection or Default ||--
local function getEasingDirection(direction)
	if not direction then
		return CameraUtil.DEFAULTS.EASING_DIRECTION
	end
	local directionType = typeof(direction)
	if not (directionType == "string" or directionType == "EnumItem") then
		return nil
	end
	return directionType == "string" and Enum.EasingDirection[direction] or direction
end



--// Functions \\--

--|| Initialize Camera ||--
function CameraUtil.Init(camera: CameraInstance)	
	-- Return existing camera
	if not CameraUtil.CameraCache[camera] then
		-- Check if camera is a Camera
		if not (camera and typeof(camera) == "Instance" and camera:IsA("Camera")) then
			warn("Camera not properly defined! Function CreateCamera(camera)")
			return
		end
		
		-- Create object
		local data = {
			Camera 				= camera,
			CurrentTween		= nil,
			CurrentRun			= nil,
			Cutscenes 			= {},
		}		
		function data.CancelRun()
			if data.CurrentRun then
				data.CurrentRun:Disconnect()
			end
		end
		
		function data.CancelTween()
			if data.CurrentTween then
				data.CurrentTween:Cancel()
			end
		end
			
		CameraUtil.CameraCache[camera] = data
		setmetatable(data, CameraUtil)
	end		
	
	return CameraUtil.CameraCache[camera]
end

--|| Tweens Camera CFrame to Target, Optional duration, style, and direction ||--
function CameraUtil:MoveTo(target: BasePartOrCFrame, duration: Number, style: EasingStyle, direction: EasingDirection)
	self:DisconnectAll()
	
	-- Check validity of arguments
	target 		= getTarget(target)
	duration 	= getDuration(duration)
	style 		= getEasingStyle(style)
	direction 	= getEasingDirection(direction)

	if not (target and duration and style and direction) then
		warn("Check Function :MoveTo(target, duration, style, direction). Formatting Invalid.")
		return
	end
	
	-- Set CFrame if duration is 0, or tween if greater
	if duration == 0 then
		self.Camera.CFrame = target
	else
		self.CurrentTween = TweenService:Create(
			self.Camera,
			TweenInfo.new(duration, style, direction),
			{CFrame = target}
		); self.CurrentTween:Play()
	end
end

--|| Tweens Camera to Point towards a Target, Optional duration, style, and direction ||--
function CameraUtil:PointTo(target: BasePartOrCFrame, duration: Number, style: EasingStyle, direction: EasingDirection)
	self:DisconnectAll()
	
	-- Check validity of arguments
	target 		= getTarget(target)
	duration 	= getDuration(duration)
	style 		= getEasingStyle(style)
	direction 	= getEasingDirection(direction)
	if not (target and duration and style and direction) then
		warn("Check Function :PointTo(target, duration, style, direction). Formatting Invalid.")
		return
	end
	
	-- Set CFrame if duration == 0 or tween if greater
	local origin = self.Camera.CFrame
	if duration == 0 then
		self.Camera.CFrame = CFrame.new(origin.Position, target.Position)
	else
		self.CurrentTween = TweenService:Create(
			self.Camera,
			TweenInfo.new(duration, style, direction),
			{CFrame = CFrame.new(origin.Position, target.Position)}
		); self.CurrentTween:Play()
	end
end

--|| Camera Follows Motion of BasePart ||--
function CameraUtil:Follow(part: BasePart)
	self:DisconnectAll()
	
	-- Check if target is an Instance
	if typeof(part) ~= "Instance" and not part:IsA("BasePart") then
		warn("Target must be instance. Function :Follow(target)")
		return
	end
	
	-- Update Camera CFrame in RenderStepped
	local origin = self.Camera.CFrame
	self.CurrentRun = RunService.RenderStepped:Connect(function()
		if not (part and part.Parent) then
			self.CurrentRun:Disconnect()
		end
		self.Camera.CFrame = CFrame.new(origin.Position, part.Position)
	end)
end

--|| Lock Camera to BasePart's CFrame ||--
function CameraUtil:Lock(part: BasePart)
	print(self)
	self.CancelRun()
	
	-- Check if target is an Instance
	if typeof(part) ~= "Instance" and not part:IsA("BasePart") then
		warn("Target must be instance. Function :Follow(target)")
		return
	end
	
	-- Update Camera CFrame in RenderStepped
	self.CurrentRun = RunService.RenderStepped:Connect(function()
		if not (part and part.Parent) then
			self.CurrentRun:Disconnect()
		end
		self.Camera.CFrame = part.CFrame
	end)
end

--|| Tween Camera POV ||--
function CameraUtil:SetFOV(fov: Number, duration: Number, style: EasingStyle, direction: EasingDirection)
	if self.FOVTween then
		self.FOVTween:Cancel()
	end
	
	-- Check validity of arguments
	duration 	= getDuration(duration)
	style 		= getEasingStyle(style)
	direction 	= getEasingDirection(direction)
	if not (duration and style and direction) then
		warn("Check Function :SetFOV(fov, duration, style, direction). Formatting Invalid.")
		return
	end
	if typeof(fov) ~= "number" then
		warn("FOV must be a number. Function :SetFOV(fov, duration, style, direction)")
		return
	end
	
	-- Set Camera FOV if duration is 0 or tween if greater
	if duration == 0 then
		self.Camera.FieldOfView = fov
	else
		self.FOVTween = TweenService:Create(
			self.Camera,
			TweenInfo.new(duration, style, direction),
			{FieldOfView = fov}
		); self.FOVTween:Play()
	end
end

--|| Sets Camera Subject to Player Humanoid ||--
function CameraUtil:FocusOnPlayer(player: Player)
	self:DisconnectAll()
	
	-- Check if player is valid
	self.Camera.CameraType = Enum.CameraType.Custom
	if not (player and typeof(player) == "Instance" and player:IsA("Player")) then
		self:Reset()
		return
	end
	
	if player == Player then
		self.Camera.CameraSubject = (Player.Character or Player.CharacterAdded:Wait()):WaitForChild("Humanoid")
	else
		-- Set CameraSubject every 0.1 second (to check if player left the game)
		local update = tick()
		self.CurrentRun = RunService.Heartbeat:Connect(function()
			if tick() - update > 0.1 then
				update = tick()
				-- If player left, return back to player
				if not (player and player.Parent) then
					self.CurrentRun:Disconnect()
					self:Reset()
					return
				end

				local character = player.Character
				if character and character:FindFirstChild("Humanoid") then
					self.Camera.CameraSubject = character.Humanoid
				end
			end
		end)
	end
end

--|| Set CameraSubject to Instance ||--
function CameraUtil:FocusOnPart(part: BasePart)
	self:DisconnectAll()
	
	-- Check if part is a BasePart
	if not (part and typeof(part) == "Instance" and part:IsA("BasePart")) then
		warn("Part must be a BasePart. Function :FocusOnPart(part).")
		return
	end
	
	-- Set CameraSubject
	self.Camera.CameraType = Enum.CameraType.Custom
	self.Camera.CameraSubject = part
end

--|| Rotate Camera on Axes, Optional speed, axes ||--
function CameraUtil:Rotate(origin: BasePartOrCFrame, speed: Number, ...: Table)
	self:DisconnectAll()
	
	-- Reestablish variables if invalid
	origin = getTarget(origin)
	origin = not origin and self.Camera.CFrame or origin
	speed = (speed and typeof(speed) == "number") and speed or 1

	-- Get angles of rotation
	local axes = {...}
	local angles = {}
	angles.X = (table.find(axes, "X") or table.find(axes, "x")) and math.pi/(1000/speed) or 0
	angles.Y = (table.find(axes, "Y") or table.find(axes, "y")) and math.pi/(1000/speed) or 0
	angles.Z = (table.find(axes, "Z") or table.find(axes, "z")) and math.pi/(1000/speed) or 0
	if angles == {} then
		angles = {Y = math.pi/(1000/speed)}
	end
	
	-- Rotate on RenderStepped
	local index = 1
	self.CurrentRun = RunService.RenderStepped:Connect(function()
		self.Camera.CFrame = origin * CFrame.Angles(angles.X * index, angles.Y * index, angles.Z * index)
		index += 1
	end)
end

--|| Orbit Around BasePart, Optional speed, horizontalOffset, verticalOffset ||--
function CameraUtil:Orbit(origin: BasePartOrCFrame, speed: Number, horizontalOffset: Number, verticalOffset: Number, ...: Table)
	self:DisconnectAll()
	
	-- Reestablish variables if invalid
	origin = getTarget(origin)
	origin = not origin and self.Camera.CFrame or origin	
	speed = (speed and typeof(speed) == "number") and speed or 1
	horizontalOffset = (horizontalOffset and typeof(horizontalOffset) == "number") and horizontalOffset or 0
	verticalOffset = (verticalOffset and typeof(verticalOffset) == "number") and verticalOffset or 0
	
	-- Get angles of rotation
	local axes = {...}
	local angles = {}
	angles.X = (table.find(axes, "X") or table.find(axes, "x")) and math.pi/(1000/speed) or 0
	angles.Y = (table.find(axes, "Y") or table.find(axes, "y")) and math.pi/(1000/speed) or 0
	angles.Z = (table.find(axes, "Z") or table.find(axes, "z")) and math.pi/(1000/speed) or 0
	if angles == {} then
		angles = {Y = math.pi/(1000/speed)}
	end
	
	-- Orbit around BasePart on 
	local index = 1
	local offset = CFrame.new(0, verticalOffset, -horizontalOffset)
	self.CurrentRun = RunService.RenderStepped:Connect(function()
		local rotation = CFrame.Angles(angles.X * index, angles.Y * index, angles.Z * index)
		self.Camera.CFrame = CFrame.new((origin * rotation * offset).Position, origin.Position)
		index += 1
	end)
end

--|| Cancel Tweens and RunService connections ||--
function CameraUtil:DisconnectAll()
	for _, cutscene in ipairs(self.Cutscenes) do
		cutscene.CutsceneObject:Cancel()
	end
	self.CancelRun()
	self.CancelTween()
	self.Camera.CameraType = Enum.CameraType.Scriptable; wait()
end

--|| Refocus Camera back on Player ||--
function CameraUtil:Reset()
	self:FocusOnPlayer(Player)
end



--// Derived Functions \\--
--|| Returns Shake Object ||--
function CameraUtil:CreateShake()
	print(self)
	if not self.Shaker then
		self.Shaker = CameraShaker.new(Enum.RenderPriority.Camera.Value, function(shakeCFrame)
			self.Camera.CFrame *= shakeCFrame
		end)
		self.Shaker:Start()
	end
	return self.Shaker
end

--|| Cutscene Class ||--
-- I had to make a new cutscene class so that when :Play() is called, I can call ":DisconnectAll()"
local Cutscene = {}
Cutscene.__index = Cutscene

function CameraUtil:CreateCutscene(...: Args)
	local data = {}
	local cutscene = CutsceneService:Create(...)
	data.CutsceneObject = cutscene
	data.Completed = cutscene.Completed
	
	setmetatable(data, Cutscene)
	table.insert(CameraUtil.Camera.Cutscenes, data)
	return cutscene
end

function Cutscene:Play()
	CameraUtil.Camera:DisconnectAll()
	self.CutsceneObject:Play()
end
function Cutscene:Pause(...)
	self.CutsceneObject:Pause(...)
end
function Cutscene:Resume()
	self.CutsceneObject:Resume()
end
function Cutscene:Cancel()
	self.CutsceneObject:Cancel()
end



return CameraUtil

Please let me know if there are any suggestions or criticism (to the code or the content) you guys may have, I would be happy to consider it :slight_smile: Please Enjoy!

182 Likes

(post marked for deletion for privacy reasons)

8 Likes

Can’t wait to try this out!

I have been becoming more interested in Camera Manipulation and have been really wanting to learn more about it. Thanks to you I can now learn by actually reading it off scripts! Don’t get me wrong, Youtube videos and the Developer Hub are useful I just prefer to learn off of a script instead of watching a YouTube video or something.

But hey, Thank You! :grin:

4 Likes

Glad to hear it, I’m happy to provide you with the tool to do so! If you have any suggestions on what more to add hmu :slight_smile:

2 Likes

Love it. Suggestion: Set this as a metable to a seperate table with the .__index as the object.
Example:

function Module.get(cam)
     local funcTable = deepcopy(YourCameraUitlModuleAsATableWithAllFunctions) --recursive algorithm which creates a duplicate of the table

     setmetatable(funcTable, {__index = cam})
end

Adding that, you would be able to:

local camera = CameraUtil.Init(game.Workspace.CurrentCamera)
camera:Bump(...)
camera.FieldOfView = 70
4 Likes

Hello! Really great module but one thing, you have a disconnectall() function, is there a way to disconnect a specific cutscene/tween?

2 Likes

AH, thank you for the suggestion, I will implement this! It appears I have much more to learn about OOP :wink:

2 Likes

Hey, thank you!

You are able to cancel cutscenes after creating it by calling

cutscene:Cancel()

As for tween-related movement like :MoveTo() or :PointTo() or :SetFOV() (Considering you have set a duration), these tweens don’t retiurn objects, thus, there is no cancel function to them. HOWEVER, though this wasn’t documented (which I should change), you can call:

camera.CancelTween()

to cancel existing tweens or

camera.FOVTween:Cancel()

to cancel the SetFOV Tween. Tweens are also automatically canceled when another function is called so that the camera doesn’t glitch like crazy. Hope this helps :slight_smile:

2 Likes

It helps, thank you! Was wondering about this!

1 Like

Bro this is awesome, and I want to put it to use but I am getting this error right here:

If you can help with this problem, I would appreciate it! :pray:

1 Like

It seems like there’s an error in the cutscene service, which is a module I did not script. I could try and resolve it, though, the utility should still work while the error is there.

I am making a horror game and how would I make it so you can hold down a key to look behind you, and then when the input stops, you look in front of you again?

1 Like

WOOOOOOOOOOOOO, sheesh best module

6 Likes

Love it! This will speed up my game creation progress.

1 Like

If you do perhaps attempt to fix the issue, what would the ETA be?

2 Likes

Heyo! Love this module! Curious if this has been fixed?

2 Likes

I’ve been waiting a while for a fix. Let’s just hope that he will make it fast!

1 Like

@remiel430 @Aurified @Scahrlet

Apologies for the VERY long wait, I’m in the process of getting ready for my first year in college!

The error has been fixed :slight_smile:

2 Likes

Awesome, thank you so much! Just need a new copy of the module and can use the existing init code and it’ll work?

Also hope your first year goes great!!

1 Like

Right, the module should already be updated, so everything should work out :slight_smile:

Thank you!!

1 Like