ECS进阶:FixedTimestepWorkaround

基于Unity2019最新ECS架构开发MMO游戏笔记7

  • 官方ECS进阶案例解析
      • 开始之前的准备工作:
    • ECS进阶:FixedTimestepWorkaround
      • 小结
        • DOTS 逻辑图表
  • 更新计划
    • 作者的话
  • ECS系列目录
    • ECS官方示例1:ForEach
    • ECS官方案例2:IJobForEach
    • ECS官方案例3:IJobChunk
    • ECS官方案例4:SubScene
    • ECS官方案例5:SpawnFromMonoBehaviour
    • ECS官方案例6:SpawnFromEntity
    • ECS官方案例7:SpawnAndRemove
    • ECS进阶:FixedTimestepWorkaround
    • ECS进阶:Boids
    • ECS进阶:场景切换器
    • ECS进阶:MegaCity0
    • ECS进阶:MegaCity1
    • UnityMMO资源整合&服务器部署
    • UnityMMO选人流程
    • UnityMMO主世界

官方ECS进阶案例解析

开始之前的准备工作:

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场景

ECS进阶:FixedTimestepWorkaround

之前学习的7个HelloCube小案例只是ECS入门,比较简单,现在的进阶篇会复杂一些了:
ECS进阶:FixedTimestepWorkaround_第1张图片
如上图所示,这个案例采用的双主角模式,让我想起了电影《无双》。这个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进阶:FixedTimestepWorkaround_第2张图片

小结

这里其实有三套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显然并没有,而且可以受姘头的影响。

DOTS 逻辑图表

Spawn流程大体如下:

DeclareReferencedPrefabs
Convert
ConvertGameObjectHierarchy
Execute
Instantiate
SetComponent
Spawner
Prefab
SpawnerEntity
PrefabEntity
CommandBuffer
Entities
DOTS

DOTS系统:

Data
Schedule
OnUpdate
Entities
Component
System
Burst
Jobs
ForEach

无双:

发哥 城哥 姘头 你天生就干这个的!任务交给你了! 我不干,我不干! 你不干的话,就休想干我! 为了干你,我干了! 我让你快点干你就快点干! 我让你慢点干你就慢点干! 好好,都听你的! 这个世界! 姘头说了算! 旋转,跳跃 我闭着眼…… 为了姘头,我 什么都可以做! 假的,什么都是假 的,我也是假的! 发哥 城哥 姘头

我是有多无聊,在这里演示无双的剧情,无双和此案例真的没有任何关联,这一篇笔记纯属臆想。

更新计划

Mon 12 Mon 19 Mon 26 1. ForEach 2. IJobForEach 3. IJobChunk 4. SubScene 5. SpawnFromMonoBehaviour 6. SpawnFromEntity 7. SpawnAndRemove 休息 修正更新计划 参加表哥婚礼 进阶:FixedTimestepWorkaround 进阶:BoidExample 进阶:SceneSwitcher 我是休息时间 资源整合 部署服务器 启动流程 登录流程 游戏主世界 待计划 待计划 待计划 待计划 待计划 我是休息时间 待计划 待计划 待计划 待计划 待计划 我是休息时间 读取Excel自动生成Entity 读取Excel自动生成Component 读取数据库自动生成Entity 读取数据库自动生成Component ESC LuaFrameWork Skynet DOTS 官方示例学习笔记 -----休息----- 基于ECS架构开发MMO学习笔记 LuaFrameWork学习笔记 -----休息----- 基于Skynet架构开发服务器学习笔记 制作代码自动生成工具 总结 基于Unity2019最新ECS架构开发MMO游戏笔记

作者的话

AltAlt

如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)

ECS系列目录

ECS官方示例1:ForEach

ECS官方案例2:IJobForEach

ECS官方案例3:IJobChunk

ECS官方案例4:SubScene

ECS官方案例5:SpawnFromMonoBehaviour

ECS官方案例6:SpawnFromEntity

ECS官方案例7:SpawnAndRemove

ECS进阶:FixedTimestepWorkaround

ECS进阶:Boids

ECS进阶:场景切换器

ECS进阶:MegaCity0

ECS进阶:MegaCity1

UnityMMO资源整合&服务器部署

UnityMMO选人流程

UnityMMO主世界

你可能感兴趣的:(ECS,Unity,DOTS)