Good news - yesterday I was able to get RVO working with ECS without using game-objects. It’s very fast! Like you suggested, I used SimulationBurst.simulationData (and also SimulationBurst.outputData).
While it mostly works, there are a few different problems I’m having and I’m hoping you can shed some light on it. My path code is at the bottom of this post.
Problem 1) The y component (altitude) value in SimulationBurst.outputData doesn’t align with the terrain mesh. If there are hills and valleys, the agents end up floating above or under ground. It’s as if the y component is wrong. The x and z components seem fine. I suspect there is something I need to do in order to fix it, but I’m not sure what. Currently I am sampling the terrain to fetch an appropriate y value, but I’m pretty sure that’s not the best way to do it.
Problem 2) Essentially, I combined the local avoidance script here Custom Local Avoidance Movement Script - A* Pathfinding Project with the basic movement script here: Writing a movement script - A* Pathfinding Project . It works fine if there are not a lot of agents, but if crowding starts to happen, some of the agents will get pushed around and it can cause them to miss one of their waypoints. When that happens, at some point they might get free, but then they’ll start going backwards until they get near the waypoint that was missed. It looks really weird when suddenly agents just start backtracking and retreating for no apparent reason. Is there a common way to handle this? Or is my approach all wrong to begin with?
Problem 3) If there are a lot of agents, some of them can get pushed off the navmesh. What’s the best way of preventing that?
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Pathfinding.RVO;
public partial class PathMovementSystem : SystemBase
{
SimulatorBurst m_simBurst;
protected override void OnCreate()
{
RequireSingletonForUpdate<TerrainHeightData>();
}
protected override void OnStartRunning()
{
m_simBurst = UnityEngine.GameObject.Find("RvoSimulator").GetComponent<RVOSimulator>().simulatorBurst;
}
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
TerrainHeightData terrainMap = GetSingleton<TerrainHeightData>();
// fetch two simulation arrays to be used in the job below
SimulatorBurst.AgentData simData = m_simBurst.simulationData;
SimulatorBurst.AgentOutputData simOutData = m_simBurst.outputData;
Entities.WithAll<EnableMovementTag, LiveTag>().ForEach((ref Rotation rot, ref WaypointIndexData waypoint, ref Translation pos, in MoveSpeedData speed, in DynamicBuffer<WaypointData> path, in LocalToWorld ltw, in RvoAgentData rvoAgent) =>
{
if (path.Length <= 0) { return; }
if (waypoint.hasReachedEndOfPath == true) { return; }
int agentIndex = rvoAgent.agentIndex;
WaypointSelectResult result = PathUtils.SelectWaypointIndex(pos.Value, 6f, waypoint.index, path);
// only set the target again if the next waypoint has changed
if ((waypoint.index == 0) || (waypoint.index != result.currentIndex)) {
// set the new waypoint index
waypoint.index = result.currentIndex;
waypoint.hasReachedEndOfPath = result.hasReachedEndOfPath;
// if this is the end of the path, then we should return (the code below wont make sense because the target waypoint
// will be very close or the same as the current position and that will give a bad direction.
if (waypoint.hasReachedEndOfPath) { return; }
// fetch the position for the current waypoint
float3 curWaypointPos = path[waypoint.index].point;
simData.SetTarget(agentIndex, curWaypointPos, speed.speed, speed.speed * 1.4f, path[path.Length - 1].point);
DebugDrawUtils.DrawCross(curWaypointPos, 0.8f, UnityEngine.Color.white, 5f);
}
// This is the point and speed that the RVO agent should move to, as dicatated by the RVO sim.
// If it's the only agent around, then this point will likely be the same as the current waypoint position.
// However, if there are a lot of other agents or obstacles in the vicinity, then this point will likely
// be different so that the agent can avoid bumping into other agents or obstacles.
// Note that this point has an incorrect height (y value) that doesn't match the terrain. I don't know why.
// This point is the same as rvoAgent.CalculatedTargetPoint.
float3 targetPt = simOutData.targetPoint[agentIndex];
// Fetch the desired speed from the sim output (same as rvoAgent.CalculatedSpeed)
float calculatedSpeed = simOutData.speed[agentIndex];
// Because targetPt has an incorrect height (not sure why), I must sample the appropriate height from the terrain,
// and set its target point's y value to that instead.
float height = terrainMap.SampleHeight(targetPt);
targetPt.y = height;
// This is the code that is in CalculateMovementDelta(),
// which is used in this example: https://arongranberg.com/astar/docs/localavoidanceintegration.html
// It doesn't really seem to do much, so I removed it.
UnityEngine.Vector2 delta = simData.movementPlane[agentIndex].ToPlane(targetPt - pos.Value);
float3 movementDelta = simData.movementPlane[agentIndex].ToWorld(UnityEngine.Vector2.ClampMagnitude(delta, calculatedSpeed * deltaTime), 0);
// Calculate the direction to the next calculated target point.
float3 dir = math.normalize(targetPt - pos.Value);
// Set the entity's rotation to face in the direction of movement.
rot.Value = quaternion.LookRotation(dir, math.up());
// Move the entity in the necessary direction by the amount at delta-time multiplied with the calculated speed.
pos.Value += (dir * deltaTime * calculatedSpeed);
//pos.Value += movementDelta;
// If the height of the entity is way above or way below the terrain, teleport it to terrain level.
// This is needed because for some reason a small percentage of entities randomly
// end up walking through the air or below ground, especially ones that missed a waypoint.
float heightAtCurPos = terrainMap.SampleHeight(pos.Value);
if (math.abs(heightAtCurPos - pos.Value.y) > 1f) { pos.Value.y = heightAtCurPos + 0.5f; }
// The sim's position must be updated as well (it should mirror the entity's position)
simData.position[agentIndex] = pos.Value;
}).Schedule();
}
}
Where PathUtils.SelectWaypointIndex() is defined as:
public static WaypointSelectResult SelectWaypointIndex(float3 pos, float desiredDistance, int currentWaypointIndex, in DynamicBuffer<WaypointData> path)
{
WaypointSelectResult result;
result.currentIndex = currentWaypointIndex;
result.desiredDistanceSq = desiredDistance * desiredDistance;
result.hasReachedEndOfPath = false;
result.distanceSq = 0f;
while (true) {
result.distanceSq = math.distancesq(pos, path[result.currentIndex].point);
if (result.distanceSq < result.desiredDistanceSq) {
// Check if there is another waypoint or if we have reached the end of the path
if ((result.currentIndex + 1) < path.Length) {
result.currentIndex++;
} else {
// Set a status variable to indicate that the agent has reached the end of the path.
// You can use this to trigger some special code if your game requires that.
result.hasReachedEndOfPath = true;
break;
}
} else {
break;
}
}
return result;
}
I setup my agents after spawning them like this:
IAgent agent = m_sim.AddAgent(spawnPoint);
agent.Radius = enemyRadius * 0.75f;
agent.Height = enemyRadius;
agent.Locked = false;
agent.AgentTimeHorizon = 2;
agent.ObstacleTimeHorizon = 2;
agent.MaxNeighbours = 10;
agent.Layer = RVOLayer.DefaultAgent;
agent.CollidesWith = (RVOLayer)(-1);
agent.Priority = 0.5f;
agent.FlowFollowingStrength = 0.1f;
agent.MovementPlane = Pathfinding.Util.SimpleMovementPlane.XZPlane;
agent.DebugDraw = false;
ecbEnd.AddComponent(entityInQueryIndex, agentEntity, new RvoAgentData { agentIndex = agent.AgentIndex });
Thanks for your help and advice! --luke