ECS官方示例1. ForEach

31 篇文章 3 订阅

关于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脚本是如何完成这项工作的:

    /// <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:

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:

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.
/// <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));
        });
    }
}

小结

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

ECSScripts继承
EntityRotationSpeedAuthoring_ForEachIConvertGameObjectToEntity
ComponentRotationSpeed_ForEachIComponentData
SystemRotationSpeedSystem_ForEachComponentSystem

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

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

  • 15
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
Unity ECS(Entity Component System)是一种用于高性能游戏开发的系统,通过使用 ECS 可以优化游戏的性能并提高游戏开发的效率。下面是一个简单的入门教程: 1. 创建一个 ECS 项目 首先,在 Unity 中创建一个新项目。在创建项目时,选择“High Definition RP(Preview)”模板,这将为你创建一个使用 ECS 的项目。 2. 创建实体(Entity) 在 ECS 中,实体是一个简单的标识符,用于标识游戏中的对象。在 Unity 中,可以通过使用实体组件(Entity Component)来创建实体。在 Unity 中,可以使用以下代码创建一个实体: ```csharp Entity entity = EntityManager.CreateEntity(); ``` 3. 创建组件(Component) 组件是实体的属性,它们包含实体的数据和行为。在 Unity ECS 中,组件是 C# 类,它们必须继承自 IComponentData 接口。以下是一个示例组件: ```csharp public struct Position : IComponentData { public float3 Value; } ``` 在这个示例中,Position 是一个包含 float3 类型变量 Value 的组件。 4. 添加组件到实体 要将组件添加到实体中,可以使用 EntityManager 的 AddComponent 方法。以下是一个示例代码,将 Position 组件添加到 entity 实体中: ```csharp EntityManager.AddComponentData(entity, new Position { Value = new float3(0, 0, 0) }); ``` 5. 创建系统(System) 系统是用于处理实体和组件的逻辑的代码。在 ECS 中,系统是 C# 类,它们必须继承自 ComponentSystem 类。以下是一个示例系统代码: ```csharp public class MoveSystem : ComponentSystem { protected override void OnUpdate() { Entities.ForEach((ref Position position) => { position.Value += new float3(0, 1, 0); }); } } ``` 在这个示例中,MoveSystem 是一个系统,它会处理所有带有 Position 组件的实体,并将它们的位置向上移动一个单位。 6. 将系统添加到 ECS 要将系统添加到 ECS 中,需要在 Unity 中创建一个 GameObject,并将其命名为“ECS Manager”。然后,将 ECS Manager 组件添加到 GameObject 中,并将 MoveSystem 添加到 ECS Manager 中。 7. 运行游戏 现在,可以启动游戏并查看实体和组件的效果。在这个示例中,应该会看到所有带有 Position 组件的实体都向上移动一个单位。 这里只是一个简单的入门教程,Unity ECS 还有很多高级功能和概念需要学习。但是通过上面的步骤,你可以了解到 ECS 的基本概念,并开始使用 ECS 来开发高性能游戏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CloudHu1989

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值