ECS官方示例1. ForEach

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

  • 关于ECS
    • 新的改变
  • 代码示例
    • 1. ForEach
    • 小结
      • ECS 逻辑图表
  • 更新计划
    • 作者的话
  • 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

关于 ECS 我想大家都不陌生了,比较Unity已经强推这么久了,如果你还不知道的话,的确有点孤陋寡闻了(其实我也是才知道的LOL)。我是因为看到MegaCity的视频才了解到ECS的,当时被庞大复杂的场景震撼到了,一直以来Unity官方出的视频都带给我强大的视觉冲击。第一部让我自发宣传的是《Adam》,后面连续出了续集2和3,然后戛然而止,却意犹未尽。Unity不开发游戏真是太可惜了,这样好的创意和功力,无论做什么都会让人拍手称赞的。至于最新的《异教徒》,让我不知道说什么好了,真希望赶快放续集,这个视频创意比亚当三部曲还要赞,千万别只出三部啊!
跑题了,我是发散性思维,超级喜欢跑题。话说回来,ECS虽然现在还没有完善,但是真是值得期待。我陆陆续续在网上找了很多相关的知识来看,越看越觉得有意思,不知不觉中越陷越深了,原本只是看了一个视频而已。
我理解的ECS是相对于OOP的,OOP就是Object Oriented Program啦,面向对象编程,格言就是Everything is Object,一切都是对象,这种设计其实是来源于现实世界的,道法自然嘛,整个虚拟世界都是在模仿现实世界,编程思维和设计也不例外。
面向对象老生常谈了,我觉得没有必要多讲了,就说说ECS吧,E是Entity实体,相对于Object对象的存在;C是Component组件,其实我认为用Data来表达更好一些,C代表的就是数据;S是System系统,相对于原来的Controller控制器。
总体的意思就是用数据来驱动实体的系统,可以这么理解,反正我是这么狭隘的认为的。
ECS相对于OOP有什么优势吗?
国外的大佬写了教学和性能比对的文章Unity* 实体组件系统 (ECS)、C# 作业系统和突发编译器入门水木本源大佬转了过来,看了实在受益匪浅。
说白了,ECS比OOP更快,而且不是一般的快,在大量测试的情况下快了上百倍。

新的改变

