【Unity】ECS框架学习笔记(二)

属性

[RequireMatchingQueriesForUpdate]

在Update中使用 Entities.WithAll<>(), Entities.WithAny<>(), Entities.ForEach() 等方法时,Unity 会自动生成一个 EntityQueryIJobEntity也会创建)。系统通过这个 EntityQuery 去查找符合条件的实体。

该属性的作用是确保只有在系统的 EntityQuery 匹配了实际的实体数据时,系统才会执行update方法。


实用接口

1
2
3
4
5
Entity entity = SystemAPI.GetSingletonEntity<PlayerTag>();						 // 获取实体
bool has = SystemAPI.HasComponent<ChampTag>(entity); // 判断entitiy是否拥有某组件
GhostOwner newtworkId = SystemAPI.GetComponent<GhostOwner>(entity).NetwordId; // 直接获取entity上的组件
bool entityExists = EntityManager.Exists(entity); // 判断实体是否存在有效
bool has = SystemAPI.HasSingleton<GamePlayingTag>() // 判断整个场景中是否有组件,几个无所谓

注意事项

returncontinue

虽然是一个很显而易见且简单的问题,但确实很容易被忽视

在遍历Query的时候如果想要跳过这一Entity的时候不要使用return,而是使用continue,例如该Entity数据为空,不需要处理
除非你不再需要遍历后面的其他的Entity,例如想要寻找某个Entity

1
2
3
4
foreach (var damageBuffer in SystemAPI.Query<DynamicBuffer<DamageBufferElement>>())
{
if (damageBuffer.IsEmpty) continue; // 注意:不能使用return,不然会跳过剩余的Entity
}

相信在处理大多数Query的时候都是希望跳过这一个Entity,而不是跳过后续所有的Entity。所以在这里提醒自己注意一下这个问题


单例

保存单例的引用

如果确定整个场景只有确定个数的实体,并且数量也不会改变,那么可以直接在OnStartRunning直接获取到该实体并保存

该方法只适合SystemBase。如果想在Isystem的OnCreate中保存单例,后续使用该组件的时候会报错,提示引用丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 实验组——存储单例
public partial class PlayerMoveSystem_______ : SystemBase
{
private Entity playerEntity;

protected override void OnCreate()
{
RequireForUpdate<PlayerTag>();
}

protected override void OnStartRunning()
{
// 获取一个
playerEntity = SystemAPI.GetSingletonEntity<PlayerTag>();
// 获取多个
// foreach (var (tag, entity) in SystemAPI.Query<PlayerTag>().WithEntityAccess())
// {
// playerEntity = entity;
// break; // 只需要第一个玩家实体
// }
}

protected override void OnUpdate()
{
var deltaTime = SystemAPI.Time.DeltaTime;
var moveInput = SystemAPI.GetComponent<PlayerMoveInput>(playerEntity);
var transform = SystemAPI.GetComponentRW<LocalTransform>(playerEntity);
var speed = SystemAPI.GetComponent<Speed>(playerEntity);

transform.ValueRW.Position.xz += moveInput.Value * speed.Value * deltaTime;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 对照组——直接查找对象
[UpdateBefore(typeof(TransformSystemGroup))]
public partial struct PlayerMoveSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var deltaTime = SystemAPI.Time.DeltaTime;

/*
JobHandle job =
new PlayerMoveJob()
{
DeltaTime = deltaTime
}.Schedule(state.Dependency);
job.Complete();*/

foreach (var (moveInput, transform, speed) in SystemAPI.Query<PlayerMoveInput, RefRW<LocalTransform>, Speed>())
{
transform.ValueRW.Position.xz += moveInput.Value * speed.Value * deltaTime;
}
}
}

以上两种方式的性能测试结果:

设置单例属性

在确定一个组件为单例时,可以使用SystemAPI.SetSingleton(IComponentData)保存该组件的值

  • 该组件必须没有实现IEnableableComponentEntityQuery.SetSingleton{T}
  • 无法在Entities.ForEachIJobEntityUtility methodsAspects中使用
1
2
3
var gamePropertyEntity = SystemAPI.GetSingletonEntity<GameStartProperties>();
var teamPlayerCounter = SystemAPI.GetComponent<TeamPlayerCounter>(gamePropertyEntity);
SystemAPI.SetSingleton(teamPlayerCounter); // 设置该组件的值

监控数量

