LightweightRVO in ECS

Hi,

I’d like to use the LightweightRVO with pure ECS. In my current implementation, I use AstarPath.StartPath to get a path, store it into a dynamic buffer on an entity and then move this entity by following all the points in the buffer using my own translation system with a bursted job.

How would i be able to add rvo into the whole mix now? Looking at the lightweight rvo script, it appears to be using an IAgent and simply a SetTarget method. IAgent is an interface that contains some managed objects such as a List, so I don’t think I can just convert it into a IComponentData.

Would it make sense to link an IAgent to every entity I have using a dictionary, do the movement on the IAgents like in the lightweightRVO script and then set the new position to the linked entities translation component? The problem with this approach is that I’ll have to get a ton of dynamic buffers onto the main thread every frame which is quite expensive. Or is there a better way?

1 Like

Hi

Try out the 4.3.9 beta. In the beta local avoidance uses burst so all the data exists in native arrays. The lightweight rvo script now uses burst as well.

See https://www.arongranberg.com/astar/documentation/dev_4_3_9_08deafd2/simulatorburst.html#simulationData
and https://www.arongranberg.com/astar/documentation/dev_4_3_9_08deafd2/rvosimulator.html#GetSimulator

var sim = RVOSimulator.active.GetSimulator() as SimulatorBurst;
sim.currentSimulationData.position[0] = new float3(0,0,0);

One issue might be that currently the order of the data might change if agents are removed.
You can experiment with it and let me know what you would need in order to achieve your use case.

Thank you, I’ll experiment with it whenever I get some more free time. I had already tried a beta a few weeks ago and although the astar samples worked fine, it crashed my own scene even though all I had was the astar object in the scene with a single grid graph and it wasn’t even linked to any of my systems back then.

Ok. Yeah I think there might have been some memory corruption bugs in the previous release.

Hey Aron, so I want to take a look again at the current state of the beta because I want to implement avoidance into my project now. I tried downloading it through UPM, and when I click install in the package manager, there’s a big red message saying

‘shasum check failed for…’ then gives 2 keys, an ‘expected’ one and an ‘actual’ one. None of these 2 keys correspond to the one given to me on the UPM Installation Guide page (the place where I enter my invoice number). Could you look into it? I couldn’t find any contact form to provide you with all the data and the invoice number, so if you need those, let me know how I may contact you in private.

Hi

Which version of Unity are you using?

I used an empty project in 2019.3.8f1

Hi

That’s odd… It works out of the box for me with Unity 2019.3.5. Do you think you could contact me in a PM with your invoice number and other details?

As I was typing my message, I went to import the package again to show you a picture of the error. However now it worked! Not sure what changed, but thanks!

I’m interested in the lightweight rvo scene and I’m liking what I see at 30k units. Time to adjust my move jobs to work with the rvo jobs. There’s just one slight caveat. Is there anything you could do to get rid of the spikes caused by RVOSimulator.Update() ?

Hey, it looks like you’ve removed ‘hierarchicalGraph’ from AstarPath in the latest beta. What should I be using now?

https://arongranberg.com/astar/documentation/4_2_5_05bb896f/hierarchicalgraph.html

I was using it to access the areas and children of the graph like so:

void Start() {
    InvokeRepeating("SyncMyNodesWithAstar",2,2);
}
void SyncMyNodesWithAstar() {
    UpdateAreaPeriodicallyJob updateAreaPeriodicallyJob = new UpdateAreaPeriodicallyJob();
    updateAreaPeriodicallyJob.Schedule(AstarPath.active.hierarchicalGraph.areas.Length, 16);

}
struct UpdateAreaPeriodicallyJob : IJobParallelFor {
    public void Execute(int index) {
        
        int area = AstarPath.active.hierarchicalGraph.areas[index];
        List<GraphNode> nodes = AstarPath.active.hierarchicalGraph.children[index];
        for (int i = 0; i < nodes.Count; ++i) {
            GraphNode graphNode = nodes[i];
            Vector3 pos = (Vector3)graphNode.position;
            int indexx = PathfindingManager.GetIndex(new int2((int)pos.x, (int)pos.y));

            if (RequiredExtensions.nodes.Length > 0 && indexx < RequiredExtensions.nodes.Length) {

                Node node = RequiredExtensions.nodes[indexx];
                if (graphNode.GraphIndex == 0) {
                    node.friendlyArea = (uint)area;
                } else {
                    node.enemyArea = (uint)area;
                }
                RequiredExtensions.nodes[indexx] = node;
            }
        }
    }
}