我总结了ECS带来的一些改变:

  1. 全新的编程设计 ,原来所有的都是对象,现在所有都是实体,我们原来是用控制器来操控对象,现在用System来操控Component里的数据,而数据通过实体表现出来;
  2. 大数据将以 大量实体 进行展示,因为速度的突破,不用担心渲染跟不上了,万人同屏不卡顿;
  3. 迎接 新一代引擎 架构,与ECS相伴的还有Jobs(C#任务系统),Burst编译器,三者构成DOTS(新的游戏开发架构);
  4. 全新的 编辑器 ,Unity的编辑器会重做,原来那套OOP的编辑器是看不见Entity的,毕竟已经不是对象了,至于做成什么样子,值得期待;
  5. 新的物理引擎Havok Physics ,Unity为了ECS还特别收购了这家公司;
  6. 看不见的 ECS底层架构 ,我们现在虽然已经可以使用了,但是底层还在完善,利用 DOTS Data-Oriented Tech Stack ,面向数据的堆栈技术,我们会跑得更快;

代码示例

啰里啰唆了这么多,不如直接上代码来的直观,前戏虽有必要,但是我的大刀早已饥渴难耐!
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目录下找到HelloCube/1. ForEach,并打开ForEach场景

1. ForEach

场景里总共有四个游戏对象,呃,等下就不是那么回事儿了!一切都源于一个神奇的脚本,我们来找到它:

  • Main Camera ……主摄像机
  • Directional Light……光源
  • RotatingCube……旋转的方块
    • ChildCube……子方块

打开RotatingCube上挂的ConvertToEntity脚本,这就是我们要找的神奇脚本,它将Unity的游戏对象GameObject转化成Entity,从而让游戏运行更加高效,详细的原理看一开始推荐国外大佬那篇文章。
下面我们来看看ConvertToEntity脚本是如何完成这项工作的:

    /// 
    /// 将游戏对象转化成ECS的实体Entity
    /// 
    [DisallowMultipleComponent]//不允许多组件
    public class ConvertToEntity : MonoBehaviour
    {
        /// 
        /// 转化模式枚举
        /// 
        public enum Mode
        {
            ConvertAndDestroy,//转化并摧毁,该模式下GameObject在被转化成实体Entity后原游戏对象被摧毁
            ConvertAndInjectGameObject//转化并注入游戏对象,这个模式不会摧毁原来的游戏对象
        }
        /// 
        /// 转化模式
        /// 
        public Mode ConversionMode;
        
        void Awake()
        {
            if (World.Active != null)//这里新增了世界的概念,目的是划分OOP世界和ECS世界,游戏对象只存在于面向对象的世界中,实体同理
            {
                // Root ConvertToEntity is responsible for converting the whole hierarchy
                //根节点的转化脚本负责转化整个层级,举个栗子:这个脚本挂在父节点RotatingCube上,那么子节点ChildCube也会被转化成实体
                if (transform.parent != null && transform.parent.GetComponentInParent<ConvertToEntity>() != null)
                    return;//一个层级中只能在根节点上挂这个脚本,否则会在这里返回出去
                //根据不同的模式调用不同的方法
                if (ConversionMode == Mode.ConvertAndDestroy)
                    ConvertHierarchy(gameObject);
                else
                    ConvertAndInjectOriginal(gameObject);
            }
            else
            {
                UnityEngine.Debug.LogWarning("ConvertEntity failed because there was no Active World", this);
            }
        }
        /// 
        /// 注入原始组件
        /// 
        /// 实体管理器
        /// 实体
        /// 变化组件
        static void InjectOriginalComponents(EntityManager entityManager, Entity entity, Transform transform)
        {
            foreach (var com in transform.GetComponents<Component>())
            {//这里遍历所有组件,并将实体注入到原始组件中,详询底层的代码
                if (com is GameObjectEntity || com is ConvertToEntity || com is ComponentDataProxyBase)
                    continue;
                //我们可以不关心更深层次的实现,只需了解这个表层的脚本功能即可,需要使用的时候把这个脚本当作组件来用
                entityManager.AddComponentObject(entity, com);
            }
        }
        /// 
        /// 添加递归,把根节点下所有子节点都添加到实体管理器中
        /// 
        /// 实体管理器
        /// 变化组件
        public static void AddRecurse(EntityManager manager, Transform transform)
        {
            GameObjectEntity.AddToEntityManager(manager, transform.gameObject);
            
            var convert = transform.GetComponent<ConvertToEntity>();
            if (convert != null && convert.ConversionMode == Mode.ConvertAndInjectGameObject)
                return;
                
            foreach (Transform child in transform)
                AddRecurse(manager, child);
        }
        /// 
        /// 注入原始组件
        /// 
        /// 源游戏对象世界
        /// 模拟世界,实体管理器所操控的实体世界
        /// 变化组件
        /// 循环递归直到层级中的所有对象都注入完,True:根节点
        public static bool InjectOriginalComponents(World srcGameObjectWorld, EntityManager simulationWorld, Transform transform)
        {
            var convert = transform.GetComponent<ConvertToEntity>();

            if (convert != null && convert.ConversionMode == Mode.ConvertAndInjectGameObject)
            {
                var entity = GameObjectConversionUtility.GameObjectToConvertedEntity(srcGameObjectWorld, transform.gameObject);
                InjectOriginalComponents(simulationWorld, entity, transform);
                transform.parent = null;
                return true;
            }
            
            for (int i = 0; i < transform.childCount;)
            {
                if (!InjectOriginalComponents(srcGameObjectWorld, simulationWorld, transform.GetChild(i)))
                    i++;
            }

            return false;
        }
        /// 
        /// 转化层级
        /// 
        /// 根节点
        public static void ConvertHierarchy(GameObject root)
        {
            using (var gameObjectWorld = new GameObjectConversionSettings(World.Active, GameObjectConversionUtility.ConversionFlags.AssignName).CreateConversionWorld())
            {
                AddRecurse(gameObjectWorld.EntityManager, root.transform);
                //关键方法,此游戏对象转化工具会将游戏对象世界转化成实体世界
                GameObjectConversionUtility.Convert(gameObjectWorld);

                InjectOriginalComponents(gameObjectWorld, World.Active.EntityManager, root.transform);

                Destroy(root);
            }
        }

        /// 
        /// 转化并注入源
        /// 
        /// 根节点
        public static void ConvertAndInjectOriginal(GameObject root)
        {
            using (var gameObjectWorld = new GameObjectConversionSettings(World.Active, GameObjectConversionUtility.ConversionFlags.AssignName).CreateConversionWorld())
            {
                GameObjectEntity.AddToEntityManager(gameObjectWorld.EntityManager, root);

                GameObjectConversionUtility.Convert(gameObjectWorld);

                var entity =GameObjectConversionUtility.GameObjectToConvertedEntity(gameObjectWorld, root);
                InjectOriginalComponents(World.Active.EntityManager, entity, root.transform);
            }
        }
    }

代码添加了详细注释,相信大家都大概明白了,就像注释里面说的那样,我们不必关心底层实现,毕竟精力有限,知道怎么使用即可。我们只需将这个脚本添加到游戏对象上,就可以把原来的游戏对象根据需求转化成实体。
World:世界这个概念以后会有很多应用场景,think about 多元世界 or 平行宇宙!
RotatingCube上还有一个负责传入旋转速度的脚本RotationSpeedAuthoring_ForEach:

using Unity.Entities;//依赖实体
using Unity.Mathematics;//数学
using UnityEngine;

/// 
/// 旋转速度写入遍历中
/// 
[RequiresEntityConversion]//必须实体转化
public class RotationSpeedAuthoring_ForEach : MonoBehaviour, IConvertGameObjectToEntity
{//这里继承了IConvertGameObjectToEntity接口,并实现了Convert转化方法,该方法会自动调用
    /// 
    /// 旋转速度:度每秒  °/s
    /// 
    public float DegreesPerSecond;

    // The MonoBehaviour data is converted to ComponentData on the entity.
    // We are specifically transforming from a good editor representation of the data (Represented in degrees)
    // To a good runtime representation (Represented in radians)
    /// 
    /// 这里的Mono数据在实体上被转化成组件数据,我们特意把一个在编辑器上好表述的数据(度)转化成实时数据表述(弧)
    /// 我们只需实现这个方法,并将数据传入即可,传入的数据会在运行的时候被使用
    /// 实体
    /// 目标实体管理器
    /// 转化系统
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var data = new RotationSpeed_ForEach { RadiansPerSecond = math.radians(DegreesPerSecond) };
        dstManager.AddComponentData(entity, data);
    }
}

