Preventing character from leaving map boundary

I thought it would be fun to remake a .io game on Roblox as a break from my current project. It is pretty much complete, except I need to ensure the player can’t leave the playspace. To do this, I am trying to make it so they can touch the edge of the playspace, but they can’t go any further.

However, this is not exactly working as intended. Currently, there are three outcomes (none of which are what I expected):

A) https://gyazo.com/78a99985d94f5571e78309b876918117
The character gets sent to some magical realm (typically, it is -3000x, 3000x, -3000z, or 3000z).

B) https://gyazo.com/6032e43747aebda2468c618a868ca41a
The character boundary works as intended… except it is roughly 1/5 studs too far inside the play area.

C) https://gyazo.com/a3bbfd1ea0d10b86c5cf064f818895fe
This is the closest it gets. The character can’t go past the edge; However, it glitches rapidly between being on the edge as it should and the behavior described in point B.

NOTE: All of these outcomes are from the same code.

The following is the section of code responsible for calculating the movement (some parts were removed to skim it down to its bones for the sake of your sanity):

local StudsPerStep = 0.5
local LastUpdatedSize = 5

local function ReturnLargestNumber(a: number, b:number)
	if a > b then
		return a,b
	elseif b > a then
		return b,a
	end

	return a,b
end

local function CalculateDifference(a: number, b:number)
	local largest, smallest = ReturnLargestNumber(a,b)
	
	return largest - smallest
end

local function isNegative(n: number)
	if n >= 0 then
		return true
	end

	return false
end

local function CalculateMovementInBorders(position: Vector3, size: Vector3, rate: number, x: number, z:number)
	if (x == 0) and (z == 0) then
		return x, z
	end

	local positionX
	local positionZ 

	if isNegative(position.X) then
		positionX = position.X + (size.X / 2)
	else
		positionX = position.X - (size.X / 2)
	end

	if isNegative(position.Z) then
		positionZ = position.Z + (size.Z / 2)
	else
		positionZ = position.Z - (size.Z / 2)
	end

	local calculatedX = positionX + x
	local calculatedZ = positionZ + z

	local insidePositiveX = if (calculatedX < 1024) then true else false
	local insideNegativeX = if (calculatedX > -1024) then true else false
	local insidePositiveZ = if (calculatedZ < 1024) then true else false
	local insideNegativeZ = if (calculatedZ > -1024) then true else false

	if insidePositiveX and insideNegativeX and insidePositiveZ and insideNegativeZ then
		return x,z
	end

	local differenceX = 0
	local differenceZ = 0

	if (not insidePositiveX) or (not insideNegativeX) then
		if isNegative(calculatedX) then
			differenceX = CalculateDifference(calculatedX,-1024)
		else
			differenceX = CalculateDifference(calculatedX,1024)
		end
	end

	if (not insidePositiveZ) or (not insideNegativeZ) then
		if isNegative(calculatedZ) then
			differenceZ = CalculateDifference(calculatedZ,-1024)
		else
			differenceZ = CalculateDifference(calculatedZ,1024)
		end
	end

	local updatedX = if (insidePositiveX and insideNegativeX) then x else differenceX
	local updatedZ = if (insidePositiveZ and insideNegativeZ) then z else differenceZ

	return updatedX,updatedZ
end

RunService.RenderStepped:Connect(function()
	if (not Character) or (not HumanoidRootPart) then
		return
	end

	local x = 0
	local z = 0

	if Keys.W or Keys.Up then
		x = x + StudsPerStep
	end

	if Keys.A or Keys.Left then
		z = z - StudsPerStep
	end

	if Keys.S or Keys.Down then
		x = x - StudsPerStep
	end

	if Keys.D or Keys.Right then
		z = z + StudsPerStep
	end
	
	local CharacterCFrame = HumanoidRootPart.CFrame
	
	x, z = CalculateMovementInBorders(CharacterCFrame.Position,HumanoidRootPart.Size,StudsPerStep,x,z)

	if (x == 0) and (z == 0) then
		if Tween then
			Tween:Cancel()
			Tween = nil
		end
	else
		local CalculatedCFrame = CharacterCFrame + Vector3.new(x,0,z)

		if CalculatedCFrame ~= CharacterCFrame then
			CharacterCFrame = CalculatedCFrame

			local difference = (CalculatedCFrame.Position - CharacterCFrame.Position).Magnitude

			if Tween then
				Tween:Cancel()
			end

			local properties = {CFrame = CalculatedCFrame}

			Tween = TweenService:Create(HumanoidRootPart,TweenInfo.new(difference,Enum.EasingStyle.Linear,Enum.EasingDirection.InOut),properties)
			Tween:Play()
		end
	end
end)

