Best approach for steering behaviors implementation in dots

Hi guys,

For my project, I’m starting to work on an open source steering behavior library fundamentally similar to what Ricardo J. Mendez did with UnitySteer (GitHub - ricardojmendez/UnitySteer: Steering, obstacle avoidance and path following behaviors for the Unity Game Engine) but with dots approach.

In this case, his approach was to have one Monobehaviour for each Steering case (i.e.: SteerForPoint, SteerForEvade, SteerForAlignment…) as well as (to sumarize as his lib actually allows much more options) another Monobehaviour (AutonomousVehicle) that would take care of an a weighted sum of all Steering forces computed to define a velocity vector and finally apply this velocity vector to Unity’s Transform position and rotation (or Rigidbody if using physics).
The system basically works by adding and configuring one AutonomousVehicle on a gameobject and as many as you want Steering Monobehaviours to enable/disable steering behaviours on it.

I like the idea of adding/removing Steering features so I though I would have the following approach:

  • Create a base ISteeringComponent inheriting from IComponentData with no fields whatsoever

  • Create all my Steering behaviour data components inheriting from ISteeringComponent

  • Create a unique ComputeSteeringForceSystem that would compute implement the computation logic

  • Create a static RegisterSteeringBehaviour method that would accept a ISteeringComponent type entry as well as a pointer/delegate pointing to the actual static method used to perform the SteeringForce computation that would be later used in OnUpdate to call the right method for each ISteeringComponent type (allowing other/new cases to be added to the system later on)

  • Create as many static methods to Calculate Steering as you’ll have Steering Behaviours

  • On the ComputeSteeringForceSystem OnUpdate method retrieve all ISteeringComponent on a given entity and call their relative computation method based on the previously registered relations

  • Create a ApplySteeringForceSystem that would then perform the actual updates on a Translation, Rotation and/or PhysicsVelocity component values.

Now, my question, this seems like the neater approach but I lack some knowledge on how I could, eventually, reach this implementation.

I actually don’t know how one can create an Entities.ForEach loop so I can retrieve all ISteeringComponent “instances” from the current Entity.
Moreover, I wonder if that would actually be the best approach on dots or should I actually have to create for each SteeringBehaviour struct (IComponentData) a given System that would perform the specific computation and have another AddSteeringForceSystem that would take care of adding these to a central SteeringAgent holding a simple float3 for the computed velocity?

Anyone with some piece of advice would be very welcome :wink:

Thanks by advance

Firstly, I’ve implemented most of that library in DOTS as I needed some basic logic to follow paths to test a larger navigation library I’m working on (which I’m not ready to release yet). For example, the “steer” looks like this

namespace BovineLabs.Movement.Behaviours
{
    using BovineLabs.Movement.Data;
    using JetBrains.Annotations;
    using Unity.Mathematics;

    public struct Steering
    {
        private readonly RigidObject rigid;
        private readonly MoveAgent agent;

        public Steering(RigidObject rigidObject, MoveAgent agent)
        {
            this.rigid = rigidObject;
            this.agent = agent;
        }

        /// <summary>
        /// A seek steering behavior. Will return the steering for the current game object to seek a given position
        /// </summary>
        [Pure]
        public float3 Seek(float3 targetPosition, float maxSeekAccel)
        {
            // Get the direction
            var acceleration = this.agent.ConvertVector(targetPosition - this.rigid.Position);

            acceleration = math.normalizesafe(acceleration);

            // Accelerate to the target
            acceleration *= maxSeekAccel;

            return acceleration;
        }

        [Pure]
        public float3 Seek(float3 targetPosition)
        {
            return this.Seek(targetPosition, this.agent.MaxAcceleration);
        }

        /// <summary>
        /// Returns the steering for a character so it arrives at the target
        /// </summary>
        [Pure]
        public float3 Arrive(float3 targetPosition)
        {
            targetPosition = this.agent.ConvertVector(targetPosition);

            // Get the right direction for the linear acceleration
            var targetVelocity = targetPosition - this.rigid.Position;

            // Get the distance to the target
            float dist = math.length(targetVelocity);

            // If we are within the stopping radius then stop
            if (dist < this.agent.TargetRadius)
            {
                return float3.zero;
            }

            // Calculate the target speed, full speed at slowRadius distance and 0 speed at 0 distance
            float targetSpeed;
            if (dist > this.agent.SlowRadius)
            {
                targetSpeed = this.agent.MaxVelocity;
            }
            else
            {
                targetSpeed = this.agent.MaxVelocity * (dist / this.agent.SlowRadius);
            }

            // Give targetVelocity the correct speed
            targetVelocity = math.normalizesafe(targetVelocity);
            targetVelocity *= targetSpeed;

            // Calculate the linear acceleration we want
            var acceleration = targetVelocity - this.rigid.Linear;

            // Rather than accelerate the character to the correct speed in 1 second,
            // accelerate so we reach the desired speed in timeToTarget seconds
            // (if we were to actually accelerate for the full timeToTarget seconds).

            if (this.agent.TimeToTarget != 0)
            {
                acceleration *= 1 / this.agent.TimeToTarget;
            }

            // Make sure we are accelerating at max acceleration
            if (math.length(acceleration) > this.agent.MaxAcceleration)
            {
                acceleration = math.normalizesafe(acceleration);
                acceleration *= this.agent.MaxAcceleration;
            }

            return acceleration;
        }

