ECS进阶:Boids

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

  • 官方ECS进阶案例解析之大群
      • 开始之前的准备工作:
    • ECS进阶:Boids
      • 小结
        • 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/Boids,并打开其下的BoidExample场景

ECS进阶:Boids

Boids其实是类鸟群的意思,在咱们IT领域叫做“集群模拟算法”,但我是个漫威粉,所以称之大群,LOL。

如上图所示,这个案例向我们展示了大量(50000)实体的风采,虽然鲨鱼的模型不怎么精致,但是大群巍巍壮观,这才是ECS的真正用法吧!下面一起来看看这样震撼的场面是如何实现的吧:

        /// 
        /// E:把大群的数据传递给C
        /// 
        [RequiresEntityConversion]
        public class Boid : MonoBehaviour, IConvertGameObjectToEntity
        {
            public float CellRadius;
            public float SeparationWeight;
            public float AlignmentWeight;
            public float TargetWeight;
            public float ObstacleAversionDistance;
            public float MoveSpeed;
    
            // Lets you convert the editor data representation to the entity optimal runtime representation
            public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
            {
                dstManager.AddSharedComponentData(entity, new Samples.Boids.Boid
                {
                    CellRadius = CellRadius,
                    SeparationWeight = SeparationWeight,
                    AlignmentWeight = AlignmentWeight,
                    TargetWeight = TargetWeight,
                    ObstacleAversionDistance = ObstacleAversionDistance,
                    MoveSpeed = MoveSpeed
                });
            }
        }

以上是大群的实体Entity(以后注释统一用E缩略,表明其身份)部分,E将数据传递给C保存,下面我们一起看C:

	/// 
    /// C:大群的共享数据
    /// 
    [Serializable]
    [WriteGroup(typeof(LocalToWorld))]
    public struct Boid : ISharedComponentData
    {
        /// 
        /// 单元半径
        /// 
        public float CellRadius;
        /// 
        /// 间隔宽度
        /// 
        public float SeparationWeight;
        /// 
        /// 对齐宽度
        /// 
        public float AlignmentWeight;
        /// 
        /// 目标宽度
        /// 
        public float TargetWeight;
        /// 
        /// 排斥距离
        /// 
        public float ObstacleAversionDistance;
        /// 
        /// 移动速度
        /// 
        public float MoveSpeed;
    }

C(Component组件,以后简写C,表明身份即可)和以前不一样了,C实现了空接口 ISharedComponentData,唯一目的只是为了表明其储存的数据是共享的,一直以来C都是最简单的脚本。

下面一起来看看最复杂的S(System系统,今后简写S),大群系统BoidSystem。
由于大群系统比较复杂,因此我画了一副图来描摹几个概念,如图:
ECS进阶:Boids_第1张图片
解释下脚本中用到的几个定语:
[RequireComponentTag(typeof(组件))]:这里的组件是指C
[BurstCompile]:Burst编译,编译速度更快
[ReadOnly]:只读的方式访问,速度更快
[DeallocateOnJobCompletion]:在任务完成的时候释放内存(解除分配内存)

/// 
/// 麦克在GDC上面的讲话‘面向数据的方式来使用组件系统’对于开发大群的案例代码来说有非常大的参考价值
/// https://youtu.be/p65Yt20pw0g?t=1446 (这是油管链接,需要观看,有兴趣的朋友可以在参考)
/// 它解释了这个案例之前版本的实现,但是几乎所有的信息仍然具有参考价值。
/// 目标(2条红鱼)和对手(1条鲨鱼)基于Unity UI里的ActorAnimation(角色动画)栏移动
/// 这样它们才能基于关键帧动画来移动
/// 
namespace Samples.Boids
{
    /// 
    /// S:大群系统,在模拟系统组中更新,在Transform系统组之前更新
    /// 
    [UpdateInGroup(typeof(SimulationSystemGroup))]
    [UpdateBefore(typeof(TransformSystemGroup))]
    public class BoidSystem : JobComponentSystem
    {
        private EntityQuery  m_BoidQuery;//大群实体查询缓存
        private EntityQuery  m_TargetQuery;//目标实体查询缓存
        private EntityQuery  m_ObstacleQuery;//对手实体查询缓存

