Positioning camera + clamping player positions for couch co-op game

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.

1 Like

One possible issue with a system like this is if players resize the screen. If they do then the camera will view a smaller or differently-shaped area, which can make some players get dragged along out of seemingly nowhere.

To pull players along, the best I can think of is checking if the X position is too far, then the Y position. NOT the magnitude of the vectors of the player, as that’ll check in a circle, not a rectangle. Then, if they are out of bounds, simply bump them back into bounds with a CFrame change. It should be done in RenderStepped, individually per client, so it is as smooth as possible.

You’ll need to define the camera’s X and Y limits positions at the start where all clients can view them.

2 Likes

Sorry if I misexplained, but the camera is scriptable and won’t be resizable by the players - it’ll always be that top-down sort of look. In online multiplayer, different clients will be free to roam anywhere, but my system has multiple players on one client with a custom character system, so I need to find the edges of the camera and teleport any player back within the camera viewport to prevent them from going off-screen and probably getting lost.

My bad, I totally misinterpreted it
So it shouldn’t be an issue, you can just define the edges of the camera and individually check the Z and Z components of the player’s position, and nudge them back towards the center if they are too far. Make sure to do this in RenderStepped.

1 Like

I found some success in using some trig to calculate a radius for a “sphere of influence” that encapsulates every player and is constrained by an anchor point (the host/player 1)

It’s not perfect, this sample script doesn’t account for vertical displacement or horizontal screen space, but with some tweaking I think it’s at least, if nothing else, a good baseline to add onto, and I’ll probably take another crack at it later cuz math like this intrigues me
camera thing.rbxl (59.9 KB)

Script
--[[ Variables ]]--
-- Services --
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

local random = Random.new()

-- Character --
local character = script.Parent
local rootPart = character:WaitForChild("HumanoidRootPart")

-- Camera --
local camera = workspace.CurrentCamera

-- Constants --
local CAMERA_OFFSET = CFrame.new(-10, 8, -10)

--[[ Functions ]]--
local function getCameraBounds(fovDegrees : number, followDistance : number) : number
	local theta = math.rad(fovDegrees / 2)
	local screenSpace = math.tan(theta) * followDistance

	return screenSpace
end

local function getAnchorPoint() : Vector3
	return CollectionService:GetTagged("FocusAnchor")[1]:GetPivot().Position
end

local function getAveragedFocusPoint(anchorPoint : Vector3, maxDistance : number) : Vector3
	local characters = CollectionService:GetTagged("PlayerCharacter")
	
	local focusAggregate = Vector3.zero
	local aggregateTotal = 0
	for _, character in characters do
		if character:HasTag("FocusAnchor") then
			continue
		end
		
		focusAggregate += character:GetPivot().Position
		aggregateTotal += 1
	end
	
	local focusPoint = focusAggregate / aggregateTotal
	local toFocusPoint = (focusPoint - anchorPoint)
	
	if toFocusPoint.Magnitude > maxDistance then
		return anchorPoint + toFocusPoint.Unit * maxDistance
	end
	
	return focusPoint
end

local function dragAlongOutsiders(focusPoint : Vector3, maxDistance : number) : ()
	local characters = CollectionService:GetTagged("PlayerCharacter")
	
	for _, character in characters do
		local distance = (character:GetPivot().Position - focusPoint).Magnitude
		if distance <= maxDistance then
			continue
		end
		
		local newPosition = focusPoint + (character:GetPivot().Position - focusPoint).Unit * maxDistance
		local newCFrame = CFrame.new(newPosition) * character:GetPivot().Rotation
		character:PivotTo(newCFrame)
	end
end

local function updateCamera(dt : number) : ()	
	local anchorPoint = getAnchorPoint()
	local cameraBounds = getCameraBounds(camera.FieldOfView, CAMERA_OFFSET.Position.Magnitude)
	
	local focusPoint = getAveragedFocusPoint(anchorPoint, cameraBounds)
	dragAlongOutsiders(focusPoint, cameraBounds)
	
	local eyePosition = (CFrame.new(focusPoint) * CAMERA_OFFSET).Position
	camera.CFrame = CFrame.lookAt(eyePosition, focusPoint)
	
	workspace.FocusPoint.CFrame = CFrame.new(focusPoint)
	workspace.Bounds.Size = Vector3.new(0, cameraBounds * 2, cameraBounds * 2)
	workspace.Bounds.CFrame = CFrame.new(focusPoint) * CFrame.Angles(0, 0, math.pi / 2)