NOTE: The playspace is a 2048 x 2048 area position at 0,0,0.

I am guessing it is something with my math, but I have been staring at this for too long and am clearly overlooking something. Any help is much appreciated.

I think you can simplify your CalculateMovementInBorders function a bit.

local MapSize = Vector3.new(2048, 0, 2048)
local MapCenter = Vector3.new(0, 0, 0)

local MapMinimumBounds = MapCenter - MapSize / 2
local MapMaximumBounds = MapCenter + MapSize / 2

local function CalculateMovementInBorders(position: Vector3, size: Vector3, movement_x: number, movement_z: number)
	local half_size = size / 2
	
	local movement_vector = Vector3.new(movement_x, 0, movement_z)
	
	-- Get the next position with the movement vector applied.
	local movement_position = position + movement_vector
	
	-- Find the maximum and minimum map boundaries relative to the character size.
	local minimum_bound_size_offset = MapMinimumBounds + half_size
	local maximum_bound_size_offset = MapMaximumBounds - half_size
	
	-- Get the position clamped by the maximum and minimum boundaries relative to the character size.
	local adjusted_movement_position = movement_position:Max(minimum_bound_size_offset):Min(maximum_bound_size_offset)
	
	-- Get the relative movement vector.
	local adjusted_movement_vector = adjusted_movement_position - position

	return adjusted_movement_vector.X, adjusted_movement_vector.Z
end

In my mind something like this would work pretty well. I tested it a bit in my own place and from what I could tell it works correctly.

Note that I removed the rate parameter, I didn’t see it being used anywhere in the function.

I used a few built-in functions for getting the minimum and maximum vectors, see
Vector3 | Roblox Creator Documentation Max and Min on Vector3s.

Here’s some test data I put in to see how things were looking.

-- Should be -1, -1 (technically already outside of the map boundaries.)
print(CalculateMovementInBorders(Vector3.new(1020, 0, 1020), Vector3.new(10, 0, 10), 10, 10))

-- Should be 9, 9 since we can only move 9 studs until we hit the map boundary.
print(CalculateMovementInBorders(Vector3.new(1010, 0, 1010), Vector3.new(10, 0, 10), 10, 10))

-- Should be -9, 10 since we'll hit the minimum boundary on the X axis.
print(CalculateMovementInBorders(Vector3.new(-1010, 0, -1010), Vector3.new(10, 0, 10), -10, 10))

-- Should be -9, -9 since we'll hit the minimum boundary on the X and Z axis.
print(CalculateMovementInBorders(Vector3.new(-1010, 0, -1010), Vector3.new(10, 0, 10), -10, -10))

-- Should be 0, 0 since we're not moving at all.
print(CalculateMovementInBorders(Vector3.new(-1010, 0, -1010), Vector3.new(10, 0, 10), 0, 0))

-- Should be -9, 9 since we'll be running into the top left corner.
print(CalculateMovementInBorders(Vector3.new(-1010, 0, 1010), Vector3.new(10, 0, 10), -10, 10))

-- Should be 9, -9 since we'll be running into the bottom right corner.
print(CalculateMovementInBorders(Vector3.new(1010, 0, -1010), Vector3.new(10, 0, 10), 10, -10))

-- Should be 10, -10 since we aren't hitting any boundaries.
print(CalculateMovementInBorders(Vector3.new(0, 0, 0), Vector3.new(10, 0, 10), 10, -10))

Results:

  > -1 -1
  >  9 9
  >  -9 10
  >  -9 -9
  >  0 0
  >  -9 9
  >  9 -9
  >  10 -10
1 Like

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