前言
Advanced示例相比较HelloCube稍微难一些,HelloCube是一些ECS的入门案例,进阶篇的学习更利于实战开发。
1.FixedTimestepWorkaround
效果图
直观的操作感受就是拖动进度条下面一个发射器发射的”子弹”频率会不同,感觉比之前的案例稍微复杂一些,还有UI的交互了,然后UI的交互也就是改变了某个频率的参数而已,并没有那么唬人。
代码
先看上面一个固定频率发射的例子,VariableSpawner还是跟之前的案例一样挂上了ConvertToEntity组件和自定义的VariableRateSpawnerAuthoring脚本。
VariableSpawner
1 | namespace Samples.FixedTimestepSystem.Authoring |
上面的代码还是参考上一篇案例中写的,一模一样,没啥特殊的地方,先声明自己的预设,然后将自身转化为实体,然后在System中利用预设实体来不断生成新的实体。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
27namespace Samples.FixedTimestepSystem
{
public struct VariableRateSpawner : IComponentData
{
public Entity Prefab;
}
/// <summary>
/// 波动生成系统
/// </summary>
public class VariableRateSpawnerSystem : ComponentSystem
{
protected override void OnUpdate()
{
//遍历所有的带有VariableRateSpawner和Translation组件的实体,发送更新命令,在更新命令中为其添加组件,并把数据交给组件存储
Entities.ForEach((Entity spawnerEntity, ref VariableRateSpawner spawnerData, ref Translation translation) =>
{
var spawnTime = (float)Time.ElapsedTime;
var newEntity = PostUpdateCommands.Instantiate(spawnerData.Prefab);
PostUpdateCommands.AddComponent(newEntity, new Parent {Value = spawnerEntity});
PostUpdateCommands.AddComponent(newEntity, new LocalToParent());
PostUpdateCommands.SetComponent(newEntity, new Translation {Value = new float3(0, 0.3f * math.sin(5.0f * spawnTime), 0)}); //根据时间设置坐标,主要是设置Y的坐标,会显得有波动感
PostUpdateCommands.SetComponent(newEntity, new ProjectileSpawnTime{SpawnTime = spawnTime});
});
}
}
}
这里把组件和系统一起写了,因为组件太简单了就一条信息,如果有强迫症的话其实是可以分成两个脚本的。这段代码的大概意思就是遍历spawner实体,然后不断生成新的临时的实体,这些临时的实体会移动,这里用到一个新的功能类PostUpdateCommands,它负责将更新命令发送给处理系统,这些更新命令会按照权重缓存起来,然后在主线程中排队执行,Instantiate是实例化Entity,SetComponent是设置组件数据,Translation组件是移动组件,使得Entity能够移动,这里有添加上两个新的组件Parent和LocalToParent,这两个是之前没有接触过的,我们来新学习一下。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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60/// <summary>
/// 类似原来的Transform组件
/// </summary>
namespace Unity.Transforms
{
/// <summary>
/// 父组件,只储存了实体的父实体
/// </summary>
[Serializable]
[WriteGroup(typeof(LocalToWorld))]
public struct Parent : IComponentData
{
public Entity Value;
}
/// <summary>
/// 前任父组件,储存上一任父实体
/// </summary>
[Serializable]
public struct PreviousParent : ISystemStateComponentData
{
public Entity Value;
}
/// <summary>
/// 子实体
/// </summary>
[Serializable]
[InternalBufferCapacity(8)]
[WriteGroup(typeof(ParentScaleInverse))]
public struct Child : ISystemStateBufferElementData
{
public Entity Value;
}
}
/// <summary>
/// 发射体的生成时间
/// </summary>
[Serializable]
public struct ProjectileSpawnTime : IComponentData
{
public float SpawnTime;
}
/// <summary>
/// 相对于父实体的本地坐标
/// </summary>
[Serializable]
[WriteGroup(typeof(LocalToWorld))]
public struct LocalToParent : IComponentData
{
public float4x4 Value;
public float3 Right => new float3(Value.c0.x, Value.c0.y, Value.c0.z);
public float3 Up => new float3(Value.c1.x, Value.c1.y, Value.c1.z);
public float3 Forward => new float3(Value.c2.x, Value.c2.y, Value.c2.z);
public float3 Position => new float3(Value.c3.x, Value.c3.y, Value.c3.z);
}
Parent组件会引用父级的LocalToWorld,更多相关内容可以查看官方文档)
下面是生成时间的数据组件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/// <summary>
/// 发射体的生成时间组件
/// </summary>
[Serializable]
public struct ProjectileSpawnTime : IComponentData
{
public float SpawnTime;
}
/// <summary>
/// 发射体生成时间设置
/// </summary>
[RequiresEntityConversion]
public class ProjectileSpawnTimeAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
public float SpawnTime;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var data = new ProjectileSpawnTime { SpawnTime = SpawnTime };
dstManager.AddComponentData(entity, data);
}
}
最后一个就是子弹移动系统MoveProjectilesSystem1
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
namespace Samples.FixedTimestepSystem
{
/// <summary>
/// 发射体移动系统,负责将发射体不断向前移动,并在超出寿命后摧毁实体
/// </summary>
public class MoveProjectilesSystem : JobComponentSystem
{
[BurstCompile]
struct MoveProjectileJob : IJobForEachWithEntity<ProjectileSpawnTime, Translation>
{
public EntityCommandBuffer.Concurrent Commands;
public float TimeSinceLoad;
public float ProjectileSpeed;
//超过生命周期的实体进行删除,没有超过时间的则向前移动
public void Execute(Entity entity, int index, [ReadOnly] ref ProjectileSpawnTime spawnTime, ref Translation translation)
{
float aliveTime = (TimeSinceLoad - spawnTime.SpawnTime);
if (aliveTime > 5.0f)
{
Commands.DestroyEntity(index, entity);
}
translation.Value.x = aliveTime * ProjectileSpeed;
}
}
BeginSimulationEntityCommandBufferSystem m_beginSimEcbSystem;
protected override void OnCreate()
{
m_beginSimEcbSystem = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var jobHandle = new MoveProjectileJob()
{
Commands = m_beginSimEcbSystem.CreateCommandBuffer().ToConcurrent(),
TimeSinceLoad = (float)Time.ElapsedTime,
ProjectileSpeed = 5.0f,
}.Schedule(this, inputDependencies);
m_beginSimEcbSystem.AddJobHandleForProducer(jobHandle); //添加任务到待处理
return jobHandle;
}
}
}
FixedSpawner
手动控制发射频率的案例跟上面案例不同之处就在于可以UI上进度条控制Update的频率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
34using Unity.Entities;
using UnityEngine;
using UnityEngine.UI;
namespace Samples.FixedTimestepSystem
{
/// <summary>
/// FixedUpdate中手动更新创建的系统
/// </summary>
[AddComponentMenu("DOTS Samples/FixedTimestepWorkaround/Fixed Timestep Updater")]
public class FixedTimestepUpdater : MonoBehaviour
{
FixedRateSpawnerSystem spawnerSystem;
public Slider fixedTimestepSlider; //控制更新频率的进度条
Text sliderLabelText;
void Start()
{
sliderLabelText = fixedTimestepSlider.GetComponentInChildren<Text>();
}
void FixedUpdate()
{
if (spawnerSystem == null)
{
spawnerSystem = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<FixedRateSpawnerSystem>();
}
Time.fixedDeltaTime = fixedTimestepSlider.value; //将进度条上的时间赋值给Time.fixedDeltaTime从而来影响系统的Update时间
sliderLabelText.text = $"Fixed Timestep: {fixedTimestepSlider.value*1000} ms";
spawnerSystem.Update();
}
}
}
1 | using Unity.Entities; |
主要的代码还是大同小异,这里有一个搜集指定组件的Entity群。GetEntityQuery也有好几种搜索方式,这是其中一种,添加上ComponentType.ReadOnly搜索效率会高一点。
其他查询相关的知识点:
- EntityQueryDesc
这里的Desc我们一般会理解为下降,但这里是描述(describe)的意思
1 | var query = new EntityQueryDesc |
查询选项
当你创建一个EntityQueryDesc,你还可以设置他的Options 变量. options变量允许进行一些特殊的查询(通常您不需要设置它们):- Default — 无设置选项;通常的查询行为.
- IncludePrefab — 包括拥有特殊预制标签组件的原型
- IncludeDisabled — 包括拥有特殊禁用标记组件的原型
- FilterWriteGroup — 考虑任何被查询组件的WriteGroup属性
当你设置了 FilterWriteGroup 选项,如果存在一些组件来自于某一个Write Group(写入组),那么只有它们显式地被包含在查询条件中,对应的实体才会包含在视图中,如果某些实体包含位于同一WriteGroup的任何其他组件,它们将会排除在外。{译者注:也就是每一类WriteGroup来说,尽可能同时只允许一个结构来执行访问}
1 | public struct C1: IComponentData{} |
- 组合查询
1 | var query0 = new EntityQueryDesc |
- 创建EntityQuery
1 | EntityQuery m_Group = CreateEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>()); |
- 筛选出数据变化的组件
- Shared component values —根据共享组件的不同数值样本,来筛选实体集
- Change filter — 根据指定类型的组件是否可能已经发生了变化,来筛选实体集
1 | m_Group = GetEntityQuery(typeof(LocalToWorld), ComponentType.ReadOnly<Translation>()); |
流程图
2.Boids
这个案例向我们展示了大量鱼群(50000)实体的风采,是不是非常的壮观,简直屌爆了,动态效果图见上图gif,关键还能保持70帧率。
效果图
代码
代码主要选一些跟之前案例不一样的挑选看一下,一样的就略过了。
鱼群的共享数据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/// <summary>
/// 大群的共享数据
/// </summary>
[Serializable]
[WriteGroup(typeof(LocalToWorld))]
public struct Boid : ISharedComponentData
{
/// <summary>
/// 单元半径
/// </summary>
public float CellRadius;
/// <summary>
/// 间隔宽度
/// </summary>
public float SeparationWeight;
/// <summary>
/// 对齐宽度
/// </summary>
public float AlignmentWeight;
/// <summary>
/// 目标宽度
/// </summary>
public float TargetWeight;
/// <summary>
/// 排斥距离
/// </summary>
public float ObstacleAversionDistance;
/// <summary>
/// 移动速度
/// </summary>
public float MoveSpeed;
}
1 | /// <summary> |
流程图
2019.3版本ECS代码改进
新版本针对一些繁琐的模板代码做了一些改进,可以对比看出简洁了不少,不过Lambda表达式的方式会产生额外的GC,所以还需要进一步改进。
更多参考资料
- https://blog.csdn.net/lrh3025/article/details/103646556 LocalToWorld和Parent相关的文档
- https://github.com/iothua/HuaECS/wiki/ECS%E5%AE%98%E6%96%B9%E4%B8%AD%E6%96%87API(%E8%87%AA%E8%AF%91)
- https://blog.csdn.net/AndrewFan/article/details/90619637 EntityQuery相关的