Jittery/shaky and slow movement (ECS only, GridGraph, 2D RTS)

  • A* version: 5.2.4
  • Unity version: 6000.0.26f1

Hello,

I’m developing a 2.5D RTS game (the graphics are in 3D, all the logic is in 2D).

  • For maximum performance I’m using the ECS for all the units/buildings
  • Units barely move, even though the speed is set to a reasonable amount (5 meters per second)
  • Units jitter/shake as they move. The jitter is in the direction of the movement. Looks like the units move a bit forward on some frames, then move backward on other frames
  • I can see in the entity inspector that the velocity in MovementStatistics goes from about 5m/s to -14m/s several times per second, thus causing the jittery/slow movement
  • I have confirmed that nothing except A* touches the units’ transform or the A* ECS components
  • delta in JobMoveAgent.MoveAgent is always positive, as it should be, so the problem is not there
  • At the same time, (transform.Position - movementStatistics.lastPosition) in the same function returns negative values several times per second. This should not happen, as the destination point of my test unit is set to have greater per-component values than the origin position
  • I’m using a flat procedurally generated GridGraph, with no gravity
  • Rotation seems? to be handled properly
  • Local avoidance is disabled
  • The path is correctly calculated
  • Enabling/disabling multi threading doesn’t fix the issue
  • No colliders are present in the scene
  • I am on Linux
  • I have tried way too many things, and still wasn’t able to figure out why it doesn’t work correctly
  • Let me know if you need the project files. I’m okay with sending them over privately
  • You can find the video of the issue here: Watch astar issue | Streamable

Thanks.

Here are the ECS components on my test unit:


Here is the ECS nav agent authoring mono behavior:

// ManagedStateInfoComp.cs
public class ManagedStateInfoComp : IComponentData {
	public RVOAgent rvoSettings;
	public PathRequestSettings pathfindingSettings;
	public bool enableLocalAvoidance;
	public bool enableGravity;
}
// NavAgentAuthoring.cs
using Pathfinding;
using Pathfinding.ECS;
using Pathfinding.ECS.RVO;
using Pathfinding.PID;
using Pathfinding.Util;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using AutoRepathPolicy = Pathfinding.ECS.AutoRepathPolicy;

namespace TheGame.ECS {
  public class NavAgentAuthoring : MonoBehaviour {
    [SerializeField] private AgentCylinderShape shape = new() {
      height = 2,
      radius = 0.5f,
    };

    [SerializeField] private MovementSettings movement = new() {
      follower = new PIDMovement {
        rotationSpeed = 600,
        speed = 5,
        maxRotationSpeed = 720,
        maxOnSpotRotationSpeed = 720,
        slowdownTime = 0.5f,
        desiredWallDistance = 0.5f,
        allowRotatingOnSpot = true,
        leadInRadiusWhenApproachingDestination = 1f,
      },
      stopDistance = 0.2f,
      rotationSmoothing = 0f,
      groundMask = -1,
      isStopped = false,
    };

    [SerializeField] private ManagedState managedState = new() {
      rvoSettings = RVOAgent.Default,
      enableLocalAvoidance = false,
      pathfindingSettings = PathRequestSettings.Default,
    };
    
    public class Baker : Baker<NavAgentAuthoring> {
      public override void Bake(NavAgentAuthoring authoring) {
        var entity = GetEntity(TransformUsageFlags.Dynamic);
        
        var pos = authoring.transform.position;

        AddComponent(entity, new MovementState {
          followerState = new PIDMovement.PersistentState {
            maxDesiredWallDistance = 0,
          },
          nextCorner = pos,
          endOfPath = pos,
          closestOnNavmesh = new float3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity),
          hierarchicalNodeIndex = -1,
          remainingDistanceToEndOfPart = float.PositiveInfinity,
          reachedDestination = false,
          reachedEndOfPath = false,
          reachedDestinationAndOrientation = false,
          reachedEndOfPathAndOrientation = false,
          pathTracerVersion = 0,
        });
        
