Let's move

Hi there! If you haven’t seen the previous post (basics and mass object spawning), then here it is.

The player

So we have a bunch of objects in our scene. They aren’t do much right now, but I’ll call them enemies. But whose enemy? We need a player which we can control. It can be a capsule for example, attached a ConvertToEntity and a Proxy script to it. And here comes the code:

ComponentData


// Assets/Scripts/Player/PlayerData.cs

using Unity.Entities;
using Unity.Transforms;

public struct PlayerData : IComponentData
{
  public Translation Position;
}

We have a new stuff here, the Translation. It’s a struct in the Unity.Transforms namespace and holds a float3 value, which is in the Unity.Mathematics namespace. We’ll use this new math whenever we can, because especially created for the Unity’s Burst compiler (link0link1link2), which is especially created for ECS.

We use this Transform to save our player’s actual position, so our enemies will know where to go.

PlayerProxy


// Assets/Scripts/Player/PlayerProxy.cs

using Unity.Entities;
using UnityEngine;

public class PlayerProxy : MonoBehaviour, IConvertGameObjectToEntity
{
  public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
  {
    dstManager.AddComponentData(entity, new PlayerData());
  }
}

The same as SpawnerProxy. We convert our Player to Entity and add the ComponentData to it.

System


// Assets/Scripts/Player/PlayerMovementSystem.cs

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class PlayerMovementSystem : ComponentSystem
{
  private EntityQuery _query;

  protected override void OnCreate()
  {
    _query = GetEntityQuery(ComponentType.ReadWrite<PlayerData>(), ComponentType.ReadOnly<Translation>());
  }

  protected override void OnUpdate()
  {
    Entities.With(_query).ForEach((Entity entity, ref PlayerData playerData, ref Translation position) =>
    {
      float x = Input.GetAxis("Horizontal");
      float y = Input.GetAxis("Vertical");

      position.Value = new float3(position.Value.x + x, .0f, position.Value.z + y);
      playerData.Position.Value = position.Value;
    });
  }
}

In the previous post we used JobComponentSystem, but here we can’t, because we need to access Unity’s static Input class. As it is a reference type, we can’t use it in a Job, so we stay in the OnUpdate method. In a ComponentSystem we can access to the Entities via the Entities EntityQueryBuilder. We don’t need all of them, only our Player, so we must set up an EntityQuery for this. You can see some similarity between this and the previous IJobForEachWithEntity. We build up the EntityQuery with the GetEntityQuery (as you can see, we specify the access constraints through the ComponentType instead of attributes), and using it we can iterate over all resulted entity (in this case we also will have only one entity, our Player).

After play, we can inspect our entities in the Entity Debugger. There we can see what ComponentDatas an entity have. This is my Player entity created with the ConvertToEntity script. Right now we only ask for the PlayerData and the Translation. If there would be another entity with a different ComponentData set, but also with PlayerData and Translation, we would get two entity with our query.


What here happening is that we get from input from the player, and adds it’s value to the Player’s actual position retrieved from it’s Translation. After that, we update the PlayerData which will be used by the EnemyMovementSystem.

Now if we hit play and give some input, our Player will be moving in an ECS style. Next step to move the enemies.

The enemies

At first, I only wanted the enemies to move towards my Player, nothing else. So make it happen.

ComponentData


// Assets/Scripts/Enemy/EnemyData.cs

using Unity.Entities;

public struct EnemyData : IComponentData
{
  public float Speed;
}

Nothing new here, let’s move on.

EnemyProxy


// Assets/Scripts/Enemy/EnemyProxy.cs

using Unity.Entities;
using UnityEngine;

public class EnemyProxy : MonoBehaviour, IConvertGameObjectToEntity
{
#pragma warning disable 0649
  [SerializeField] private float _speed;
#pragma warning restore 0649

  public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
  {
    dstManager.AddComponentData(entity, new EnemyData() { Speed = _speed });
  }
}

Nothing new here either, just passing the speed value from the Editor to the entity. We could use a Vector2 for a random float if we want the enemies with various speed.

System


// Asstes/Scripts/Enemy/EnemyMovementSystem.cs

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class EnemyMovementSystem : JobComponentSystem
{
  [BurstCompile]
  private struct GetPlayerPositionJob : IJobForEach<PlayerData>
  {
    public NativeArray<PlayerData> PlayerData;

    public void Execute([ReadOnly] ref PlayerData playerData)
    {
      PlayerData[0] = playerData;
    }
  }

  [BurstCompile]
  private struct EnemyMovementJob : IJobForEach<EnemyData, Translation>
  {
    // [NativeDisableParallelForRestriction]
    [ReadOnly] public NativeArray<PlayerData> PlayerData;
    public float DeltaTime;

    public void Execute([ReadOnly] ref EnemyData enemyData, ref Translation enemyPosition)
    {
      float3 dir = PlayerData[0].Position.Value - enemyPosition.Value;
      float magnitude = math.sqrt((dir.x * dir.x) + (dir.y * dir.y) + (dir.z * dir.z));
      float3 normalized = new float3(dir.x / magnitude, dir.y / magnitude, dir.z / magnitude);

      float3 newPos = enemyPosition.Value + normalized * DeltaTime * enemyData.Speed;

      enemyPosition.Value = newPos;
    }
  }

  protected override JobHandle OnUpdate(JobHandle inputDeps)
  {
    NativeArray<PlayerData> playerData = new NativeArray<PlayerData>(1, Allocator.TempJob);

    JobHandle playerDataJob = new GetPlayerPositionJob()
    {
      PlayerData = playerData
    }.Schedule(this, inputDeps);

    JobHandle enemyMovementJob = new EnemyMovementJob()
    {
      PlayerData = playerData,
      DeltaTime = Time.deltaTime
    }.Schedule(this, playerDataJob);

    enemyMovementJob.Complete();
    playerData.Dispose();

    return enemyMovementJob;
  }
}

Anno when I got here, there’s a lot of things I didn’t know about, and not this solution is the best, but it can teach us some new things and you can see the whole process. Two new things are here, Job dependency and how we can pass information from one to another. We use two Job, one for retrieving the Player’s position from the PlayerData and one for moving the enemies. As you can see, the first’s Schedule takes the inputDepts as a second parameter, and the second one takes the first one. With this, the backend knows there is an order, and starts the second one when the first is completed.

For passing data between Jobs, we use NativeContainers (in case of simple managed types you don’t need it). We create it in the OnUpdate, passing to the first Job, then to the second one. We must dispose it in the same frame, but for this, we must get back the ownership of the NativeContainer. This is done by manually call the Complete method on the Job. By default, it’s called by the backend when it’s necessary, but in this case we have to do it by ourselves. Also, we don’t need to Complete our Jobs independently, because the our second Job depending on the first one.

At first, I didn’t know about the [ReadOnly] attribute and got an error message like this:

IndexOutOfRangeException: Index 0 is out of restricted IJobParallelFor range [4160...0] in ReadWriteBuffer.
ReadWriteBuffers are restricted to only read & write the element at the job index. You can use double buffering strategies to avoid race conditions due to reading & writing in parallel to the same elements from a job.

I didn’t know, what caused the problem and found the commented out [NativeDisableParallelForRestriction], as a solution here. Later on I realized I write PlayerData in the PlayerMovementSystem and if I tell the compiler that I want to only read that data, there won’t be a problem. The disabling attribute not an elegant solution, because we kill the performance part, what we want to achieve.

Now we have something that started to look alike a game. At least a little bit. At this point I arrived to the next tricky part, I want this working with physics!

next Post ⇒