使用EntityQuery监控指定类型的数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public partial class PlayerSpawnerSystem : SystemBase
{
protected override void OnUpdate()
{
EntityQuery playerEntityQuery = EntityManager.CreateEntityQuery(typeof(PlayerTag));

PlayerSpawner playerSpawner = SystemAPI.GetSingleton<PlayerSpawner>();

var entityCommandBuffer = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(World.Unmanaged);

int spawnAmount = 10;
if (playerEntityQuery.CalculateEntityCount() < spawnAmount)
{
var entity = entityCommandBuffer.Instantiate(playerSpawner.playerPrefab);

entityCommandBuffer.SetComponent(entity, new Speed
{
value = DotsHelpers.GetRandomFloat(2, 5)
});
}
}
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public partial struct ClientRequestGameEntrySystem : ISystem
{
private EntityQuery _pendingNetworkIdQuery;

public void OnCreate(ref SystemState state)
{
// 虽然是在OnCreate中创建的Query,但是并没有销毁掉,所以还是会一直更新里面的实体
var builder = new EntityQueryBuilder(Allocator.Temp).WithAll<NetworkId>().WithNone<NetworkStreamInGame>();
_pendingNetworkIdQuery = state.GetEntityQuery(builder);
state.RequireForUpdate(_pendingNetworkIdQuery);
}
public void OnUpdate(ref SystemState state)
{
var pendingNetworkIds = _pendingNetworkIdQuery.ToEntityArray(Allocator.Temp);
}
}

ISystem的用法,需要使用.Dispose()手动释放资源

1
var query = SystemAPI.QueryBuilder().WithAll<NewEnemyTag>().Build();

移除组件

v1.2.4貌似添加和删除组件都只能使用缓冲器

确认之后不会再使用的组件可以使用缓冲器移除

1
2
3
4
5
var ecb = SystemAPI.GetSingleton<EndInitializationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);		// 延迟到这一帧初始组结束时移除
foreach (var (_, entity) in SystemAPI.Query<NewEnemyTag>().WithEntityAccess())
{
ecb.RemoveComponent<NewEnemyTag>(entity);
}
1
2
3
4
5
6
7
var ecb = new EntityCommandBuffer(Allocator.Temp);			// 立即移除
foreach (var (_, entity) in SystemAPI.Query<NewEnemyTag>().WithEntityAccess())
{
ecb.RemoveComponent<NewEnemyTag>(entity);
}
ecb.Playback(state.EntityManager); // 手动创建的ecb需要手动触发
ecb.Dispose();

创建实体

1
2
3
4
var ecb = new EntityCommandBuffer(Allocator.Temp);
var swordEntity = ecb.Instantiate(_swordSprite);
ecb.Playback(EntityManager);
ecb.Dispose();

销毁实体

SystemBase

1
EntityManager.DestroyEntity(gamePlayingEntity);

ISystem

1
2
3
var ecb = new EntityCommandBuffer(Allocator.Temp);
ecb.DestroyEntity(requestEntity);
ecb.Playback(EntityManager);

在其他方法中使用SystemAPI接口

1
2
3
4
5
6
7
8
public void OnUpdate(ref SystemState state)
{
SpawnOnEachLane(ref state);
}
private void SpawnOnEachLane(ref SystemState state) // 如果没有传入state,就无法使用SystemAPI的接口
{
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
}

一个组件烘焙多次

一个实体上只能拥有一个同名的组件,如果一个组件需要挂载多次,可以使用CreateAdditionalEntity创建一个额外的实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Vector3[] TopLanePath;
public Vector3[] MidLanePath;
public Vector3[] BotLanePath;

public override void Bake(MinionPathAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.None); // 创建本身
// 添加
var topLane = CreateAdditionalEntity(TransformUsageFlags.None, false, "TopLane");
var midLane = CreateAdditionalEntity(TransformUsageFlags.None, false, "MidLane");
var botLane = CreateAdditionalEntity(TransformUsageFlags.None, false, "BotLane");
var topLanePath = AddBuffer<MinionPathPosition>(topLane);
var midLanePath = AddBuffer<MinionPathPosition>(midLane);
var botLanePath = AddBuffer<MinionPathPosition>(botLane);
}

灵活使用关键字WithAny捕获

WithAny只要有一个符合就捕获

1
2
// 捕获拥有碰撞物理,且有队伍的实体					// 既可以是英雄,也可以是小兵
SystemAPI.Query<RefRW<PhysicsMass>, MobaTeam>().WithAny<NewChampTag, NewMinionTag>();

事件

在GameObject中,事件是使用代码写Action来触发