Hi

It is not removed: https://www.arongranberg.com/astar/documentation/dev_4_3_19_434810ae/hierarchicalgraph.html

I’m not sure why you needed the hierarchical graph though? It doesn’t seem like you are using any information in it really?

You can access the area using something like

foreach (var graph in AstarPath.active.data.graphs) {
    if (graph != null) graph.GetNodes(node => {
         var area = node.Area;
    }
}

Oh my mistake, I forgot I had modified your script to mark that field (and others) public. Just took a look inside the package and it is indeed there, marked as internal. Sadly Visual Studio Intelisense doesn’t open source files that are in the packages folder for some reason, which is why I assumed it was removed!

1 Like

Hey Aron, I’m not sure I understand what you’re proposing we do here. Like radu, I’m using Astart.StartPath to get a path, and then storing the path in a buffer attached to the entity that I want to move. There is an ECS system that simply moves the entity along the waypoints (pos.Value += velocity * deltaTime) until it reaches then end. It also rotates the entities so they face in the direction of the next waypoint. I’m not using any of the AI class like AIPath, AILerp, etc because they’re all based on game-objects instead of entities. I’m using a grid graph. I have physics turned on and physics bodies on each object, which keeps them from occupying the same space.

I’d like to try RVO… but I’m having trouble seeing the big picture of how I get from here to the point where my entities are following the RVO algorithm…

  1. Add an empty game object to the scene and attach an RVOSimulator to it?
  2. Inherit IAgent and implement all the required functions using various ECS queries (since the data isn’t organized in an OOP fashion).
  3. Whenever I spawn a new agent, call GetSimulator()->AddAgent()
  4. I think somehow the RVOController needs to come into play because it’s what has the SetTarget() function, but it’s a monobehaviour so I don’t know how I can use it within ECS unless maybe I tether it to each agent entity as a hybrid? Or rewrite the RVOController class?

Sorry, I’m a bit slow at getting this concept and honestly I’m pretty lost as to what to do… I’m not even sure if there is a way to do it without rewriting a bunch of the classes to be ECS compatible.

@Ph0t0n

I haven’t investigated this fully, but what you would have to do I think is to the SimulatorBurst.simulationData field (https://www.arongranberg.com/astar/documentation/dev_4_3_40_a3cd82aa/simulatorburst.html#simulationData) which exposes the internal simulation data with as bunch of NativeArrays.

Unfortunately, that doesn’t help very much. I think there needs to be a lot more involved than just that (such as rewriting an RVOController and other scripts). Thanks anyway.

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

Amazing that you got it working! :smiley:

The local avoidance system doesn’t change the y coordinate at all. It just returns what you send in. So if you want them to follow your terrain, sampling that is the way to go.

Either recalculate their paths once they get far enough away, or use a more robust path following method (e.g. using the RichPath class, which can tolerate disturbances better than just following waypoints directly).

Clamp them to the navmesh using e.g. AstarPath.active.GetNearest. See AIPath.ClampToNavmesh for an example.

Thanks Aron!

As for Problem 1 - I think I see what you are saying - I’m “sending in” a waypoint along the path, which has a y value that is on the terrain, yet the value that comes out of simBurst.outputData.targetPoint[agentIndex] is often a slightly different point because other agents might be in its way or whatever, and that slightly different point uses the same y value as the original waypoint that I “sent in”. So I need to sample terrain to get the actual height of the output target point that comes out.

For Problem 2 - I’ll take a look at the RichPath class. Unfortunately, since it’s all built around OOP and game-objects, I can’t just use it as-is in my burst-enabled jobs and ECS components, which can’t use managed code or game-objects. At first glance, RickPath looks pretty complicated and intertwined with a bunch of other stuff, so hopefully I can figure out which pieces I need to extract and replicate.

For Problem 3 - I’ll give it a try. Hopefully I can rewrite some of the GetNearest() function to make it DOTS compatible.