Search Results for

    Show / Hide Table of Contents

    Custom Locomotion

    This tutorial will show you how to create a custom locomotion for your agents that mimics tank movement. It is also expected that you have a minimum knowledge about ECS and Unity's Job System.

    Important

    Tutorials may have code that does not compile and may be outdated. Their purpose here is to set you in the right direction. If you have questions, do not hesitate to reach creator in the discord channel.

    Entity Component

    The first script that you will be creating is the tank's locomotion entity component. This component will contain information about the tank's movement speed, acceleration, angular speed, stopping distance, and auto braking. This script is essentially a copy of the AgentLocomotion component.

    /// <summary>
    /// Tanks locomotion that moves towards destination with arrival.
    /// </summary>
    public struct TankLocomotion : IComponentData
    {
        /// <summary>
        /// Maximum movement speed when moving to destination.
        /// </summary>
        public float Speed;
        /// <summary>
        /// The maximum acceleration of an agent as it follows a path, given in units / sec^2.
        /// </summary>
        public float Acceleration;
        /// <summary>
        /// Maximum turning speed in (rad/s) while following a path.
        /// </summary>
        public float AngularSpeed;
        /// <summary>
        /// Stop within this distance from the target position.
        /// </summary>
        public float StoppingDistance;
        /// <summary>
        /// Should the agent brake automatically to avoid overshooting the destination point?
        /// </summary>
        public bool AutoBreaking;
    }
    

    Authoring Component

    The next script that you will be creating is the authoring component. The authoring component is a regular MonoBehaviour that serves the purpose of keeping serialized information about the entity component. The Awake and Destroy methods here are used by the hybrid path to create and destroy the entity component. The TankLocomotionBaker is used by the pure ECS path to bake the subscene. Most of the code in this component is boilerplate to support both paths, and typically, all authoring components share these methods.

    [RequireComponent(typeof(AgentAuthoring))]
    [DisallowMultipleComponent]
    public class TankLocomotionAuthoring : MonoBehaviour
    {
        [SerializeField]
        float Speed = 3.5f;
    
        [SerializeField]
        float Acceleration = 8;
    
        [SerializeField]
        float AngularSpeed = 120;
    
        [SerializeField]
        float StoppingDistance = 0;
    
        [SerializeField]
        bool AutoBreaking = true;
    
        Entity m_Entity;
    
        /// <summary>
        /// Returns default component of <see cref="TankLocomotion"/>.
        /// </summary>
        public TankLocomotion DefaultLocomotion => new()
        {
            Speed = Speed,
            Acceleration = Acceleration,
            AngularSpeed = math.radians(AngularSpeed),
            StoppingDistance = StoppingDistance,
            AutoBreaking = AutoBreaking,
        };
    
        void Awake()
        {
            var world = World.DefaultGameObjectInjectionWorld;
            m_Entity = GetComponent<AgentAuthoring>().GetOrCreateEntity();
            world.EntityManager.AddComponentData(m_Entity, DefaultLocomotion);
        }
    
        void OnDestroy()
        {
            var world = World.DefaultGameObjectInjectionWorld;
            if (world != null)
                world.EntityManager.RemoveComponent<TankLocomotion>(m_Entity);
        }
    }
    
    internal class TankLocomotionBaker : Baker<TankLocomotionAuthoring>
    {
        public override void Bake(TankLocomotionAuthoring authoring)
        {
            AddComponent(GetEntity(TransformUsageFlags.Dynamic), authoring.DefaultLocomotion);
        }
    }
    

    Systems

    Finally, there will be two systems that will operate with our newly created TankLocomotion component.

    Seeking System

    The first system is the tank's seeking system. Its purpose is to set AgentBody.Force towards the destination. Usually, this system's AgentBody.Force will be overridden by navigation mesh systems, but in case it fails, it can fallback to this system. This is a copy of the AgentSeekingSystem with some minor changes.

    /// <summary>
    /// System that steers agent towards destination.
    /// </summary>
    [BurstCompile]
    [RequireMatchingQueriesForUpdate]
    [UpdateInGroup(typeof(AgentSeekingSystemGroup))]
    public partial struct TankSeekingSystem : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            new TankSteeringJob().ScheduleParallel();
        }
    
        [BurstCompile]
        partial struct TankSteeringJob : IJobEntity
        {
            public void Execute(ref AgentBody body, in TankLocomotion locomotion, in LocalTransform transform)
            {
                if (body.IsStopped)
                    return;
    
                float3 towards = body.Destination - transform.Position;
                float distance = math.length(towards);
                float3 desiredDirection = distance > math.EPSILON ? towards / distance : float3.zero;
                body.Force = desiredDirection;
                body.RemainingDistance = distance;
            }
        }
    }
    

    Locomotion System

    The second system is the tank's locomotion system. Its purpose is to move the agent towards the destination. This is a copy of the AgentLocomotionSystem with added logic that prevents agent movement if its facing direction and movement direction match within certain degrees.

    [BurstCompile]
    [RequireMatchingQueriesForUpdate]
    [UpdateInGroup(typeof(AgentLocomotionSystemGroup))]
    public partial struct TankLocomotionSystem : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            new TankLocomotionJob
            {
                DeltaTime = state.WorldUnmanaged.Time.DeltaTime
            }.ScheduleParallel();
        }
    
        [BurstCompile]
        partial struct TankLocomotionJob : IJobEntity
        {
            public float DeltaTime;
    
            public void Execute(ref LocalTransform transform, ref AgentBody body, in TankLocomotion locomotion, in AgentShape shape)
            {
                if (body.IsStopped)
                    return;
    
                // Check, if we reached the destination
                float remainingDistance = body.RemainingDistance;
                if (remainingDistance <= locomotion.StoppingDistance + 1e-3f)
                {
                    body.Velocity = 0;
                    body.IsStopped = true;
                    return;
                }
    
                float maxSpeed = locomotion.Speed;
    
                // Start breaking if close to destination
                if (locomotion.AutoBreaking)
                {
                    float breakDistance = shape.Radius * 2 + locomotion.StoppingDistance;
                    if (remainingDistance <= breakDistance)
                    {
                        maxSpeed = math.lerp(locomotion.Speed * 0.25f, locomotion.Speed, remainingDistance / breakDistance);
                    }
                }
    
                // Force force to be maximum of unit length, but can be less
                float forceLength = math.length(body.Force);
                if (forceLength > 1)
                    body.Force = body.Force / forceLength;
    
                // Update rotation
                if (shape.Type == ShapeType.Circle)
                {
                    float angle = math.atan2(body.Velocity.x, body.Velocity.y);
                    transform.Rotation = math.slerp(transform.Rotation, quaternion.RotateZ(-angle), DeltaTime * locomotion.AngularSpeed);
                }
                else if (shape.Type == ShapeType.Cylinder)
                {
                    float angle = math.atan2(body.Velocity.x, body.Velocity.z);
                    transform.Rotation = math.slerp(transform.Rotation, quaternion.RotateY(angle), DeltaTime * locomotion.AngularSpeed);
                }
    
                // Tank should only move, if facing direction and movement direction is within certain degrees
                float3 direction = math.normalizesafe(body.Velocity);
                float3 facing = math.mul(transform.Rotation, new float3(1, 0, 0));
                if (math.dot(direction, facing) > math.radians(10))
                {
                    maxSpeed = 0;
                }
    
                // Interpolate velocity
                body.Velocity = math.lerp(body.Velocity, body.Force * maxSpeed, DeltaTime * locomotion.Acceleration);
    
                float speed = math.length(body.Velocity);
    
                // Early out if steps is going to be very small
                if (speed < 1e-3f)
                    return;
    
                // Avoid over-stepping the destination
                if (speed * DeltaTime > remainingDistance)
                {
                    transform.Position += (body.Velocity / speed) * remainingDistance;
                    return;
                }
    
                // Update position
                transform.Position += DeltaTime * body.Velocity;
            }
        }
    }
    

    Agent Authoring

    The last thing you need to do is add your newly created steering component to the agent game object and set the Motion Type in the Agent component to Dynamic. This will ensure that the agent will use your custom steering component instead of the default one.

    Sample

    In the Scenarios sample, you can find the Tank Locomotion scene that showcases this custom locomotion behavior.

    In This Article
    Back to top Copyright © ProjectDawn.
    Generated by DocFX