但是在Entity中,事件的触发分为如下步骤

  1. 准备:
    • 创建一个预制体,单独给这个预制体准备一个标签如GameOverTag,并保存该对该预制体的引用。不能直接将该预制体放置在Entities场景中,而是存储其引用。
    • 创建一个监听系统,如GameOverSystem,一直捕获或者使用RequireForUpdate<GameOverTag>();执行逻辑
  2. 在你需要触发该事件的时候,比如在DestroySystem中检测到被摧毁的entity是基地,那么就将创建准备好的entity
  3. GameOverSystem监听到场景中存在GameOverTag标签后,执行GameOverSystem逻辑

创建预制体详细流程:

与[Entities => GameObject](#Entities => GameObject)不同,我们需要创建的是Entity,所以我们要存储的是Entity类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public struct MobaPrefabs : IComponentData		// 是结构体,而不是class
{
public Entity GameOverEntity; // 以Entity的形式保存下来
}
public class MobaPrefabsAuthoring : MonoBehaviour
{
public GameObject GameOverEntity; // 在Inspector窗口上引入预制体
private class Baker : Baker<MobaPrefabsAuthoring>
{
public override void Bake(MobaPrefabsAuthoring authoring)
{
var prefabContainerEntity = GetEntity(TransformUsageFlags.None);

AddComponent(prefabContainerEntity, new MobaPrefabs()
{ // 烘焙该Entity
GameOverEntity = GetEntity(authoring.GameOverEntity, TransformUsageFlags.None),
});
}
}
}

碰撞

球形射线

触发条件:

  1. 被检测的物体必须要有Physics Shape组件
  2. Physics Shape组件的Collision Filter的设置必须与射线设定的Collision Filter成对应关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CollisionFilter _collisionFilter = new CollisionFilter()
{
BelongsTo = 1 << 4, // 属于第五层Raycasts
CollidesWith = 1 << 2 // 与第二层Monsters碰撞
};

var collisionWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>().CollisionWorld; // 获取物理系统
var playerEntity = SystemAPI.GetSingletonEntity<PlayerTag>();
var transform = SystemAPI.GetComponentRO<LocalTransform>(playerEntity); // 获取玩家位置

var hits = new NativeList<DistanceHit>(Allocator.TempJob); // 存放击中的物体
if (collisionWorld.OverlapSphere(transform.ValueRO.Position, 100f, ref hits, _collisionFilter))
{
Debug.Log("hit");
}
hits.Dispose(); // 记得释放

物理碰撞

两种触发方式:

  • ICollisionEventsJob:碰撞事件
  • ITriggerEventsJob:触发事件,就可以理解成普通的包围盒勾选的Trigger

触发条件:

  1. (与射线一样)被检测的物体必须要有Physics Shape组件
  2. (与射线一样)Physics Shape组件的Collision Filter的设置必须与射线设定的Collision Filter成对应关系
  3. 碰撞的两个物体之中必须要有一个物体有Rigidbody(必须是3D的,不支持2D的Rigidbody)
  4. 最后一项也是最重要的一项,必须设置Collision Response,两个物体分别选择什么会触发哪个事件也标明在图二了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[UpdateInGroup(typeof(PhysicsSystemGroup))]			// 必须
[UpdateAfter(typeof(PhysicsSimulationGroup))] // 必须
public partial struct DamageOnTriggerSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var simulationSingleton = SystemAPI.GetSingleton<SimulationSingleton>();
// 执行碰撞事件
state.Dependency = new CountNumCollisionEvents
{
// 可以传递参数
}.Schedule(simulationSingleton, state.Dependency);
// 执行触发事件,两种写法是一样的,都可以
var job = new DamageOnTriggerJob
{
// 可以传递参数
};
state.Dependency = job.Schedule(simulationSingleton, state.Dependency);
}
}
// 碰撞事件
public struct CountNumCollisionEvents : ICollisionEventsJob
{
public void Execute(CollisionEvent collisionEvent)
{
Debug.Log($"ICollisionEventsJob");
}
}
// 触发事件
public struct DamageOnTriggerJob : ITriggerEventsJob
{
public void Execute(TriggerEvent triggerEvent)
{
Debug.Log($"ITriggerEventsJob");
}
}

关于Tick和IsFirstTimeFullyPredictingTick