        [Pure]
        public float3 Interpose(RigidObject target1, RigidObject target2)
        {
            var midPoint = (target1.Position + target2.Position) / 2;

            var timeToReachMidPoint = math.distance(midPoint, this.rigid.Position) / this.agent.MaxVelocity;

            var futureTarget1Pos = target1.Position + (target1.Linear * timeToReachMidPoint);
            var futureTarget2Pos = target2.Position + (target2.Linear * timeToReachMidPoint);

            midPoint = (futureTarget1Pos + futureTarget2Pos) / 2;

            return this.Arrive(midPoint);
        }
    }
}

My component data looks something like this

    [GenerateAuthoringComponent]
    public struct MoveAgent : IComponentData
    {
        // General
        public float MaxVelocity;
        public float MaxAcceleration;
        public float MaxAccelerationAvoidance;
        public float TurnSpeed;

        public float Radius;

        [Tooltip("How much space can be between two characters before they are considered colliding")]
        public float DistanceBetween;


        public bool CanFly;
        public int NumSamplesForSmoothing;

        // Arrive

        /// <summary> The radius from the target that means we are close enough and have arrived. </summary>
        public float TargetRadius;

        /// <summary> The radius from the target where we start to slow down. </summary>
        public float SlowRadius;

        /// <summary> The time in which we want to achieve the targetSpeed. </summary>
        public float TimeToTarget;
    }

    public struct AgentFollowPath : IComponentData
    {
        public float StopRadius;
        public float PathOffset;
    }

Secondly, if you actually want a really powerful avoidance library, while not free, the very popular A* Pathfinding library A* Pathfinding Project actually has a burst/job implementation of RVO that you can use in the beta package.

The default implementation works fine, but I ended up rewriting it for our project to work much more naturally with DOTS and it’s great.

1 Like

Thanks for sharing your approach but in your case, I don’t see exactly when you actually compute and apply the velocity, do you have one system for it?

Furthermore, in your approach, I believe you are trying to answer a unique usecase where you only want to work with a set of steering behaviors (for what I see, a simple Seek/Arrive and an Interpose).
Any idea how you would make so the overall system is less tied to your specific case and can be expanded?

I have all the behaviors implemented

Follow path for example

namespace BovineLabs.Movement.Behaviours
{
    using BovineLabs.Movement.Data;
    using Unity.Mathematics;

    public struct FollowPath
    {
        private readonly RigidObject rigid;
        private readonly MoveAgent agent;
        private readonly AgentFollowPath followPath;
        private readonly Steering steering;

        public FollowPath(RigidObject rigidObject, MoveAgent agent, AgentFollowPath followPath)
        {
            this.rigid = rigidObject;
            this.agent = agent;
            this.followPath = followPath;

            this.steering = new Steering(rigidObject, agent);
        }

        public float3 GetSteering(LinePath path, bool pathLoop = false)
        {
            return this.GetSteering(path, pathLoop, out _);
        }

        public float3 GetSteering(LinePath path, bool pathLoop, out float3 targetPosition)
        {
            // If the path has only one node then just go to that position.
            if (path.Length == 1)
            {
                targetPosition = path.Nodes[0];
            }

            // Else find the closest spot on the path to the character and go to that instead. //
            else
            {
                // Get the param for the closest position point on the path given the character's position
                float param = path.GetParam(this.rigid.Position, this.agent);

                if (!pathLoop)
                {
                    // If we are close enough to the final destination then stop moving
                    if (this.IsAtEndOfPath(path, param, out var finalDestination))
                    {
                        targetPosition = finalDestination;
                        return float3.zero;
                    }
                }

                // Move down the path
                param += this.followPath.PathOffset;

                // Set the target position
                targetPosition = path.GetPosition(param, pathLoop);
            }

            // TODO
            return this.steering.Arrive(targetPosition);
        }

        private bool IsAtEndOfPath(LinePath path, float param, out float3 finalDestination)
        {
            bool result;

            // Find the final destination of the character on this path
            finalDestination = path.Nodes[path.Length - 1];
            finalDestination = this.agent.ConvertVector(finalDestination);

            // If the param is closest to the last segment then check if we are at the final destination
            if (param >= path.Distances[path.Length - 2])
            {
                result = math.distance(this.rigid.Position, finalDestination) < this.followPath.StopRadius;
            }

            // Else we are not at the end of the path
            else
            {
                result = false;
            }

            return result;
        }
    }
}

