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.