        AddComponent(entity, new AgentMovementPlane {
          value = new NativeMovementPlane(SimpleMovementPlane.XZPlane),
        });
        
        AddComponent(entity, new DestinationPoint {
          destination = new float3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity),
        });
        
        AddComponent(entity, authoring.movement);

        AddComponentObject(entity, new ManagedStateInfoComp {
          pathfindingSettings = authoring.managedState.pathfindingSettings,
          enableGravity = authoring.managedState.enableGravity,
          enableLocalAvoidance = authoring.managedState.enableLocalAvoidance,
        });

        AddComponent(entity, new MovementStatistics {
          estimatedVelocity = float3.zero,
          lastPosition = pos,
        });
        
        AddComponent(entity, authoring.shape);
        
        AddComponent(entity, new SimulateMovement());
        AddComponent(entity, new SimulateMovementControl());
        AddComponent(entity, new SimulateMovementFinalize());
        AddComponent(entity, new SearchState());
        AddComponent(entity, new ResolvedMovement());
        AddComponent(entity, new MovementControl());
        AddSharedComponent(entity, new AgentMovementPlaneSource {
          value = MovementPlaneSource.Graph
        });
        AddComponent(entity, new AutoRepathPolicy {
          mode = Pathfinding.AutoRepathPolicy.Mode.Dynamic,
          period = 1
        });
      }
    }
  }
}
// NavAgentInitSystem.cs
using Pathfinding;
using Pathfinding.ECS;
using Unity.Collections;
using Unity.Entities;

namespace TheGame.ECS {
  /*
   * A system to initialize ManagedState for entities with ManagedStateInfoComp
   * ManagedStateInfoComp is removed once the ManagedState is initialized
   *
   * ManagedStated is used by the A* pathfinding project
   */
  [UpdateInGroup(typeof(InitializationSystemGroup))]
  public partial struct NavAgentInitSystem : ISystem {
    public void OnCreate(ref SystemState state) {
      state.RequireForUpdate<EndInitializationEntityCommandBufferSystem.Singleton>();
    }

    public void OnUpdate(ref SystemState state) {
      if (AstarPath.active == null) {
        return;
      }
      
      var ecb = SystemAPI.GetSingleton<EndInitializationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);

      foreach (var (s, entity) in SystemAPI.Query<ManagedStateInfoComp>().WithEntityAccess()) {
        var managed = new ManagedState {
          pathTracer = new PathTracer(Allocator.Persistent),
          rvoSettings = s.rvoSettings,
          pathfindingSettings = s.pathfindingSettings,
          enableLocalAvoidance = s.enableLocalAvoidance,
          enableGravity = s.enableGravity,
        };

        ecb.RemoveComponent<ManagedStateInfoComp>(entity);
        ecb.AddComponent(entity, managed);
      }
    }
  }
}

Hi

Can you replicate this using the FollowerEntity component, or does it only happen with your own custom code?

Navmesh settings:

FollowerEntity works just fine. The problem is I need in to be 100% in ECS as I’ll have as much as 2-3k units at the same time

Try to create one FollowerEntity, and check for any differences in the entity components. Let me know if you find anything significant.

One thing I can note is that your agent doesn’t seem to have the SimulateMovementRepair component. Meaning the agent will never repair its path.

Finally, managed to fix it! The unit was missing the SimulateMovementRepair ECS component, It wasn’t present in an earlier version of A* I was using on a different project which I used as a base for this project.

Thank you so much for your quick help!

But I have to say, having a section for setting up ECS-only pathfinding with A* in the docs would be tremendously helpful. I basically wasted 6 hours of my life on this.

I’m planning to include an official baking component in the future.

There’s also the FollowerEntity.CreateEntity static method. Though it’s not quite the same as baking.

Nice. Anyway, thanks for your help.