end

local function makeFakeCharacter() : ()
	local p = Instance.new("Part")
	p.Anchored = true
	p.CanCollide = false
	p.CanQuery = false
	p.BrickColor = BrickColor.random()
	p.Parent = workspace
	p:PivotTo(rootPart:GetPivot())
	p:AddTag("PlayerCharacter")
	
	while true do
		local direction = (random:NextUnitVector() * Vector3.new(1, 0, 1)).Unit
		local distance = random:NextNumber(12, 24)
		local speed = 16
		
		while distance > 0 do
			local dt = task.wait()
			local travel = direction * speed * dt
			if travel.Magnitude > distance then
				travel = travel.Unit * distance
			end
			
			p.Position = Vector3.new(p.Position.X, rootPart.Position.Y, p.Position.Z)
			p.CFrame = CFrame.lookAlong(p.Position + travel, direction)
			distance -= travel.Magnitude
		end
		
		task.wait(random:NextNumber(8, 12))
	end
end


local function init() : ()
	character:AddTag("PlayerCharacter")
	character:AddTag("FocusAnchor")
	
	for i = 1, 3 do
		task.spawn(makeFakeCharacter)
	end
	
	camera.CameraType = Enum.CameraType.Scriptable
	while true do
		local dt = task.wait()
		updateCamera(dt)
	end
end
init()
1 Like

This is actually really cool!

I’ve been editing my system in the meantime and I’ve gotten it to mostly work (the main problem was that I forgot to set the camera type to Scriptable which was creating wrong boundaries, how did I forget to do that??) and I’m now using rays to get the corners of the screen in world space. However, my calculations assume that the camera’s projection onto the ground is rectangular, which it never will be unless I set the camera to always look exactly top-down. My camera is ideally at an angle with a bit of distance away from the characters. It can afford a bit of a tilt but if it gets too close to being parallel to the ground then the corners are just too far off for it to be accurate.

In the image you can see the two corners at the top, which aren’t in the corner of the screen (ignoring GUIInset). The other two corners are not visible.

So, I need to project the camera shape accurately (a trapezium) and somehow calculate those bounds, because a rectangle isn’t accurate enough unless I shrink the size of the ‘safe zone’ (currently, it’s 80% of the screen, to allow for visibility near the edges).

Here’s my current relevant code:

local function getWorldCorners(camera, averagePosition)
	local viewportSize = camera.ViewportSize
	local viewportXStart = viewportSize.X * 0.1
	local viewportXEnd = viewportSize.X * 0.9
	local viewportYStart = viewportSize.Y * 0.1
	local viewportYEnd = viewportSize.Y * 0.9
	local screenCorners = {Vector2.new(viewportXStart, viewportYStart), Vector2.new(viewportXEnd, viewportYStart), Vector2.new(viewportXStart, viewportYEnd), Vector2.new(viewportXEnd, viewportYEnd)}
	local XStart, ZStart = math.huge, math.huge
	local XEnd, ZEnd = -math.huge, -math.huge
	local Y = averagePosition.Y --Y here is the floor height, this is just temporary - when there are actual maps, set the floor height dynamically so that verticality can occur
	for index, screenCorner in screenCorners do
		local newRay = camera:ViewportPointToRay(screenCorner.X, screenCorner.Y)
		local distance = (Y - newRay.Origin.Y) / newRay.Direction.Y
		local floorIntersectionPoint = newRay.Origin + newRay.Direction * distance
		XStart = math.min(XStart, floorIntersectionPoint.X) --If the current point's X is less than the stored one, replace it
		ZStart = math.min(ZStart, floorIntersectionPoint.Z)
		XEnd = math.max(XEnd, floorIntersectionPoint.X)
		ZEnd = math.max(ZEnd, floorIntersectionPoint.Z)
	end
	return XStart, XEnd, ZStart, ZEnd, Y
end

local function raycastAtPosition(raycastStart, direction, filterType, filterTag)
	local raycastParameters = RaycastParams.new()
	raycastParameters.FilterType = filterType
	local objectsToFilterFor = {}
	for index, descendant in workspace:GetDescendants() do
		if descendant:HasTag(filterTag) then
			table.insert(objectsToFilterFor, descendant)
		end
	end
	raycastParameters.FilterDescendantsInstances = objectsToFilterFor
	return workspace:Raycast(raycastStart, direction, raycastParameters)
end