        /// 
        /// 在这个案例中总共有3个独特的大群变体,各有各的共享组件值
        /// (提示:这包含了在索引0上默认未初始化的值,该值并未被实际用在这个案例中,)
        /// 
        private List<Boid>                               m_UniqueTypes = new List<Boid>(3);
        private List<NativeMultiHashMap<int, int>> m_PrevFrameHashmaps = new List<NativeMultiHashMap<int, int>>();

        /// 
        /// `CopyPositions`和`CopyHeadings`都是为了提取相对位置、导向组件到NativeArrays(原生数组)
        /// 这样它们才能被`MergeCells`(合并单元)和`Steer`(导航)任务随机访问到 (下面是这两个结构体)
        /// 
        [BurstCompile]
        struct CopyPositions : IJobForEachWithEntity<LocalToWorld>
        {
            /// 
            /// 位置,原生数组
            /// 
            public NativeArray<float3> positions;
            /// 
            /// 把本地位置放到原生数组里
            /// 
            /// 实体
            /// 索引
            /// 只读本地位置
            public void Execute(Entity entity, int index, [ReadOnly]ref LocalToWorld localToWorld)
            {
                positions[index] = localToWorld.Position;
            }
        }
        //同上
        [BurstCompile]
        struct CopyHeadings : IJobForEachWithEntity<LocalToWorld>
        {
            public NativeArray<float3> headings;

            public void Execute(Entity entity, int index, [ReadOnly]ref LocalToWorld localToWorld)
            {
                headings[index] = localToWorld.Forward;
            }
        }
        /// 
        /// 生成一个哈希表,每一栏里包含了所有大群的索引,其位置量化成同一个值,即提供的单元半径
        /// 这样它们才能被`MergeCells`(合并单元)和`Steer`(导航)任务随机访问到 
        /// 
        [BurstCompile]
        [RequireComponentTag(typeof(Boid))]
        struct HashPositions : IJobForEachWithEntity<LocalToWorld>
        {
            public NativeMultiHashMap<int, int>.ParallelWriter hashMap;
            public float                                       cellRadius;

            public void Execute(Entity entity, int index, [ReadOnly]ref LocalToWorld localToWorld)
            {
                var hash = (int)math.hash(new int3(math.floor(localToWorld.Position / cellRadius)));
                hashMap.Add(hash, index);
            }
        }
        
        /// 
        /// 合并所有单元
        /// 这里收集了所有单元大群的位置和导向,为了做以下三件事情:
        /// 1.计算每个单元的数量
        /// 2.找到最近的对手并将互相标记为目标
        /// 3.找到包含已收集每个大群单元值的数组索引
        /// 
        [BurstCompile]
        struct MergeCells : IJobNativeMultiHashMapMergedSharedKeyIndices
        {
            public NativeArray<int>                 cellIndices;//单元索引原生数组
            public NativeArray<float3>              cellAlignment;//单元对齐原生数组
            public NativeArray<float3>              cellSeparation;//单元间隔
            public NativeArray<int>                 cellObstaclePositionIndex;//单元对手位置索引
            public NativeArray<float>               cellObstacleDistance;//单元对手距离
            public NativeArray<int>                 cellTargetPositionIndex;//单元目标位置索引
            public NativeArray<int>                 cellCount;//单元数量
            [ReadOnly] public NativeArray<float3>   targetPositions;//目标位置
            [ReadOnly] public NativeArray<float3>   obstaclePositions;//对手位置
            /// 
            /// 循环数组找到最近的位置
            /// 
            /// 目标
            /// 位置
            /// 最近位置索引
            /// 最近距离
            void NearestPosition(NativeArray<float3> targets, float3 position, out int nearestPositionIndex, out float nearestDistance )
            {
                nearestPositionIndex = 0;
                nearestDistance      = math.lengthsq(position-targets[0]);
                for (int i = 1; i < targets.Length; i++)
                {
                    var targetPosition = targets[i];
                    var distance       = math.lengthsq(position-targetPosition);
                    var nearest        = distance < nearestDistance;

                    nearestDistance      = math.select(nearestDistance, distance, nearest);
                    nearestPositionIndex = math.select(nearestPositionIndex, i, nearest);
                }
                nearestDistance = math.sqrt(nearestDistance);
            }
            /// 
            /// 首先执行:找到最近的对手和目标,并储存单元索引
            /// 
            /// 
            public void ExecuteFirst(int index)
            {
                var position = cellSeparation[index] / cellCount[index];

                int obstaclePositionIndex;
                float obstacleDistance;
                NearestPosition(obstaclePositions, position, out obstaclePositionIndex, out obstacleDistance);
                cellObstaclePositionIndex[index] = obstaclePositionIndex;
                cellObstacleDistance[index]      = obstacleDistance;

                int targetPositionIndex;
                float targetDistance;
                NearestPosition(targetPositions, position, out targetPositionIndex, out targetDistance);
                cellTargetPositionIndex[index] = targetPositionIndex;

                cellIndices[index] = index;
            }
            
