Prediction for AI entities

Is there any way to use client side prediction for AIs running on server?
My AI system is already modifying a IInputComponentData and it acts like a remote player (but modifyng the Input Data from server).

Is that the right way to “predict” AI entities? looks like it is not even working

When you ghost field the IInputComponentData you’re ghost fielding both the backing command buffer & component. Are you updating that buffer on server side as well to contain the input from server?

Yes, i`m updating it from the server side (the only one who is running the AI code)

Can i not ghost the background command buffer somehow? or what do i need to ghost?

Can you post the code?

You are implicitly ghosting the backing buffer by ghosting the IInputComponentData. It’s a bit unecessary to sync the buffer since you only use latest input anyway, I’ve noted this to the devs before. So best is to split it for now into the buffer & manually fetch & store data from buffer to current ticks component. This means you can skip the buffer on AI anyway until you need the behavior.

        [BurstCompile]
        [WithAll(typeof(UtilityBehaviourState))]
        public partial struct UtilityBehaviourJob : IJobEntity
        {
            [ReadOnly]
            public CollisionWorld CollisionWorld;

            public float DeltaTime;

            private unsafe void AvoidObstacles(Entity self, RefRW<UtilityBehaviourState> state, RefRW<GameplayCharacterInputs> inputs, RefRO<LocalTransform> transform, RefRO<PhysicsCollider> physicsCollider)
            {
                if (state.ValueRO.Target.HasValue)
                {
                    float3 directionToTarget = math.normalizesafe(state.ValueRO.Target.Value - transform.ValueRO.Position);

                    CapsuleCollider* capsuleCollider = (CapsuleCollider*)state.ValueRO.CollisionAvoidanceCollider.GetUnsafePtr();

                    float maxDistance = 0.5f;

                    CollisionAvoidanceCollector collector = new CollisionAvoidanceCollector(self, transform.ValueRO.Position, directionToTarget, state, maxDistance, CollisionWorld);

                    ColliderDistanceInput distanceInput = new ColliderDistanceInput()
                    {
                        Collider = (Collider*)(capsuleCollider),
                        MaxDistance = maxDistance,
                        Scale = 1.0f,
                        Transform = new RigidTransform() { pos = transform.ValueRO.Position }
                    };

                    CollisionWorld.CalculateDistance(distanceInput, ref collector);
                }
            }

            private unsafe void AvoidStacking(Entity self, RefRW<UtilityBehaviourState> state, RefRW<GameplayCharacterInputs> inputs, RefRO<LocalTransform> transform, RefRO<PhysicsCollider> physicsCollider)
            {
                CapsuleCollider* capsuleCollider = (CapsuleCollider*)state.ValueRO.StackingAvoidanceCollider.GetUnsafePtr();

                float distanceAvoidanceRadius = capsuleCollider->Radius;

                StackingAvoidanceCollector collector = new StackingAvoidanceCollector(self, transform.ValueRO.Position, state, distanceAvoidanceRadius, CollisionWorld);

                CollisionWorld.OverlapSphereCustom(transform.ValueRO.Position, distanceAvoidanceRadius + 1.0f, ref collector, capsuleCollider->GetCollisionFilter());
            }

            private unsafe void Seek(Entity self, RefRW<UtilityBehaviourState> state, RefRW<GameplayCharacterInputs> control, RefRO<LocalTransform> transform, RefRO<PhysicsCollider> physicsCollider)
            {
                if (state.ValueRO.Target.HasValue)
                {
                    float3 directionToTarget = state.ValueRO.Target.Value - transform.ValueRO.Position;

                    float distanceToTarget = math.length(directionToTarget);

                    if (distanceToTarget > state.ValueRO.ApproachRadius)
                    {
                        directionToTarget /= distanceToTarget;

                        distanceToTarget = math.min((distanceToTarget - state.ValueRO.ApproachRadius), state.ValueRO.ApproachRadius);

                        distanceToTarget = distanceToTarget > 1.0f ? state.ValueRO.MaxGroundSpeed : (distanceToTarget * state.ValueRO.MaxGroundSpeed); 

                        for (int it = 0; it < 8; ++it)
                        {
                            state.ValueRW.Interest[it] = math.max(state.ValueRW.Interest[it], distanceToTarget * math.dot(directionToTarget, Utils.ComputeDirection(it)));
                        }
                    }
                }
            }


            [BurstCompile]
            public unsafe void Execute(Entity self, RefRW<UtilityBehaviourState> state, RefRW<GameplayCharacterInputs> inputs, RefRO<LocalTransform> localToWorld, RefRO<PhysicsCollider> physicsCollider)
            {
                for (int it = 0; it < 8; ++it)
                {
                    state.ValueRW.Interest[it] = 0.0f;
                    state.ValueRW.Danger[it] = 0.0f;
                }

                AvoidObstacles(self, state, inputs, localToWorld, physicsCollider);
                AvoidStacking(self, state, inputs, localToWorld, physicsCollider);
                Seek(self, state, inputs, localToWorld, physicsCollider);

                float3 average = 0.0f;

                float3 averageInterest = 0.0f;
                float3 averageDanger = 0.0f;

                for (int it = 0; it < 8; ++it)
                {
                    float3 dir = Utils.ComputeDirection(it);

                    averageInterest += dir * state.ValueRO.Interest[it];
                    averageDanger += dir * (state.ValueRO.Danger[it] * state.ValueRO.MaxGroundSpeed);
                }

                averageInterest = Utils.ClampToMaxLength(averageInterest, state.ValueRO.MaxGroundSpeed);
                averageDanger = Utils.ClampToMaxLength(averageDanger, state.ValueRO.MaxGroundSpeed);

                average = averageInterest - averageDanger;

                average = math.lengthsq(average) >= 0.25f ? (average / state.ValueRO.MaxGroundSpeed) : float3.zero;

                inputs.ValueRW.MoveInput = new float2(average.x, average.z);

            }
        }

  [GhostComponent(PrefabType = GhostPrefabType.Server)]
  public unsafe struct UtilityBehaviourState : IComponentData
  {
      public BlobAssetReference<Collider> CollisionAvoidanceCollider;
      public BlobAssetReference<Collider> StackingAvoidanceCollider;
      public BlobAssetReference<Collider> TargetCollider;

      public fixed float Interest[8];
      public fixed float Danger[8];

      public float MaxGroundSpeed;

      public float ApproachRadius;
      public float AccumulatedDeltaTime;
      public float3? Target;
  }

From the posted code you are only updating the IInputComponentData & not the buffer as well explicitly.

Do i need to update only the latest entry on buffer or what?

If you want to use the buffer, you can check the CopyInputToCommandBufferSystem in NetCode as reference. It also deals with incrementing events for InputEvents which you are using.

1 Like