function cameraService.updateCamera()
	local camera = workspace.CurrentCamera
	if camera.CameraType ~= Enum.CameraType.Scriptable then
		camera.CameraType = Enum.CameraType.Scriptable
	end
	local localPlayers = playerService.getLocalPlayers()
	local totalPosition = Vector3.zero
	local totalWeight = 0
	local leaderPrimaryPartPosition : Vector3, leaderPrimaryPartSize : Vector3
	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
			totalWeight += cameraLeaderAddedWeightPerExtraPlayer * #localPlayers
			leaderPrimaryPartPosition = primaryPart.Position
			leaderPrimaryPartSize = primaryPart.Size
		else
			totalPosition += primaryPart.Position
			totalWeight += 1
		end
	end
	if totalWeight > 0 then
		local averagePosition : Vector3 = totalPosition * (1 / totalWeight) --Multiply with the reciprocal because you cannot directly divide by the number of found characters
		local XStart, XEnd, ZStart, ZEnd, Y = getWorldCorners(camera, averagePosition)
		if leaderPrimaryPartPosition then
			if debuggingEnabled then
				minMinPart.Position = Vector3.new(XStart, Y, ZStart)
				minMaxPart.Position = Vector3.new(XEnd, Y, ZStart)
				maxMinPart.Position = Vector3.new(XStart, Y, ZEnd)
				maxMaxPart.Position = Vector3.new(XEnd, Y, ZEnd)
			end
			local newX = averagePosition.X
			local newZ = averagePosition.Z
			if XEnd < leaderPrimaryPartPosition.X then
				newX += 2 * (leaderPrimaryPartPosition.X - XEnd)
			elseif leaderPrimaryPartPosition.X < XStart then
				newX -= 2 * (XStart - leaderPrimaryPartPosition.X)
			end
			if ZEnd < leaderPrimaryPartPosition.Z then
				newZ += 2 * (leaderPrimaryPartPosition.Z - ZEnd)
			elseif leaderPrimaryPartPosition.Z < ZStart then
				newZ -= 2 * (ZStart - leaderPrimaryPartPosition.Z)
			end
			local newPosition = Vector3.new(newX, Y, newZ)
			if averagePosition ~= newPosition then
				averagePosition = newPosition
				print("Shifting camera to cover leader")
			end
		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
				if not character then
					warn("No character found for player object with name: " .. player.Name)
					continue
				end
				local primaryPart = character.PrimaryPart
				if not primaryPart then
					warn("No primary part found for character of player object with name: " .. player.Name)
					continue
				end
				local newX = math.clamp(primaryPart.Position.X, XStart, XEnd)
				local newZ = math.clamp(primaryPart.Position.Z, ZStart, ZEnd)
				if newX ~= primaryPart.Position.X or newZ ~= primaryPart.Position.Z then --If the non-leader is not on-screen then proceed to move them
					local newPosition = Vector3.new(newX, Y + 100, newZ) --Y + 100 to account for walls, for example
					local direction = Vector3.new(0, -250, 0) --Fire straight down 250 studs
					local raycastResult = raycastAtPosition(newPosition, direction, Enum.RaycastFilterType.Include, "LevelStructure")
					local success = false
					if raycastResult then
						local raycastHit = raycastResult.Instance
						if raycastHit:IsA("BasePart") then
							if raycastHit:HasTag("Floor") then
								local oldCFrame = primaryPart.CFrame
								primaryPart.CFrame = CFrame.new(raycastResult.Position + Vector3.new(0, primaryPart.Size.Y / 2 - 0.05, 0)) * (oldCFrame - oldCFrame.Position)
								success = true
							end
						end
					end
					if not success then
						warn("Unsuccessful, here is the raycast result:", raycastResult)
					end
				end
			end
		end
	else
		camera.CFrame = CFrame.lookAt(globalVariables.blackScreenPart.Position + Vector3.new(0, globalVariables.blackScreenPart.Size.Y + 0.05, 0), globalVariables.blackScreenPart.Position)
	end
	if centrePart then
		centrePart.Position = cameraService.currentCameraFocusPosition
	end
end

In the documentation for ViewportPointToRay(), it says that the returned ray is oriented in the direction of the camera, so maybe this could be used to accurately project the boundaries because I think right now I’m re-orienting the ray straight down, creating the rectangle. I’m gonna see if I can change it. If I can get these bounds to work then I think I can ditch the buggy system that moves the camera over to cover the leader because all players will always be visible if I drag them along.