            /// 
            /// 其次执行:合计被考虑到的实际索引的对齐和间隔,并保存我们正在保存单元的第一个值的索引
            /// 
            /// 单元索引
            /// 索引
            public void ExecuteNext(int cellIndex, int index)
            {
                cellCount[cellIndex]      += 1;
                cellAlignment[cellIndex]  = cellAlignment[cellIndex] + cellAlignment[index];
                cellSeparation[cellIndex] = cellSeparation[cellIndex] + cellSeparation[index];
                cellIndices[index]        = cellIndex;
            }
        }

        /// 
        /// 导航:这里读取之前计算的大群信息,再基于使用标准集群蜂拥算法新计算出来的导向,
        /// 用来更新每一个大群的本地位置
        /// 
        [BurstCompile]
        [RequireComponentTag(typeof(Boid))]
        struct Steer : IJobForEachWithEntity<LocalToWorld>
        {
            public float                                                       dt; 
            [ReadOnly] public Boid                                             settings;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int>     cellIndices;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3>  targetPositions;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3>  obstaclePositions;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3>  cellAlignment;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3>  cellSeparation;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int>     cellObstaclePositionIndex;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float>   cellObstacleDistance;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int>     cellTargetPositionIndex;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int>     cellCount;

            public void Execute(Entity entity, int index, ref LocalToWorld localToWorld)
            {
                // temporarily storing the values for code readability
                //为了代码的可读性缓存这些值
                var forward                           = localToWorld.Forward;
                var currentPosition                   = localToWorld.Position;
                var cellIndex                         = cellIndices[index];
                var neighborCount                     = cellCount[cellIndex];
                var alignment                         = cellAlignment[cellIndex];
                var separation                        = cellSeparation[cellIndex];
                var nearestObstacleDistance           = cellObstacleDistance[cellIndex];
                var nearestObstaclePositionIndex      = cellObstaclePositionIndex[cellIndex];
                var nearestTargetPositionIndex        = cellTargetPositionIndex[cellIndex];
                var nearestObstaclePosition           = obstaclePositions[nearestObstaclePositionIndex];
                var nearestTargetPosition             = targetPositions[nearestTargetPositionIndex];

                //基于集群模拟算法的导航计算
                var obstacleSteering                  = currentPosition - nearestObstaclePosition;
                var avoidObstacleHeading              = (nearestObstaclePosition + math.normalizesafe(obstacleSteering)
                                                        * settings.ObstacleAversionDistance)- currentPosition;
                var targetHeading                     = settings.TargetWeight
                                                        * math.normalizesafe(nearestTargetPosition - currentPosition);
                var nearestObstacleDistanceFromRadius = nearestObstacleDistance - settings.ObstacleAversionDistance;
                var alignmentResult                   = settings.AlignmentWeight
                                                        * math.normalizesafe((alignment/neighborCount)-forward);
                var separationResult                  = settings.SeparationWeight
                                                        * math.normalizesafe((currentPosition * neighborCount) - separation);
                var normalHeading                     = math.normalizesafe(alignmentResult + separationResult + targetHeading);
                var targetForward                     = math.select(normalHeading, avoidObstacleHeading, nearestObstacleDistanceFromRadius < 0);
                var nextHeading                       = math.normalizesafe(forward + dt*(targetForward-forward));

                //基于新的导向更新本地位置
                localToWorld = new LocalToWorld
                {
                    Value = float4x4.TRS(
                        new float3(localToWorld.Position + (nextHeading * settings.MoveSpeed * dt)),
                        quaternion.LookRotationSafe(nextHeading, math.up()),
                        new float3(1.0f, 1.0f, 1.0f))
                };
            }
        }
        /// 
        /// 停止运行的时候释放上一帧的哈希表
        /// 
        protected override void OnStopRunning()
        {
            for (var i = 0; i < m_PrevFrameHashmaps.Count; ++i)
            {
                m_PrevFrameHashmaps[i].Dispose();
            }
            m_PrevFrameHashmaps.Clear();
        }

        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            EntityManager.GetAllUniqueSharedComponentData(m_UniqueTypes);