参考Full vs. Partial Ticks

  • Tick是固定一秒60次,并不是update执行的次数。由于服务器没反应过来,服务器update的次数一般都会比Tick次数少,客户端反应比较快一半会比60大。

    • 服务端会将数据以Tick的频率发送给客户端,而客户端会平滑的设置其数值,有如下两种情景

      • 数值:服务端告诉客户端第20TickValue = 10,那么客户端在19Tick(假设Value=0)与20Tick之间会多次执行update,让Value从0平滑的到达10。假如客户端一个Tick可以运行10次,那么value第一次update为0,第二次update为1,第三次update为3….直到完整的到达第20Tick,Value就等于10了。

      • 事件:服务端告诉客户端第20Tick的时候创建一个小兵,那么在19Tick与20Tick之间,每次update都会创建一个小兵

        这时就需要用到IsFirstTimeFullyPredictingTick了,在19到20Tick之间updateIsFirstTimeFullyPredictingTick返回false;当你真正达到20Tick,IsFirstTimeFullyPredictingTick返回True,表示这是第一次完整的预测Tick

      假设有代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]	system在模拟组中

      var networkTime = SystemAPI.GetSingleton<NetworkTime>();
      if (networkTime.IsFirstTimeFullyPredictingTick)
      {
      ecb.生成小兵(); // 只有在20Tick的时候才会执行
      }
      Value = 10; // 在18到20Tick之间,客户端每次update都将让Value更接近10
      // 虽然这里写的是直接赋值,但客户端并不会生硬的将Value设置为10

  • 服务端与客户端的Tick并不同步。这应该就是延迟的由来?

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public partial struct TestSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var isServer = state.WorldUnmanaged.IsServer();
if (isServer)
{
var networkTime = SystemAPI.GetSingleton<NetworkTime>();
Debug.Log($"Server: Tick: {networkTime.ServerTick}, ElapsedTime: {SystemAPI.Time.ElapsedTime}");
Debug.Log("Server: Update time");
}
else
{
var networkTime = SystemAPI.GetSingleton<NetworkTime>();
Debug.Log($"Client: Tick: {networkTime.ServerTick}, ElapsedTime: {SystemAPI.Time.ElapsedTime}");
Debug.Log("Client: Update time");
}
}
}

GameObject与Entities世界交互

GameObject => Entities

系统:

1
2
3
4
// 判断Default World中 PlayerMoveSystem 系统是否存在
ClientStartGameSystem startGameSystem = World.DefaultGameObjectInjectionWorld.GetExistingSystemManaged<ClientStartGameSystem>();
// 直接使用系统中的公开属性
startGameSystem.OnUpdatePlayersRemainingToStart -= UpdatePlayerRemainingText;

实体:

1
2
3
4
// 创建实体
Entity entity = World.DefaultGameObjectInjectionWorld.EntityManager.CreateEntity();
// 添加组件
World.DefaultGameObjectInjectionWorld.EntityManager.AddComponentData(teamRequestEntity, new ClientTeamSelect(){ Value = team });
1
2
3
4
5
6
7
8
9
// 捕获EntityQuery
EntityQuery playerTagEntityQuery = World.DefaultGameObjectInjectionWorld.EntityManager
.CreateEntityQuery(typeof(PlayerTag)); // 只读
EntityQuery networkDriverQuery = World.DefaultGameObjectInjectionWorld.EntityManager
.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>()); // 读写
// 保存捕获到的Entities
NativeArray<Entity> entityNativeArray = networkDriverQuery.ToEntityArray(Allocator.Temp);
// 修改组件
networkDriverQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.Listen(serverEndpoint);
1
2
3
4
5
6
7
8
9
_entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
playerEntityQuery = _entityManager.CreateEntityQuery(typeof(PlayerTag));
// 保存捕获到的Entity
playerEntity = playerEntityQuery.GetSingletonEntity(); // 只适合确定只有一个Entity的情况
transform.position = _entityManager.GetComponentData<LocalTransform>(playerEntity).Position; // 读取组件
_entityManager.SetComponentData(playerEntity, new MoveSpeed{ Value = 2f }); // 设置组件
_entityManager.AddComponent<MoveSpeed>(playerEntity); // 添加组件
_entityManager.RemoveComponent<MoveSpeed>(playerEntity); // 删除组件

如果使用了NetCode包,在创建Client World时,需要将Default World设置为Cilient World

1
2
3
4
// 创建Client World
var clientWorld = ClientServerBootstrap.CreateClientWorld("Coffee Client World");
// 将Default World 设置为 Cilient World
World.DefaultGameObjectInjectionWorld = clientWorld;

Entities => GameObject

方法一:单例,比较简单,就不说了