Update:

Looks like my rays already got the four trapezium corners, I was just storing them as a rectangle by taking, for example, the corner with the smallest X and assuming the other corner on that Z axis edge has the same X position, which it would only have with a top-down camera on a flat surface. So I just need to read these corners correctly to get this working.

Ok, I’ve finally managed to (kind of) solve this issue.

See it working here

So I’ve got the trapezium boundaries working through some messy, probably longer-than-it-should-be code that calculates the triangles at the edges of the trapezium and clamps the player’s position into the border:

local screenCorners = getWorldCorners(camera, cameraService.currentCameraFocusPosition, true)
			local straightAxisIsX --Either X or Y
			local lowestToHighestOnStraightAxis = {} --Each point in order from smallest to largest X or Y value, depending on straightAxisIsX
			local lowestOnNonStraightAxis = math.huge
			local highestOnNonStraightAxis = -math.huge
			local primaryPartPosition = camera:WorldToViewportPoint(primaryPart.Position)
			local primaryPartX, primaryPartY = primaryPartPosition.X, primaryPartPosition.Y
			local firstCorner = screenCorners[1]
			for index, point in screenCorners do
				if point == firstCorner then
					continue
				end
				if point.X == firstCorner.X or point.Y == firstCorner.Y then
					straightAxisIsX = point.X == firstCorner.X
					break
				end
			end
			for index, point in screenCorners do
				local insertIndex
				if straightAxisIsX then
					lowestOnNonStraightAxis = math.min(point.Y, lowestOnNonStraightAxis)
					highestOnNonStraightAxis = math.max(point.Y, highestOnNonStraightAxis)
					for index2, existingPoint in ipairs(lowestToHighestOnStraightAxis) do
						if point.X < existingPoint.X then
							insertIndex = index2
							break
						end
					end
				else
					lowestOnNonStraightAxis = math.min(point.X, lowestOnNonStraightAxis)
					highestOnNonStraightAxis = math.max(point.X, highestOnNonStraightAxis)
					for index2, existingPoint in ipairs(lowestToHighestOnStraightAxis) do
						if point.Y < existingPoint.Y then
							insertIndex = index2
							break
						end
					end
				end
				if insertIndex then
					table.insert(lowestToHighestOnStraightAxis, insertIndex, point)
				else
					table.insert(lowestToHighestOnStraightAxis, point)
				end
			end
			if straightAxisIsX then
				primaryPartX = math.clamp(primaryPartX, lowestToHighestOnStraightAxis[1].X, lowestToHighestOnStraightAxis[4].X)
				primaryPartY = math.clamp(primaryPartY, lowestOnNonStraightAxis, highestOnNonStraightAxis)
				if primaryPartX < lowestToHighestOnStraightAxis[2].X then
					local pointsXDifference = lowestToHighestOnStraightAxis[2].X - lowestToHighestOnStraightAxis[1].X
					local primaryPartXProgress = pointsXDifference / (primaryPartX - lowestToHighestOnStraightAxis[1].X)
					local pointsYDifference = lowestToHighestOnStraightAxis[2].Y - lowestToHighestOnStraightAxis[1].Y
					local YBoundary = primaryPartXProgress * pointsYDifference
					if pointsYDifference > 0 then
						primaryPartY = math.clamp(primaryPartY, YBoundary, math.min(lowestToHighestOnStraightAxis[1].Y, lowestToHighestOnStraightAxis[2].Y))
					else
						primaryPartY = math.clamp(primaryPartY, YBoundary, math.max(lowestToHighestOnStraightAxis[1].Y, lowestToHighestOnStraightAxis[2].Y))
					end
				elseif primaryPartX > lowestToHighestOnStraightAxis[3].X then
					local pointsXDifference = lowestOnNonStraightAxis[4].X - lowestToHighestOnStraightAxis[3].X
					local primaryPartXProgress = 1 - pointsXDifference / (primaryPartX - lowestToHighestOnStraightAxis[3].X)
					local pointsYDifference = lowestToHighestOnStraightAxis[4].Y - lowestToHighestOnStraightAxis[3].Y
					local YBoundary = primaryPartXProgress * pointsYDifference
					if pointsYDifference > 0 then
						primaryPartY = math.clamp(primaryPartY, YBoundary, math.min(lowestToHighestOnStraightAxis[3].Y, lowestToHighestOnStraightAxis[4].Y))
					else
						primaryPartY = math.clamp(primaryPartY, YBoundary, math.max(lowestToHighestOnStraightAxis[3].Y, lowestToHighestOnStraightAxis[4].Y))
					end
				end
			else --Same as before but with all X's and Y's swapped
				primaryPartY = math.clamp(primaryPartY, lowestToHighestOnStraightAxis[1].Y, lowestToHighestOnStraightAxis[4].Y)
				primaryPartX = math.clamp(primaryPartX, lowestOnNonStraightAxis, highestOnNonStraightAxis)
				if primaryPartY < lowestToHighestOnStraightAxis[2].Y then
					local pointsYDifference = lowestToHighestOnStraightAxis[2].Y - lowestToHighestOnStraightAxis[1].Y
					local primaryPartYProgress = pointsYDifference / (primaryPartY - lowestToHighestOnStraightAxis[1].Y)
					local pointsXDifference = lowestToHighestOnStraightAxis[2].X - lowestToHighestOnStraightAxis[1].X
					local XBoundary = primaryPartYProgress * pointsXDifference
					if pointsYDifference > 0 then
						primaryPartX = math.clamp(primaryPartX, XBoundary, math.min(lowestToHighestOnStraightAxis[1].X, lowestToHighestOnStraightAxis[2].X))
					else
						primaryPartX = math.clamp(primaryPartX, XBoundary, math.max(lowestToHighestOnStraightAxis[1].X, lowestToHighestOnStraightAxis[2].X))
					end
				elseif primaryPartY > lowestToHighestOnStraightAxis[3].Y then
					local pointsYDifference = lowestOnNonStraightAxis[4].Y - lowestToHighestOnStraightAxis[3].Y
					local primaryPartYProgress = 1 - pointsYDifference / (primaryPartY - lowestToHighestOnStraightAxis[3].Y)
					local pointsXDifference = lowestToHighestOnStraightAxis[4].X - lowestToHighestOnStraightAxis[3].X
					local XBoundary = primaryPartYProgress * pointsXDifference
					if pointsXDifference > 0 then
						primaryPartX = math.clamp(primaryPartX, XBoundary, math.min(lowestToHighestOnStraightAxis[3].X, lowestToHighestOnStraightAxis[4].X))
					else
						primaryPartX = math.clamp(primaryPartX, XBoundary, math.max(lowestToHighestOnStraightAxis[3].X, lowestToHighestOnStraightAxis[4].X))
					end
				end
			end