第一个脚本其实我们可以不管,直接拖拽使用即可,我们需要编写的是这个脚本,它需要引入实体和数学两个组件才能工作。我们会把方块旋转的速度通过这里传递给Component数据组件,该数据会在那里储存,并在System中被调用,在我们看完所有脚本之后再理清ECS的开发思路。下面我们来看组件脚本RotationSpeed_ForEach:

using System;
using Unity.Entities;//依赖实体

// Serializable attribute is for editor support.
/// 
/// 这个脚本继承IComponentData组件,它的功能就是储存数据
/// 
[Serializable]//可序列化特性是为了支持编辑器
public struct RotationSpeed_ForEach : IComponentData
{
    /// 
    /// 每秒旋转的弧度
    /// 
    public float RadiansPerSecond;
}

这个脚本最纯粹,它就是储存数据给System使用,下面我们来看系统RotationSpeedSystem_ForEach:

using Unity.Entities;//依赖实体和数学
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

// This system updates all entities in the scene with both a RotationSpeed_ForEach and Rotation component.
/// 
///这个系统脚本会每帧更新所有场景中同时带有RotationSpeed_ForEach和Rotation的实体
/// 
public class RotationSpeedSystem_ForEach : ComponentSystem
{
    /// 
    /// 重写父类ComponentSystem的方法,每帧调用
    /// 
    protected override void OnUpdate()
    {
        // Entities.ForEach processes each set of ComponentData on the main thread. This is not the recommended
        // method for best performance. However, we start with it here to demonstrate the clearer separation
        // between ComponentSystem Update (logic) and ComponentData (data).
        // There is no update logic on the individual ComponentData.
        ///Entities.ForEach这个方法会处理在主线程上的每一组组件数据(ComponentData)
        /// 这不是推荐的最佳性能实现方法。但是我们从这里开始表明更加干净的逻辑和数据分离,所谓解耦。
        /// 没有任何更新逻辑会出现在单独的数据组件上。
        Entities.ForEach((ref RotationSpeed_ForEach rotationSpeed, ref Rotation rotation) =>
        {
            var deltaTime = Time.deltaTime;
            rotation.Value = math.mul(math.normalize(rotation.Value),
                quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
        });
    }
}

小结

我们来看看这个小案例是如何实现ECS架构的:

ECS Scripts 继承
Entity RotationSpeedAuthoring_ForEach IConvertGameObjectToEntity
Component RotationSpeed_ForEach IComponentData
System RotationSpeedSystem_ForEach ComponentSystem

通过上面这个对照表是不是理清开发思路了?下面通过逻辑图表来看看解耦的模式:

ECS 逻辑图表

Entity Component System 你好!Component, 数据交给你储存? 数据给你操控,System? 转起来,谢谢! 我不想转,谢谢! Entity转了很长时间 …… …… Entity Component System

我大概是这么理解的,建议把Component换成Data,更容易看懂。:

Data
Entity
Component
System
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官方示例1. ForEach_第1张图片

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