方法二:创建GameObject时保存对其的引用,方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HealthBarUIReference : ICleanupComponentData		// 注意这里使用的是class而不是结构体
{
public GameObject Value;
}
// 创建GameObject
var newHealthBar = Object.Instantiate(healthBarPrefab, spawnPosition, quaternion.identity);
// 将GameObject的引用保存在组件上
ecb.AddComponent(entity, new HealthBarUIReference() { Value = newHealthBar });
// 获取该组件并使用
foreach(var obj in SystemAPI.Query<HealthBarUIReference>())
{
obj.Value.transform = new Vector3(1f, 0f, 0f); // 改变位置坐标
Slider slider = obj.GetComponentInChildren<Slider>(); // 获取子物体的UI。能获取到组件,那么就很灵活了
}

由于烘焙的时候只能挂载预制体,所以entities无法直接引用到GameObject中的物体


BlobArray<T>

特点:

  • 允许存储大量数据,并且只读
  • 存储在一块连续的内存区域,访问速度快,缓存友好
  • 由于是只读的,所以可以在多个线程中安全地共享和访问

使用方法:

  1. 定义BlobArrayBlobAsset

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public struct SpawnEnemyPoint : IComponentData
    {
    public BlobAssetReference<SpawnEnemyPointBlob> Value;
    }

    public struct SpawnEnemyPointBlob
    {
    public BlobArray<float3> Value;
    }
  2. SpawnEnemyPoint烘焙到任意实体上

  3. 使用BlobBuilder赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 初始化一个临时用的builder
    using (BlobBuilder builder = new BlobBuilder(Allocator.Temp))
    {
    // 确定需要存储的Blob结构体数据类型
    ref SpawnEnemyPointBlob pointBlob = ref builder.ConstructRoot<SpawnEnemyPointBlob>();
    // 分配大小空间
    BlobBuilderArray<float3> arrayBuilder = builder.Allocate(ref pointBlob.Value, 10);
    // 填充数据
    for (int i = 0; i < 10; i++)
    {
    arrayBuilder[i] = new float3{i, 0f, 0f};
    }
    // 创建Blob的引用(类似于指针?)
    BlobAssetReference<SpawnEnemyPointBlob> blobAsset = builder.CreateBlobAssetReference<SpawnEnemyPointBlob>(Allocator.Persistent);
    // 将创建的引用赋值给gameEntity的组件上,供其使用
    ecb.SetComponent(gameEntity, new SpawnEnemyPoint(){Value = blobAsset});
    }
  4. 使用方法与列表一样

    1
    2
    private readonly RefRO<SpawnEnemyPoint> _spawnEnemyPoint;
    var position = _spawnEnemyPoint.ValueRO.Value.Value.Value[0];

为什么不直接将BlobArray当做IComponentData挂载到实体上?

  1. BlobArray直接放在实体上,每个实体都会有独立的BlobArray副本,大大增加了内存使用
    BlobAssetReference 存储的是指向连续内存块的指针
    多个实体可以共享一个BlobArray,而不需要为每个实体分配内存
  2. IComponentData是可更改的,每次修改组件都可能会导致内存重新分配和复制,对于不可变数据来说不必要

System报错

InvalidOperationException: The previously scheduled 'IJobEntity' writes to the ComponentTypeHandle<Unity.Collections.NativeText.ReadOnly>

1
2
3
InvalidOperationException: The previously scheduled job <AJob> writes to the ComponentTypeHandle<Unity.Collections.NativeText.ReadOnly> <AJob>.<Value>.
You are trying to schedule a new job <BJob>, which reads from the same ComponentTypeHandle<Unity.Collections.NativeText.ReadOnly> (via <BJob>.<Value>).
To guarantee safety, you must include PlayerMoveSystem:PlayerMoveJob as a dependency of the newly scheduled job.
  • 两个任务共同访问了同一个IComponentData
原因

两个System.IJobEntityIAspect中以读写的方式定义了同一个实体的组件数据

生成敌人系统敌人移动系统,在Systems窗口中可以看到两个系统的执行先后

根本原因:

  • 没有定义系统之间执行顺序
  • Run()执行完后再才进入下一个system,并且在执行期间会保护数据,其他系统只能读取无法修改Run()所访问的读写组件
错误代码

生成敌人错误代码:

1
2
3
4
5
6
7
8
9
10
11
12
public partial struct SpawnEnemySystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
new AJob().Run(); // 立即在当前线程上执行,完成后再进入下一个System,期间的数据其他system无法修改其访问的组件
}
}

