[UPDATE - Points Of Interest!] Isometric LazyCam: For when you're too lazy to write your own Diablo-style CameraScript

The newest version of LazyCam has arrived! Now the camera will focus on nearby interest points, like shops and monster spawners:


The camera is set to track the player from overhead at ~45 degree angle, and will follow slightly ahead of where they’re moving to get a better view of what’s directly ahead of them:


If the player isn’t moving, the camera will eventually center itself back to the player:

You can also zoom in and out by altering the FOV with the mouse wheel (and now -/+ keys):


Patch notes:

  • Camera will now gravitate toward/focus on tagged points of interest while player is within range
  • Most checks now done using values provided by function returns
  • Camera speed/max distance now based on zoom level to better keep the player in frame
  • Event handling that binds/unbinds camera controls depending on current state
    (in a shop, dead, etc.)
  • Slightly improved overall script organization

Here’s the script:

local Players = game:GetService("Players")
local RunSVC = game:GetService("RunService")
local ContextActionSVC = game:GetService("ContextActionService")
local CollectionSVC = game:GetService("CollectionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Events = ReplicatedStorage:FindFirstChild("Events")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local humanoid, rootPart

local camNearPlayer = false
local minZoom = 30
local maxZoom = 75
local currentZoom = 50
local minCamSpeed = 0.001
local maxCamSpeed = 0.025

local camGoal : CFrame
local focusPoint : Vector3
local camDistFromGoal : number
local currentCamSpeed : number

camera.CameraType = Enum.CameraType.Scriptable
camera.FieldOfView = currentZoom

local function getDist(object, goal) : number
	return (object.CFrame.Position - goal).Magnitude
end

local function setCamSpeed() : number
	return camNearPlayer and minCamSpeed
		or math.clamp(0.002*camDistFromGoal, minCamSpeed, maxCamSpeed)
end

local function newPointAheadOfPlayer() : Vector3
	return rootPart.CFrame.Position + (humanoid.MoveDirection * currentZoom/2)
end

local function pointIsNearby(point : CFrame) : CFrame
	return getDist(point, rootPart.CFrame.Position) < currentZoom*0.85
end

local function getNearestInterestPoint() : Vector3
	for _, point in pairs(CollectionSVC:GetTagged("PointOfInterest")) do
		if not pointIsNearby(point) then continue end
		return point.CFrame.Position
	end
	return nil
end

local function getNewFocusPoint() : Vector3
	return camDistFromGoal < 1 and rootPart.CFrame.Position
		or humanoid.MoveDirection.Magnitude == 0 and focusPoint
		or getNearestInterestPoint()
		or newPointAheadOfPlayer()
end

local function setCamGoal() : CFrame
	return CFrame.new(focusPoint + Vector3.new(-30,75,-30), focusPoint)
end

local function invalidState(state) : boolean
	return state == Enum.UserInputState.End
		or state == Enum.UserInputState.Cancel
		or state == Enum.UserInputState.None
end

local function isAtMaxZoom(isZoomingOut) : boolean
	return isZoomingOut and currentZoom >= maxZoom
		or not isZoomingOut and currentZoom <= minZoom
end

local function isZoomingOut(input : InputObject) : boolean
	return input.UserInputType == Enum.UserInputType.MouseWheel and input.Position.Z < 0
		or input.KeyCode == Enum.KeyCode.Minus
end

local function SetZoom(inputObject)
	local zoomingOut = isZoomingOut(inputObject)
	currentZoom += isAtMaxZoom(zoomingOut) and 0 or zoomingOut and 5 or -5
	camera.FieldOfView = currentZoom
end

local function GetZoom(action, inputState, inputObject : InputObject?)
	return invalidState(inputState) and Enum.ContextActionResult.Pass
		or SetZoom(inputObject)
end

local function camIsMoving() : boolean
	return camDistFromGoal > 0.1 and camDistFromGoal <= 10
end

local function OverheadCam()
	camDistFromGoal = getDist(camera, camGoal.Position)
	camera.CFrame = camera.CFrame:Lerp(camGoal, setCamSpeed())

	if getDist(rootPart, focusPoint) < currentZoom/3 and camIsMoving() then return end

	focusPoint = getNewFocusPoint()
	camGoal = setCamGoal()
	camNearPlayer = getDist(rootPart, focusPoint) < 5
end

local function BindCamera()
	focusPoint = rootPart.CFrame.Position
	camGoal = setCamGoal()
	camera.CFrame = camGoal
	
	RunSVC:BindToRenderStep("Camera", 0, OverheadCam)
	
	ContextActionSVC:BindAction("Zoom", GetZoom, false,
		Enum.KeyCode.Equals,
		Enum.KeyCode.Minus,
		Enum.UserInputType.MouseWheel
	)
end

local function UnbindCamera(char)
	RunSVC:UnbindFromRenderStep("Camera")
	ContextActionSVC:UnbindAction("Zoom")
	currentZoom = 50
	camera.FieldOfView = currentZoom
	if not char then return end
	humanoid, rootPart = nil
end

local function InitCamera(char)
	humanoid = char:WaitForChild("Humanoid")
	rootPart = char:WaitForChild("HumanoidRootPart")
	
	BindCamera()
	humanoid.Died:Connect(UnbindCamera)
end

local function SwapCamShopState(state)
	return state == 2 and BindCamera()
		or state == 1 and UnbindCamera()
end
-- used in combination with values used by a shop system
-- state 1 = Entering Shop
-- state 2 = Exiting Shop
-- state 0 = Dead/Invalid (handled exclusively by shop system for now,
-- since death is already handled here)

player.CharacterAdded:Connect(InitCamera)
player.CharacterRemoving:Connect(UnbindCamera)

Events.ShopMenuChanged.OnClientEvent:Connect(SwapCamShopState)

I’d greatly appreciate any feedback y’all can give me on what I’ve written so far.

Older versions below:

LazyCam 1.2
-- [[CameraScript]]--
-- Updated zoom function to use ContextActionService
local Players = game:GetService("Players")
local RunSVC = game:GetService("RunService")
local ContextActionSVC = game:GetService("ContextActionService")

local player = Players.LocalPlayer
local mouse = player:GetMouse()
local camera = workspace.CurrentCamera
local character, humanoid, rootPart

local focusPoint : CFrame
local camGoal : CFrame
local camDistFromGoal : number
local minZoom = 30
local maxZoom = 75
local currentZoom = 50

local camReturningToPlayer = false

camera.CameraType = Enum.CameraType.Scriptable
camera.FieldOfView = currentZoom

local function isAtMaxZoom(isZoomingOut) : boolean
	return isZoomingOut and currentZoom >= maxZoom
		or not isZoomingOut and currentZoom <= minZoom
end

local function checkIfZoomingOut(inputObject : InputObject) : boolean
	return inputObject.UserInputType == Enum.UserInputType.MouseWheel and inputObject.Position.Z < 0
		or inputObject.KeyCode == Enum.KeyCode.Minus
		or false
end

local function zoom(action, inputState, inputObject : InputObject)
	if inputState ~= Enum.UserInputState.Begin and inputState ~= Enum.UserInputState.Change then
		return Enum.ContextActionResult.Pass
	end
	local isZoomingOut = checkIfZoomingOut(inputObject)
	currentZoom += isAtMaxZoom(isZoomingOut) and 0 or isZoomingOut and 5 or -5
	camera.FieldOfView = currentZoom
end

local function setCamGoal() : CFrame
	return CFrame.new(focusPoint.Position + Vector3.new(-30,60,-30), focusPoint.Position)
end

local function getDist(object, goal) : number
	return (object.CFrame.Position - goal).Magnitude
end

local function setNewFocusPoint() : CFrame
	return camDistFromGoal < 1.1 and CFrame.new(rootPart.CFrame.Position)
		or humanoid.MoveDirection.Magnitude == 0 and focusPoint
		or CFrame.new(rootPart.CFrame.Position + (humanoid.MoveDirection * 20))
end

local function OverheadCam()
	camDistFromGoal = getDist(camera, camGoal.Position)
	camera.CFrame = camera.CFrame:Lerp(camGoal, camReturningToPlayer and .0005 or math.clamp(0.002*camDistFromGoal, 0.0005, 0.025))
	
	if getDist(rootPart, focusPoint.Position) < 22 and camDistFromGoal <= 10 and camDistFromGoal >= 1 then return end
	
	focusPoint = setNewFocusPoint()
	camGoal = setCamGoal()
	
	camReturningToPlayer = focusPoint == rootPart.CFrame
end

local function UnbindCamera()
	character, humanoid, rootPart = nil
	RunSVC:UnbindFromRenderStep("Camera")
	ContextActionSVC:UnbindAction("Zoom")
end

player.CharacterAdded:Connect(function(char)
	character = char
	humanoid = char:WaitForChild("Humanoid")
	rootPart = char:WaitForChild("HumanoidRootPart")
	
	focusPoint = rootPart.CFrame
	camGoal = setCamGoal()
	camera.CFrame = camGoal
	
	RunSVC:BindToRenderStep("Camera", 0, OverheadCam)
	ContextActionSVC:BindAction("Zoom", zoom, false, Enum.KeyCode.Equals, Enum.KeyCode.Minus, Enum.UserInputType.MouseWheel)
	
	humanoid.Died:Connect(UnbindCamera)
end)

player.CharacterRemoving:Connect(UnbindCamera)
LazyCam 1.1
--[[CameraScript]]--
local Players = game:GetService("Players")
local RunSVC = game:GetService("RunService")

local player = Players.LocalPlayer
local mouse = player:GetMouse()
local camera = workspace.CurrentCamera
local character, humanoid, rootPart

local focusPoint : CFrame
local camGoal : CFrame
local camDistFromGoal : number
local minZoom = 30
local maxZoom = 75
local currentZoom = 50
local zoomIn, zoomOut

camera.CameraType = Enum.CameraType.Scriptable
camera.FieldOfView = currentZoom

-- determines whether the player is trying to zoom past the min/max zoom FOVs
local function isAtMaxZoom(isZoomingOut) : boolean
	return isZoomingOut and currentZoom >= maxZoom or not isZoomingOut and currentZoom <= minZoom
end

-- To be used for adjusting FOV when called using a Connection
local function zoom(isZoomingOut)
	return function()
		currentZoom += isAtMaxZoom(isZoomingOut) and 0 or isZoomingOut and 5 or -5
		camera.FieldOfView = currentZoom
	end
end

-- Gives the camera a goal position and focus point to move to
local function setCamGoal() : CFrame
	return CFrame.new(focusPoint.Position + Vector3.new(-30,60,-30), focusPoint.Position)
end

local function getDist(object, goal) : number
	return (object.CFrame.Position - goal).Magnitude
end

-- sets new camera focus point depending on current camera/player behavior
local function setNewFocusPoint() : CFrame
	return camDistFromGoal <= 3 and CFrame.new(rootPart.CFrame.Position)
		or humanoid.MoveDirection.Magnitude == 0 and focusPoint
		or CFrame.new(rootPart.CFrame.Position + (humanoid.MoveDirection * 20))
end

-- main function, lerps camera toward goal depending on player position and movement.
local function OverheadCam()
	camDistFromGoal = getDist(camera, camGoal.Position)
	camera.CFrame = camera.CFrame:Lerp(camGoal, math.clamp(0.002*camDistFromGoal, 0.0005, 0.025))
	
	if getDist(rootPart, focusPoint.Position) < 22 and camDistFromGoal <= 2 then return end
	
	focusPoint = setNewFocusPoint()
	camGoal = setCamGoal()
end

-- to be called when the character dies/is removed
local function UnbindCamera()
	character, humanoid, rootPart = nil
	RunSVC:UnbindFromRenderStep("Camera")
	zoomIn:Disconnect()
	zoomOut:Disconnect()
end

player.CharacterAdded:Connect(function(char)
	character = char
	humanoid = char:WaitForChild("Humanoid")
	rootPart = char:WaitForChild("HumanoidRootPart")
	
	focusPoint = rootPart.CFrame -- camera focuses on character position when spawning
	camGoal = setCamGoal()
	camera.CFrame = camGoal
	
	RunSVC:BindToRenderStep("Camera", 0, OverheadCam)
	zoomIn = mouse.WheelForward:Connect(zoom(false))
	zoomOut = mouse.WheelBackward:Connect(zoom(true))
	
	humanoid.Died:Connect(UnbindCamera)
end)

player.CharacterRemoving:Connect(UnbindCamera)
LazyCam 1.0
local Players = game:GetService("Players")
local RunSVC = game:GetService("RunService")

local player = Players.LocalPlayer

local camera = workspace.CurrentCamera
camera.CameraType = Enum.CameraType.Scriptable

local maxZoom = 35
local minZoom = 75
local currentZoom = 50

local character, humanoid, rootPart

local focusPoint : CFrame
local camGoal : CFrame
local camDistFromGoal : number

local function setCamGoal() : CFrame
	return CFrame.new(focusPoint.Position + Vector3.new(-30,60,-30), focusPoint.Position)
end

local function getDist(object, goal) : number
	return (object.CFrame.Position - goal).Magnitude
end

local function setNewFocusPoint() : CFrame
	return camDistFromGoal <= 3.1 and CFrame.new(rootPart.CFrame.Position)
		or humanoid.MoveDirection.Magnitude == 0 and focusPoint
		or CFrame.new(rootPart.CFrame.Position + (humanoid.MoveDirection * 20))
end

local function OverheadCam()
	
	camDistFromGoal = getDist(camera, camGoal.Position)
	camera.CFrame = camera.CFrame:Lerp(camGoal, math.clamp(0.002*camDistFromGoal, 0.0005, 0.025))
	
	if getDist(rootPart, focusPoint.Position) > 22 or camDistFromGoal > 3 then
		focusPoint = setNewFocusPoint()
		camGoal = setCamGoal()
	end
	
end

local zoomIn, zoomOut

local function zoom(out, zoom)
	return function()
		currentZoom += (out and currentZoom >= zoom or not out and currentZoom <= zoom) and 0 or out and 5 or -5
		camera.FieldOfView = currentZoom
	end
end

player.CharacterAdded:Connect(function(char)
	character = char
	humanoid = char:WaitForChild("Humanoid")
	rootPart = char:WaitForChild("HumanoidRootPart")
	
	focusPoint = rootPart.CFrame
	camGoal = setCamGoal()
	camera.CFrame = camGoal
	
	RunSVC:BindToRenderStep("Camera", 0, OverheadCam)
	
	zoomIn = player:GetMouse().WheelForward:Connect(zoom(false, maxZoom))
	
	zoomOut = player:GetMouse().WheelBackward:Connect(zoom(true, minZoom))
	
end)

player.CharacterRemoving:Connect(function(char)
	character, humanoid, rootPart = nil
	RunSVC:UnbindFromRenderStep("Camera")
	zoomIn:Disconnect()
	zoomOut:Disconnect()
end)
24 Likes

Is there a RBXL file for this?

For sure!

Here’s the place file:
merchant.rbxl (129.8 KB)

Feel free to use whatever you want out of it, just note that I have been fiddling with it in my spare time, so it’s lacking comments and a few things have moved around. It’s still pretty bare-bones, so the formatting of the place is pretty wonky atm, just be warned lmao

I plan on updating the post to reflect changes I’ve been making, so I’ll be sure to add that to the parent post as I make progress that feels significant enough to warrant being bumped on the Resources page

1 Like

Appreciated!

charlimitmoment

Is there a way to properly install this? In my game at the moment the camera seems to be all wobbly and not really lock onto any parts.

I’m not sure what the issue could be. I redownloaded the RBXL and ran it from there too, and it all still seems to be running fine for me. The whole place file is vanilla. I didn’t use any plugins except for a Tag Editor.

If the issue is arising after adding new points of interest, are they being tagged properly and contained in the right folders? And is the CameraType properly getting set to Scriptable when the player joins/spawns?

Could also just be an issue with studio. Maybe a reinstall could help.

Not to be annoying, but is there a barebones version of this? Just like a testing place perhaps?

No, it’s the only one I currently have for the project. Sorry!

Alright! I recommend making a testing place, it’d help it so anyone can pick it up easily as all the shop-things make it sort of hard to understand.