            var obstacleCount = m_ObstacleQuery.CalculateEntityCount();
            var targetCount = m_TargetQuery.CalculateEntityCount();

            ///暂时还不能在哈希表上调用释放内存定语([DeallocateOnJobCompletion]),
            ///所以在这里手动释放上一次迭代循环产生的哈希
            for (int i = 0; i < m_PrevFrameHashmaps.Count; ++i)
            {
                m_PrevFrameHashmaps[i].Dispose();
            }
            m_PrevFrameHashmaps.Clear();

            ///每一个大群的变体代表一个不同的SharedComponentData值,并且是独立的
            ///意味着同一种变体的群只与别的变体群互动。因此这个循环单独处理每一个变体类型
            for (int boidVariantIndex = 0; boidVariantIndex < m_UniqueTypes.Count; boidVariantIndex++)
            {
                var settings = m_UniqueTypes[boidVariantIndex];
                m_BoidQuery.SetFilter(settings);
                var boidCount = m_BoidQuery.CalculateEntityCount();
                
                if (boidCount == 0)
                {
                    ///如果变体不包含任何大群,则提前跳过当前循环。
                    /// 举个栗子,变体0总是会跳过,因为它代表默认的,未初始化的
                    /// 大群结构体并没有在这个案例里出现
                    continue;
                }

                ///下面计算相邻大群单元之间的空间
                /// 注意:使用的是一个稀疏的网格而不是密集带边界的网格,
                /// 所以这里没有预定义的空间边界
                var hashMap                   = new NativeMultiHashMap<int,int>(boidCount,Allocator.TempJob);

                var cellIndices               = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                var cellObstaclePositionIndex = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                var cellTargetPositionIndex   = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                
                var cellCount                 = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);

                var cellObstacleDistance      = new NativeArray<float>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                var cellAlignment             = new NativeArray<float3>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                var cellSeparation            = new NativeArray<float3>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                
                var copyTargetPositions       = new NativeArray<float3>(targetCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
                var copyObstaclePositions     = new NativeArray<float3>(obstacleCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);

                ///接下来的任务全部并行,因为在安排任务的时候同一个JobHandle被传递给它们的输入依赖,
                ///因此它们可以任意顺序运行(或并发运行),并发是它们任务安排的属性,而不是任务结构体本身
                var initialCellAlignmentJob = new CopyHeadings
                {
                    headings = cellAlignment
                };
                var initialCellAlignmentJobHandle = initialCellAlignmentJob.Schedule(m_BoidQuery, inputDeps);

                var initialCellSeparationJob = new CopyPositions
                {
                    positions = cellSeparation
                };
                var initialCellSeparationJobHandle = initialCellSeparationJob.Schedule(m_BoidQuery, inputDeps);

                var copyTargetPositionsJob = new CopyPositions
                {
                    positions = copyTargetPositions
                };
                var copyTargetPositionsJobHandle = copyTargetPositionsJob.Schedule(m_TargetQuery, inputDeps);

                var copyObstaclePositionsJob = new CopyPositions
                {
                    positions = copyObstaclePositions
                };
                var copyObstaclePositionsJobHandle = copyObstaclePositionsJob.Schedule(m_ObstacleQuery, inputDeps);

                ///暂时还不能在哈希表上调用释放内存定语([DeallocateOnJobCompletion]),
                /// 所以把已解决的哈希添加到列表,这样它们就能在接下来的单元任务中使用了,并且也能直接清除
                m_PrevFrameHashmaps.Add(hashMap);

                // setting up the jobs for position and cell count
                ///为位置和单元数量设置任务
                var hashPositionsJob = new HashPositions
                {
                    hashMap        = hashMap.AsParallelWriter(),
                    cellRadius     = settings.CellRadius
                };
                var hashPositionsJobHandle = hashPositionsJob.Schedule(m_BoidQuery, inputDeps);

                var initialCellCountJob = new MemsetNativeArray<int>
                {
                    Source = cellCount,
                    Value  = 1
                };
                var initialCellCountJobHandle = initialCellCountJob.Schedule(boidCount, 64, inputDeps);

                var initialCellBarrierJobHandle = JobHandle.CombineDependencies(initialCellAlignmentJobHandle, initialCellSeparationJobHandle, initialCellCountJobHandle);
                var copyTargetObstacleBarrierJobHandle = JobHandle.CombineDependencies(copyTargetPositionsJobHandle, copyObstaclePositionsJobHandle);
                var mergeCellsBarrierJobHandle = JobHandle.CombineDependencies(hashPositionsJobHandle, initialCellBarrierJobHandle, copyTargetObstacleBarrierJobHandle);
                //合并所有单元
                var mergeCellsJob = new MergeCells
                {
                    cellIndices               = cellIndices,
                    cellAlignment             = cellAlignment,
                    cellSeparation            = cellSeparation,
                    cellObstacleDistance      = cellObstacleDistance,
                    cellObstaclePositionIndex = cellObstaclePositionIndex,
                    cellTargetPositionIndex   = cellTargetPositionIndex,
                    cellCount                 = cellCount,
                    targetPositions           = copyTargetPositions,
                    obstaclePositions         = copyObstaclePositions
                };
                var mergeCellsJobHandle = mergeCellsJob.Schedule(hashMap,64,mergeCellsBarrierJobHandle);
                //导航
                var steerJob = new Steer
                {
                    cellIndices               = cellIndices,
                    settings                  = settings,
                    cellAlignment             = cellAlignment,
                    cellSeparation            = cellSeparation,
                    cellObstacleDistance      = cellObstacleDistance,
                    cellObstaclePositionIndex = cellObstaclePositionIndex,
                    cellTargetPositionIndex   = cellTargetPositionIndex,
                    cellCount                 = cellCount,
                    targetPositions           = copyTargetPositions,
                    obstaclePositions         = copyObstaclePositions,
                    dt                        = Time.deltaTime,
                };
                var steerJobHandle = steerJob.Schedule(m_BoidQuery, mergeCellsJobHandle);

                inputDeps = steerJobHandle;
                m_BoidQuery.AddDependency(inputDeps);
            }
            m_UniqueTypes.Clear();

