0下载Unity编辑器(2019.1.0f1 or 更新的版本),if(已经下载了)continue;
1下载官方案例,打开Git Shell输入:
git clone https://github.com/Unity-Technologies/EntityComponentSystemSamples.git --recurse
or 点击Unity官方ECS示例下载代码
if(已经下载了)continue;
2用Unity Hub打开官方的项目:ECSSamples
3在Assets目录下找到Advanced/FixedTimestepWorkaround,并打开FixedTimestepWorkaround场景
之前学习的7个HelloCube小案例只是ECS入门,比较简单,现在的进阶篇会复杂一些了:
如上图所示,这个案例采用的双主角模式,让我想起了电影《无双》。这个VariableSpawner可以看成发哥,FixedSpawner可以看成城哥,不要小看城哥了,虽然用了Fixed来修饰,让人误以为一层不变,然而却装配了一个Slider,使其在不同的时间步长上千变万化,这就像我们的大脑,表面上波澜不惊,实则已经波涛汹涌。
跑题了,我们先看VariableSpawner发哥好了,打开发哥生成器的Inspector窗口,我们发现上面挂载了ConvertToEntity脚本。这个ConvertToEntity可以看成是常用组件了,任何游戏对象上挂载就可以在运行的时候自动转化成实体。
我猜在新一代的Unity编辑器中,新建物体的时候,不再需要挂载ConvertToEntity脚本。在右键Create的时候,就会有Entity选项,直接创建实体。或者创建出来的所有东西都是默认以实体创建的,再也没有转化的过程。
届时我们将忽略实体和游戏对象的概念,回归组件式开发,ECS默默在底层运行。
再次跑题,发哥身上除了ConvertToEntity脚本外,还有一个VariableRateSpawnerAuthoring脚本,且看:
///
/// 参考案例6. SpawnFromEntity
///
public class VariableRateSpawnerAuthoring : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
public GameObject projectilePrefab;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new VariableRateSpawner
{
Prefab = conversionSystem.GetPrimaryEntity(projectilePrefab),
};
dstManager.AddComponentData(entity, spawnerData);
}
public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
referencedPrefabs.Add(projectilePrefab);
}
}
这个脚本和案例6. SpawnFromEntity的实体写法一样,先声明自己的预设,然后将自身转化成实体,最后在System中利用预设实体来不断生成新的实体。
///
/// 组件,只负责存储数据
///
public struct VariableRateSpawner : IComponentData
{
public Entity Prefab;
}
///
/// 波动生成系统
///
public class VariableRateSpawnerSystem : ComponentSystem
{
protected override void OnUpdate()
{
//遍历所有实体,发送更新命令,在更新命令中为其添加组件,并把数据交给组件储存
Entities.ForEach((Entity spawnerEntity, ref VariableRateSpawner spawnerData, ref Translation translation) =>
{
var spawnTime = Time.timeSinceLevelLoad;
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)});
PostUpdateCommands.SetComponent(newEntity, new ProjectileSpawnTime{SpawnTime = spawnTime});
});
}
}
这里把组件和系统合在一起写了,毕竟组件就储存一条信息而已,实在没有必要额外弄一个脚本。
这里学到一个新的功能类PostUpdateCommands,它负责将更新命令发送给处理系统,这些更新命令会按照权重缓存起来,然后在主线程中排队执行。这里使用Instantiate来实例化实体,AddComponent来添加组件,SetComponent来传递数据。
这里值得一体的是System用到的几个组件:
///
/// 类似原来的Transform组件
///
namespace Unity.Transforms
{
///
/// 父组件,只储存了实体的父实体
///
[Serializable]
[WriteGroup(typeof(LocalToWorld))]
public struct Parent : IComponentData
{
public Entity Value;
}
///
/// 前任父组件,储存上一任父实体
///
[Serializable]
public struct PreviousParent : ISystemStateComponentData
{
public Entity Value;
}
///
/// 子实体
///
[Serializable]
[InternalBufferCapacity(8)]
[WriteGroup(typeof(ParentScaleInverse))]
public struct Child : ISystemStateBufferElementData
{
public Entity Value;
}
}
///
/// 发射体的生成时间
///
[Serializable]
public struct ProjectileSpawnTime : IComponentData
{
public float SpawnTime;
}
///
/// 相对于父实体的本地坐标
///
[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);
}
这几个实体组件,都继承了组件相关的空接口,作用只是为了表明自己是什么样的组件,方便系统在使用时刷选。
在波动生成系统中不断发射出来的动感光波,其实就是ProjectileSpawnTimeAuthoring实体,其本身就是实体了,并不需要转化,只是需要把生成时间数据交给ProjectileSpawnTime 组件保存,这里姑且把两者放到一起:
///
/// 发射体的生成时间组件
///
[Serializable]
public struct ProjectileSpawnTime : IComponentData
{
public float SpawnTime;
}
///
/// 发射体生成时间设置
///
[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);
}
}
剩下一个MoveProjectilesSystem 的脚本:
///
/// 发射体移动系统,负责将发射体不断向前移动,并在超出寿命后摧毁实体
///
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 = Time.timeSinceLevelLoad,
ProjectileSpeed = 5.0f,
}.Schedule(this, inputDependencies);
m_beginSimEcbSystem.AddJobHandleForProducer(jobHandle);
return jobHandle;
}
}
这个系统类似于奥特曼的动感光波发射器,很简单。
而城哥FixedSpawner和发哥发射动感光波的做法是一样的,只是改了个名字而已,你可以认为他们俩其实是一个人。就像电影《无双》里面的情节一样,发哥是城哥臆想出来的一个人物,发哥干了什么好事坏事,城哥也干了。
城哥与发哥不一样的地方在于,城哥还有一个厉害的姘头FixedTimestepUpdater,这个姘头获取了滑动条Slider的数据,用来改变FixedUpdate的更新速率,然后在FixedUpdate中调用其生成系统的Update,最终达到操纵动感光波发射时间的目的。
姘头并没有实际改变实体的组件数据和发射系统,改变的只是更新时间,例如原来是0.02秒更新一次,通过改变Time.fixedDeltaTime的值,就可以改变FixedUpdate的更新速率,如下图所示:
这里其实有三套ECS,首先是发射体Projectile,它的整个系统都被后者利用;然后是发哥VariableSpawner和城哥FixedSpawner,他们俩都是发射动感光波,也就是不断生成发射体;最后是城哥的姘头FixedTimestepUpdater,姘头其实向我们展示了如何在MonoBehaviour中操作ECS的系统。
Projectile:
ECS | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity | ProjectileSpawnTimeAuthoring | IConvertGameObjectToEntity | |
Component | ProjectileSpawnTime | IComponentData | |
System | MoveProjectilesSystem | JobComponentSystem |
VariableSpawner:
ECS | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity | VariableRateSpawnerAuthoring | IConvertGameObjectToEntity | IDeclareReferencedPrefabs |
Component | VariableRateSpawner | IComponentData | |
System | VariableRateSpawnerSystem | ComponentSystem |
FixedSpawner:
ECS | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity | FixedRateSpawnerAuthoring | IConvertGameObjectToEntity | IDeclareReferencedPrefabs |
Component | FixedRateSpawner | IComponentData | |
System | FixedRateSpawnerSystem | ComponentSystem |
这里值得注意的是城哥和发哥的生成系统继承的是ComponentSystem,而非发射体系统继承的JobComponentSystem,JobComponentSystem利用了Jobs和Burst编译器,ComponentSystem显然并没有,而且可以受姘头的影响。
Spawn流程大体如下:
DOTS系统:
无双:
我是有多无聊,在这里演示无双的剧情,无双和此案例真的没有任何关联,这一篇笔记纯属臆想。
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)