RTS Unit Shared Destination Problem

Hello, I’ve been programming RTS style movement system, but I am struggling how I would solve the ‘destination problem’, where group of units are all told to go to one position but end up ‘fighting’ each other to achieve the target position.

I have implemented a system previously where it takes the mid-point position of all the units selected and then calculate the distance of each unit to that mid-point, and then apply those distances as offsets to the end position. This work great except when there is a unit far away from the main group, it will stay far away from the end position.

To solve this, I could just tell the units to make a circle around the end position, but then the units will end up fighting to get to their assigned end positions, unless I turned canCollide off, but decided not to turn it off as it would not look good.

As of now I’m stuck on where to go from here. I could go back to the original mid-point distance offset system; I also was looking at implementing a Boids algorithm possibly but have no idea how going about implementing this.

If anyone has any idea who to go about solving this shared destination problem, it would be very helpful.

The following code is my unit movement function, which only tells the units to go the same endPosition where they will fight for in order to end their loops, there are no formations or anything to tell the groups to organize into something.

local MoveInstanceClass = {}

--Services
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local selectedUnits = 0

function MoveInstanceClass.moveToPosition(player, targetPos, selectedUnit)
	for _, team in pairs(workspace.Teams:GetChildren()) do
		if team.Name == player.Team.Name then
			--Check if multiple Units are Selected
			selectedUnits = 0
			for _, unit in pairs(team.Units:GetChildren()) do
				if unit:GetAttribute("selected") == true then
					selectedUnits = selectedUnits + 1
					if selectedUnits > 1 then
						break
					end
				end
			end
			
			--Check if unit is selected, then assign properties
			if selectedUnit:GetAttribute("selected") == true then
				selectedUnit:SetAttribute("targetPoint", targetPos)
				selectedUnit:SetAttribute("repeating", true)
				selectedUnit.CollisionBox.AlignOrientation.CFrame = CFrame.lookAt(selectedUnit.PrimaryPart.Position, Vector3.new(selectedUnit:GetAttribute("targetPoint").X, selectedUnit.PrimaryPart.Position.Y, selectedUnit:GetAttribute("targetPoint").Z))

				for _, wayPoint in pairs(selectedUnit.ObjectValues.wayPoints:GetChildren()) do
					wayPoint:Destroy()
				end

				local wayPointPart = Instance.new("Part")
				wayPointPart.Anchored = true
				wayPointPart.Size = Vector3.new(1, 1, 1)
				wayPointPart.CanCollide = false
				wayPointPart.Transparency = .8
				wayPointPart.Position = selectedUnit:GetAttribute("targetPoint")
				wayPointPart.Parent = selectedUnit.ObjectValues.wayPoints

				local attachment2 = Instance.new("Attachment")
				attachment2.Parent = wayPointPart
				attachment2.WorldCFrame = wayPointPart.CFrame
			end
			
			--Begin Unit Movmement Loop
			local loop = true

			if selectedUnits > 1 then
				
				print("Multiple Units")
				
			else
				
				print("Single Unit")
			
				while loop do
					local allUnitsReachedTarget = true

					for _, repeatingUnit in pairs(team.Units:GetChildren()) do
						if repeatingUnit:GetAttribute("repeating") then
							repeatingUnit.CollisionBox.UnitVelocity.PlaneVelocity = Vector2.new(repeatingUnit:GetAttribute("targetPoint").X - repeatingUnit.PrimaryPart.Position.X, repeatingUnit:GetAttribute("targetPoint").Z - repeatingUnit.PrimaryPart.Position.Z).Unit * 5

							if (Vector3.new(repeatingUnit.PrimaryPart.Position.X, 0, repeatingUnit.PrimaryPart.Position.Z) - Vector3.new(repeatingUnit:GetAttribute("targetPoint").X, 0, repeatingUnit:GetAttribute("targetPoint").Z)).Magnitude > 1 then
								allUnitsReachedTarget = false
								repeatingUnit:SetAttribute("repeating", true)
							else
								repeatingUnit:SetAttribute("repeating", false)
								repeatingUnit.CollisionBox.UnitVelocity.PlaneVelocity = Vector2.new(0, 0)
							end
						end
					end

					if allUnitsReachedTarget then
						loop = false
					end

					task.wait()
				end
			end
		end
	end
end


return MoveInstanceClass

Ignoring if multiple units selected, this is what it would look like if the while loop to move the units are applied to all the units selected.

3 Likes

One idea is to generate each path separately and make the shortest paths to get to the center have the furthest targets which might solve the conflicts you’re having with sending them all in a specific structure.

Another potential solution is to make it so that you have units that are too far outside your structure come just to where it would collide with the other units that have already made it there. Basically walk them to the center until they hit a unit that has already stopped.

Finally maybe a solution that combines the 2 where units try to walk until one would hit another (calculating their positions a bit ahead of where they are) and when they are going to hit in the future swapping their targets might work? I feel like this solution might run into some issues though in some strange cases that cause weird conditions.

1 Like

I would calculate one path and create a grid around a center. In that grid, each unit has a place and tries to keep it. That way, you could more or less guarantee that units will retain their position during their movement.

-- Per unit
-- centerPosition will be dictated by the waypoints that result from pathfinding
-- gridPositionOffset is the relative offset in the grid from the centerPosition
desiredPosition = centerPosition + gridPositionOffset

There will be some more challenges ahead, but I would start simple and solve this problem first. With this approach, the units will collide with each other in order to find their place in the grid. But once that is over with, you will end up with a group that retains a grid formation.

1 Like