Notice it uses the above Steering struct.

The velocity is applied in the controller

namespace BovineLabs.Movement.Behaviours
{
    using BovineLabs.Core.Extensions;
    using BovineLabs.Movement.Data;
    using Unity.Mathematics;

    public struct Controller
    {
        private readonly MoveAgent agent;
        private RigidObject rigidObject;

        /// <summary>
        /// Initializes a new instance of the <see cref="Controller"/> struct.
        /// </summary>
        /// <param name="rigidObject"></param>
        /// <param name="agent"></param>
        public Controller(RigidObject rigidObject, MoveAgent agent)
        {
            this.agent = agent;
            this.rigidObject = rigidObject;
        }

        public float3 LinearVelocity => this.rigidObject.Linear;

        /// <summary> Applies an acceleration over a delta time. </summary>
        /// <param name="linearAcceleration"> The acceleration to apply. </param>
        /// <param name="dt"> The delta time. </param>
        public void Steer(float3 linearAcceleration, float dt)
        {
            this.rigidObject.Linear += linearAcceleration * dt;

            if (math.length(this.rigidObject.Linear) > this.agent.MaxVelocity)
            {
                this.rigidObject.Linear = math.normalizesafe(this.rigidObject.Linear) * this.agent.MaxVelocity;
            }
        }
    }
}

And it all ties together in jobs.

For example, the follow path job.

[BurstCompile]
        private struct FollowPathJob : IJobChunk
        {
            public ArchetypeChunkComponentType<PhysicsVelocity> PhysicsVelocity;

            [ReadOnly]
            public ArchetypeChunkComponentType<Translation> Translations;

            [ReadOnly]
            public ArchetypeChunkComponentType<Rotation> Rotations;

            [ReadOnly]
            public ArchetypeChunkComponentType<MoveAgent> MoveAgents;

            [ReadOnly]
            public ArchetypeChunkBufferType<Path> Paths;

            [ReadOnly]
            public ArchetypeChunkComponentType<AgentFollowPath> AgentFollowPaths;

            public float DeltaTime;

            public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
            {
                var translations = chunk.GetNativeArray(this.Translations);
                var rotations = chunk.GetNativeArray(this.Rotations);
                var physicsVelocities = chunk.GetNativeArray(this.PhysicsVelocity);
                var moveAgents = chunk.GetNativeArray(this.MoveAgents);
                var paths = chunk.GetBufferAccessor(this.Paths);

                var agentFollowPaths = chunk.GetNativeArray(this.AgentFollowPaths);

                var hasFollowPaths = chunk.Has(this.AgentFollowPaths);

                for (var i = 0; i < chunk.Count; i++)
                {
                    var linear = physicsVelocities[i].Linear;
                    var angular = physicsVelocities[i].Angular;
                    var agent = moveAgents[i];
                    var pathBuffer = paths[i];

                    var rigid = new RigidObject
                    {
                        Position = translations[i].Value,
                        Rotation = rotations[i].Value,
                        Linear = linear,
                        Angular = angular,
                    };

                    var agentFollow = hasFollowPaths
                        ? agentFollowPaths[i]
                        : new AgentFollowPath { StopRadius = 0.005f, PathOffset = 0.71f, };

                    var followPath = new FollowPath(rigid, agent, agentFollow);
                    var path = new LinePath(pathBuffer);

                    var steering = followPath.GetSteering(path);

                    var controller = new Controller(rigid, agent);
                    controller.Steer(steering, this.DeltaTime);

                    var velocity = new PhysicsVelocity { Linear = controller.LinearVelocity, Angular = angular, };

                    physicsVelocities[i] = velocity;
                }
            }
        }

Breaking up the logic in structs is like the original library breaking up the logic in different monobehaviours. Let’s me re-use the same logic for different behaviours. I have similar jobs for fear, evade, wander etc

As I said, I wrote this more as a way to test another library than an intended implementation so there are a few things I’d probably improve if I was to work on it more, you just happened to be looking at doing something like this so I thought it could help.

1 Like

So you actually decided to take the one System per Steering Behaviour you wanted to implement as far as I understand.
Would you see any reason not to try to have one single job as the actual logic behind steering behaviours is to return a SteeringForce expressed as a float3 that could then be “averaged” or “weighted averaged” in one pass per entity?

let’s take an example, most of your entities using steering behaviours (on a general project use case) will actually use the same behaviours: a Shark with a SteerForWander, ObstacleAvoidance behaviours and a SimpleFish with a SteerForWander, ObstacleAvoidance and a SteerForFear behaviours.
These two entities (or EntityArchetypes) would use the exact same way to distribute the work on these behaviours, it’s just their combination that changes…
Then the actual changes on Translation or Rotation components would be on another System.

Am I thinking this wrong?

And if not, I still cannot figure out how I can in one Entities.ForEach loop query for specific components that might or not be available in the current entity…

You really should use IJobChunk not Entities.ForEach for that.