Hi,
I’m creating a game using a couch co-op system where up to 4 players can play on one device. I’m trying to make a camera system similar to the one used for multiplayer in Minecraft Dungeons - the camera should follow the group but take priority to a certain player (e.g. Player 1) to keep everyone together. I’ve got the group average aspect working, but there are two systems I just can’t get to work:
- Ensuring that the camera follows the ‘leader’ if the leader moves off-screen, so that the leader is always visible
- Clamping the positions of all non-leaders to within the camera, so they cannot run off-screen and if the camera moves faster than them they will be dragged along smoothly without changing the camera
Here’s what it’s like so far (The neon green part shows the focus point of the camera)
As you can see, near the SpawnLocation the camera seems to jitter around, but further away this does not happen. This is something to do with where I use WorldToViewportPoint() because earlier debug prints revealed it’s returning values that don’t seem to be accurate, such as negative coordinates whilst the ‘leader’ is on screen, coordinates in the thousands and tens of thousands, and coordinates close to 0 when the leader is in the centre of the screen (which is why the camera jittering is happening near the SpawnLocation). It should be returning the position of the leader character on screen in pixels (to my knowledge).
Also, Player 2 is not being dragged smoothly by the camera - rather, they are being clipped significantly within the camera view, which in turn sharply affects the camera position (due to the average position of all the players significantly changing) and causes them to be repeatedly bumped along the edge of the screen which doesn’t look great.
Here’s the function (running from a module script on the client) that updates the camera, firing every runService.RenderStepped()
function cameraService.updateCamera()
local camera = workspace.CurrentCamera
local localPlayers = playerService.getLocalPlayers()
local totalPosition = Vector3.zero
local totalWeight = 0
local leaderPrimaryPartPosition
for index, playerObject in localPlayers do
local character = playerObject.Character
if not character then
warn("No character found for player object with name: " .. playerObject.Name)
continue
end
local primaryPart = character.PrimaryPart
if not primaryPart then
warn("No primary part found for character of player object with name: " .. playerObject.Name)
continue
end
if playerObject.ClientCameraLeader then
totalPosition += primaryPart.Position * cameraLeaderAddedWeightPerExtraPlayer * (#localPlayers - 1)
totalWeight += cameraLeaderAddedWeightPerExtraPlayer * (#localPlayers - 1)
leaderPrimaryPartPosition = primaryPart.Position
else
totalPosition += primaryPart.Position
totalWeight += 1
end
end
if totalWeight > 0 then
local averagePosition = totalPosition * (1 / totalWeight) --Multiply with the reciprocal because you cannot directly divide by the number of found characters
if leaderPrimaryPartPosition then
local viewportSize = camera.ViewportSize
local edgeBufferX = viewportSize.X * 0.2
local edgeBufferY = viewportSize.Y * 0.2
local minX, maxX = edgeBufferX, viewportSize.X - edgeBufferX
local minY, maxY = edgeBufferY, viewportSize.Y - edgeBufferY
local positionOnScreen, onScreen = camera:WorldToViewportPoint(leaderPrimaryPartPosition)
if positionOnScreen.X < minX or positionOnScreen.X > maxX or positionOnScreen.Y < minY or positionOnScreen.Y > maxY then
print("Lerping")
averagePosition = averagePosition:Lerp(leaderPrimaryPartPosition, 0.1)
else
print("Not lerping")
end
else
print("Not lerping")
end
cameraService.currentCameraFocusPosition = averagePosition
camera.CFrame = CFrame.lookAt(cameraService.currentCameraFocusPosition + cameraService.currentCameraOffset, cameraService.currentCameraFocusPosition)
for index, player in localPlayers do
if not player.ClientCameraLeader then
local character = player.Character
local rootPart = character.RootPart
local viewportSize = camera.ViewportSize
local positionOnScreen, onScreen = camera:WorldToViewportPoint(rootPart.Position)
local z = rootPart.Position.Z
if not onScreen then
local x1 = camera:ViewportPointToRay(0, 0, z).Origin.X
local x2 = camera:ViewportPointToRay(viewportSize.X, 0, z).Origin.X
local x3 = {x1, x2}
if x1 > x2 then
x3 = {x2, x1}
end
local y1 = camera:ViewportPointToRay(0, 0, z).Origin.Y
local y2 = camera:ViewportPointToRay(0, viewportSize.Y, z).Origin.Y
local y3 = {y1, y2}
if y1 > y2 then
y3 = {y2, y1}
end
local clampedX = math.clamp(rootPart.Position.X, x3[1], x3[2])
local clampedY = math.clamp(rootPart.Position.Z, y3[1], y3[2])
rootPart.CFrame = CFrame.new(clampedX, rootPart.Position.Y, clampedY) * CFrame.fromOrientation(rootPart.Orientation.X, rootPart.Orientation.Y, rootPart.Orientation.Z)
end
end
end
else
camera.CFrame = CFrame.lookAt(globalVariables.blackScreenPart.Position - Vector3.new(0, globalVariables.blackScreenPart.Size.Y + 0.01, 0), globalVariables.blackScreenPart.Position)
end
if centrePart then
centrePart.Position = cameraService.currentCameraFocusPosition
end
end
And yes, I’m certain one leader is being set every time, no more or less. In the clip, the leader is the character I’m controlling.
Any help would be greatly appreciated.