private partial struct AJob : IJobEntity
{
private void Execute(TestAspect aspect) { }
}
1
2
3
4
public readonly partial struct TestAspect : IAspect
{
readonly RefRW<TargetPosition> targetPosition;
}

移动系统错误代码:

1
2
3
4
5
6
7
8
9
10
11
12
public partial struct EnemyMoveSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
new BJob().Schedule(); // 调度任务到其他线程访问
}
}

public partial struct BJob : IJobEntity
{
private void Execute(EnemyMoveAspect enemy) { }
}
1
2
3
4
5
public readonly partial struct EnemyMoveAspect : IAspect
{
readonly RefRO<EnemyTag> enemyTag;
readonly RefRW<TargetPosition> targetPosition;
}

从上面的代码可以看出,仅仅只是引用就会报错

这里生成和移动敌人例子可能不太合理,但只要理解到两个系统的IAspect读写的方法定义了同一个的实体的同一个组件就OK了

注意:为了节约空间简化了不少代码

解决方法

方法一:

使用UpdateAfer属性定义执行顺序

1
2
[UpdateAfter(typeof(SpawnEnemySystem))]
public partial struct EnemyMoveSystem : ISystem

这样,在成功生成敌人后,在执行移动系统,移动系统就不会在敌人还在生成的时候就修改其组件

方法二:

将选择其中Entity较少的任务设置依赖项,并等待其编译完成

1
2
3
4
5
6
7
8
public partial struct EnemyMoveSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
JobHandle job = new BJob().Schedule(state.Dependency); // 设置依赖项
job.Complete(); // 主线程被阻塞,直到该任务完成
}
}

方法三:

使用Query获取Aspect

1
2
3
4
5
6
7
8
9
10
public partial struct EnemyMoveSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
foreach(var aspect in SystemAPI.Quert<RefRW<MoveAspect>>.WithEntityAccess())
{
...
}
}
}

InvalidOperationException: 'EntityCommandBuffer' is not declared [ReadOnly] in a IJobParallelFor job. The container does not support parallel writing. Please use a more suitable container type.

原因

该容器不支持并行写入

错误代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public partial struct EnemyMoveSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);

new EnemyMoveJob
{
ECB = ecb
}.ScheduleParallel();
}

public partial struct EnemyMoveJob : IJobEntity
{
public EntityCommandBuffer ECB;

private void Execute(EnemyMoveAspect enemy)
{
ECB.RemoveComponent<EnemyTag>(enemy.Entity);
}
}
}
解决方法

修改类型成可并行写入的缓冲器容器

1
var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter();
1
2
3
4
private void Execute(EnemyMoveAspect enemy, [ChunkIndexInQuery]int sortKey)	// 并不是普通的int类型,需要指定类型
{
ECB.RemoveComponent<EnemyTag>(sortKey, enemy.Entity);
}

使用方法:需要指定执行的顺序,如果没有特殊的顺序需要考虑,可以使用ChunkIndexInQuery

ChunkIndexInQuery

在ECS中,EntityQuery 会返回多个 “chunk”(即实体的内存块),而每个 “chunk” 存储一组符合查询条件的实体ChunkIndexInQuery 就是这些 chunk 在整个查询中的唯一索引值。

每个 chunk 在查询的范围内都有一个唯一的索引值,避免多个 chunk 之间产生混淆。

即使调度方式发生变化(例如工作在多个线程之间并行执行),这个索引值也保持不变,保证了在不同的调度执行顺序下,ChunkIndexInQuery 始终一致。

因为索引值是确定且稳定的,所以在使用 EntityCommandBuffer(ECB)时,可以保证对实体的操作是可预测和一致的(例如,记录、重放命令时不受并行或调度的影响)。


'MultiplayerDOTS.NpcAttackSystem' creates a Lookup object (e.g. ComponentLookup) during OnUpdate. Please create this object in OnCreate instead and use type _MyLookup.Update(ref systemState); in OnUpdate to keep it up to date instead. This is significantly faster.

原因

在使用了错误的API

错误代码
1
2
3
4
state.Dependency = new NpcAttackJob()
{
TransformLookup = state.GetComponentLookup<LocalTransform>(true),
}.ScheduleParallel(state.Dependency);
解决方法
1
2
3
4
state.Dependency = new NpcAttackJob()
{
TransformLookup = SystemAPI.GetComponentLookup<LocalTransform>(true),
}.ScheduleParallel(state.Dependency);