Basically, I convert the player’s position into on-screen coordinates using WorldToViewportPoint() and then clamp the player’s position to within the greater rectangle (like before). Then, if they are in one of the edge sections, I get the position of the border point that corresponds to either their X or Y (depending on the rotation of the trapezium) and clamp the player’s position between that and either the top or bottom of the rectangle, depending on the gradient of the triangle.

If you watched the video you might notice if there’s something blocking the dragged player’s way, they disappear and respawn. I haven’t fully finished this yet, as I want to make them respawn next to the leader so that they can regroup, but basically before clamping the player’s position to within the trapezium, I take the X and Y screen coordinates and find the corresponding position on screen, and fire a raycast straight down from that position in search of any BasePart with the tag ‘LevelStructure’. If there is no hit or the hit doesn’t have the tag ‘floor’, it falls back to that respawn near leader system. So basically, the player can only be clamped onto an object with tags ‘LevelStructure’ and ‘Floor’. I’ll show the full system once it’s done.

Ok so it’s now all working - It’ll TP the non-leader player to any valid position in a 6 stud radius from the leader, and if that fails it’ll just TP them to the leader’s own CFrame.

						local radius = 6 --6 studs from primary part position
						local stepDegrees = 45
						local step = math.rad(stepDegrees)
						success = false
						for angle = 0, 2 * math.pi, step do
							local origin = leaderPrimaryPartPosition + Vector3.new(math.cos(angle), 0, math.sin(angle)) * radius
							local direction = Vector3.new(0, -2 * leaderPrimaryPartSize.Y, 0) --Remember, direction is basically just an offset from the origin!
							raycastResult = raycastAtPosition(origin, direction, Enum.RaycastFilterType.Include, "LevelStructure")
							if raycastResult then
								local raycastHit = raycastResult.Instance
								if raycastHit:IsA("BasePart") then
									if raycastHit:HasTag("Floor") then
										player:Unjail(CFrame.new(raycastResult.Position))
										success = true
										break
									end
								end
							end
						end

Here it is working

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.