            return inputDeps;
        }

        protected override void OnCreate()
        {
            ///查询大群
            m_BoidQuery = GetEntityQuery(new EntityQueryDesc
            {
                All = new [] { ComponentType.ReadOnly<Boid>(), ComponentType.ReadWrite<LocalToWorld>() },
            });
            //目标大群
            m_TargetQuery = GetEntityQuery(new EntityQueryDesc
            {
                All = new [] { ComponentType.ReadOnly<BoidTarget>(), ComponentType.ReadOnly<LocalToWorld>() },
            });
            //对手大群
            m_ObstacleQuery = GetEntityQuery(new EntityQueryDesc
            {
                All = new [] { ComponentType.ReadOnly<BoidObstacle>(), ComponentType.ReadOnly<LocalToWorld>() },
            });
        }
    }
}

这个大群系统是最复杂的,至于生成Spawn就相对比较简单了,都是之前几篇解析过的实体生成实体,所以这里就不用再多讲。

小结

大群的复杂在于其算法,这种比较大量的操作,先合并所有单元,再进行导航的方式是比较高效的。即使达到100000的量也感觉不到卡顿,完全展示出了ECS的强大速度优势,在拥有大量实体的项目中性能得到极大优化。
Boid:

ECS Scripts Interface1
Entity Boid IConvertGameObjectToEntity
Component Boid IComponentData
System BoidSystem JobComponentSystem

DOTS 逻辑图表

BoidSystem流程大体如下:

GetEntityQuery
GetEntityQuery
GetEntityQuery
Schedule
Schedule
Schedule
Schedule
hashMap
Schedule
OnCreate
m_BoidQuery
m_TargetQuery
m_ObstacleQuery
OnUpdate
CopyHeadings
CopyPositions
HashPositions
MergeCells
Steer

太混乱了,拆分一下:
1.OnCreate:

GetEntityQuery
GetEntityQuery
GetEntityQuery
OnCreate
m_BoidQuery
m_TargetQuery
m_ObstacleQuery
OnUpdate

2.OnUpdate

Schedule
Schedule
Schedule
Schedule
hashMap
Schedule
OnUpdate
CopyHeadings
CopyPositions
m_BoidQuery
m_TargetQuery
m_ObstacleQuery
HashPositions
MergeCells
Steer

还是乱,再做减法:

Schedule
Schedule
Schedule
Schedule
Schedule
OnUpdate
CopyHeadings
CopyPositions
HashPositions
MergeCells
Steer

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)