Notifications
Article
基于Unity2019最新ECS架构开发MMO游戏笔记0
Published 4 months ago
182
0
游戏开发+ECS学习=全记录

关于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控制器。这些是我的个人见解,大体上应该没有什么问题吧。
总体的意思就是用数据来驱动实体的系统,可以这么理解吗?Whatever,反正我是这么狭隘的认为的。
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 ,面向数据的堆栈技术,我们会跑得更快;

官方代码示例

啰里啰唆了这么多,不如直接上代码来的直观,前戏虽有必要,但是我的大刀早已饥渴难耐!
我是从官方案例开始研究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示例](https://github.com/Unity-Technologies/EntityComponentSystemSamples.git)下载代码
if(已经下载了)continue;
2、把ECSSamples添加Unity Hub项目中,并用对应的Unity版本打开:ECSSamples
3、在Assets目录下找到HelloCube/1. ForEach,并打开ForEach场景

1. ForEach

场景里总共有四个游戏对象,呃,等下就不是那么回事儿了!一切都源于一个神奇的脚本,我们来找到它:
- Main Camera ……主摄像机
- Directional Light ……光源
- RotatingCube ……旋转的方块
- ChildCube ……子方块
打开RotatingCube上挂的ConvertToEntity脚本,这就是我们要找的神奇脚本,它将Unity的游戏对象GameObject转化成Entity,从而让游戏运行更加高效,详细的原理看一开始推荐国外大佬那篇文章。
下面我们来看看ConvertToEntity脚本是如何完成这项工作的:
/// <summary> /// 将游戏对象转化成ECS的实体Entity /// </summary> [DisallowMultipleComponent]//不允许多组件 public class ConvertToEntity : MonoBehaviour { /// <summary> /// 转化模式枚举 /// </summary> public enum Mode { ConvertAndDestroy,//转化并摧毁,该模式下GameObject在被转化成实体Entity后原游戏对象被摧毁 ConvertAndInjectGameObject//转化并注入游戏对象,这个模式不会摧毁原来的游戏对象 } /// <summary> /// 转化模式 /// </summary> 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); } } /// <summary> /// 注入原始组件 /// </summary> /// <param name="entityManager">实体管理器</param> /// <param name="entity">实体</param> /// <param name="transform">变化组件</param> 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); } } /// <summary> /// 添加递归,把根节点下所有子节点都添加到实体管理器中 /// </summary> /// <param name="manager">实体管理器</param> /// <param name="transform">变化组件</param> 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); } /// <summary> /// 注入原始组件 /// </summary> /// <param name="srcGameObjectWorld">源游戏对象世界</param> /// <param name="simulationWorld">模拟世界,实体管理器所操控的实体世界</param> /// <param name="transform">变化组件</param> /// <returns>循环递归直到层级中的所有对象都注入完,True:根节点</returns> 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; } /// <summary> /// 转化层级 /// </summary> /// <param name="root">根节点</param> 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); } } /// <summary> /// 转化并注入源 /// </summary> /// <param name="root">根节点</param> 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; /// <summary> /// 旋转速度写入遍历中 /// </summary> [RequiresEntityConversion]//必须实体转化 public class RotationSpeedAuthoring_ForEach : MonoBehaviour, IConvertGameObjectToEntity {//这里继承了IConvertGameObjectToEntity接口,并实现了Convert转化方法,该方法会自动调用 /// <summary> /// 旋转速度:度每秒 °/s /// </summary> 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) /// <summary> /// 这里的Mono数据在实体上被转化成组件数据,我们特意把一个在编辑器上好表述的数据(度)转化成实时数据表述(弧) /// </summary>我们只需实现这个方法,并将数据传入即可,传入的数据会在运行的时候被使用 /// <param name="entity">实体</param> /// <param name="dstManager">目标实体管理器</param> /// <param name="conversionSystem">转化系统</param> 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: ```javascript using System; using Unity.Entities;//依赖实体 // Serializable attribute is for editor support. /// <summary> /// 这个脚本继承IComponentData组件,它的功能就是储存数据 /// </summary> [Serializable]//可序列化特性是为了支持编辑器 public struct RotationSpeed_ForEach : IComponentData { /// <summary> /// 每秒旋转的弧度 /// </summary> public float RadiansPerSecond; }
这个脚本最纯粹,它就是储存数据给System使用,下面我们来看系统RotationSpeedSystem_ForEach:
/// <summary> ///这个系统脚本会每帧更新所有场景中同时带有RotationSpeed_ForEach和Rotation的实体 /// </summary> public class RotationSpeedSystem_ForEach : ComponentSystem { /// <summary> /// 重写父类ComponentSystem的方法,每帧调用 /// </summary> 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)); }); } }
通过上面这个对照表是不是理清开发思路了?下面通过逻辑图表来看看解耦的模式:

学习计划

真希望这里可以使用Markdown语言,支持mermaid,可以导入md就更好了!
Tags:
CloudHu
Game Developer - Manager
2
Comments