Unity 革命性技术DOST入门二 ECS简单使用介绍

作者介绍:铸梦xy。IT公司技术合伙人,IT高级讲师,资深Unity架构师,铸梦之路系列课程创始人。

上一篇:Unity 革命性技术DOST入门一 使用介绍

1.什么是ECS?

ECS是一种软件架构模式,由三个元素组成:实体(Entity),组件(Component)和系统(System)(看起来和MVC很相似)。游戏程序分为这三个主要元素,并且通过定义每个系统的责任和关系来管理游戏。
实体代表游戏世界中的事物。实体本身没有特定功能,它们将会被组件填充来成为一个实体。

组件是附加到事物的数据。重点不是对象,而是数据,没有办法操纵它。比如操作游戏的角色时,位置,速度和体力等每个状态都将成为一个组件部分,并与称为“角色”实体相关联, 我们在修改角色状态时只需要修改其单独的数据组件即可,不会触碰到角色的逻辑。

下面为官方介绍:

实体组件系统(ECS)是Unity面向数据的技术堆栈的核心。顾名思义,ECS包含三个主要部分:

  • Entities:填充您的游戏或程序的实体或事物。
  • Components:与您的实体相关联的数据,但是应该有数据本身而不是实体来组织。(这种组织上的差异正是面向对象和面向数据的设计之间的关键差异之一)
  • Systems:主要逻辑所在,是把Components的数据从当前状态转换为下一个状态的逻辑,例如,一个system可能会通过他们的速度乘以从前一帧到这一帧的时间间隔来更新所有的移动中的entities的位置。

这里为实际开发中理解:

  • E: Entity 一个不代表任何意义的实体(可以理解为Unity里的一个空的GameObject)
  • C: Component 一个只包含数据的组件(可以理解为Unity的一个自定义组件,里面只有数据,没有任何方法)
  • S: System 一个用来处理数据的系统(可以理解为Unity的一个自定义组件,里面只有方法,没有任何数据)

2.ECS为何快?

要想知道ECS为和快,我们就需要去了解一下 CPU与缓存(Cache)

首先我们先来了解一下CPU读取数据时的操作,首先CPU会先从自己的缓存中去查找,如下图,若缓存中没有找到需要的数据,则会去内存中查找,CPU在内存中找到数据后就会将新数据存放在缓存(Cache)当中。但是CPU访问内存的速度会比访问Cache的速度慢100倍,因此提高缓存命中率,避免频繁去内存会大大提高性能。因此我们应该尽量使用数组,避免数据分散,尽量连续的进行处理。

最常见的例子就是在数据量小的情况下遍历数组会比遍历List快上很多,因为数组是有序的,而列表则是分散的,无序的。
在这里插入图片描述
在我们的传统模式中,假设我们现在想要旋转并移动场景中的一个物体,那么我们会修改它的Position和Rotation,但是使用的时候整个Transform都会被加到缓存当中,而Transform中有很多我们不需要的属性占用了很大的缓存空间,如下图,所以就造成了严重的内存浪费。若有上千万个这样的方块,那么我们缓存中可能就会存在超50%以上的内存垃圾,在加上这些属性在内存中的排放都是无序的,从而导致缓存的数据命中率大大的降低,导致我们的性能下降。
在这里插入图片描述
使用ECS就可以解决上述的这些问题,从而提高性能。

因为我们ECS是数据组件化的,需要哪些数据,就声明哪些数据,不会造成上面那样严重的内存浪费!

比如我们同样需要修改位移和旋转,但是我们只需要声明一个float3数据和一个float4的数据即可解决,与上面相比而言, 我们占的内存,微乎其微!

3.ECS框架生命周期示意图

相信有不少人在初学ECS时,只知道在System.Update中编写逻辑就行,知道Update会执行,但可能不明白他是怎么被调用的,于是博主去剖析了一下Entities源码,得到了下面这一张运行流程图。
在这里插入图片描述
首先在游戏启动时,World脚本开始运行,初始化Entity世界。

接着World会初始化EntityManager脚本,也就ECS中的 E ,而EntityManager脚本负责所有Entity的管理,包括创建、销毁、设置数据等。

然后 World在初始化或销毁的时候又在 AddSystem()DestroySystem() 中调用 ComponentSystemBase基类的 OnCreate() 和OnDestroy(),这里由于咱们的System是继承自ComponentSystem的,而ComponentSystem又是继承于ComponentSystemBase基类的,所以咱们的System得到了初始化。

AddSystem和DestoroySystem的调用由系统自动分配,AddSystem在World初始化时自动调用,DestoroySystem则在World生命周期结束时自动调用,不受其他特性控制。

System脚本生命周期

在这里插入图片描述

方法名触发时机
OnCreateSystem被创建的时候调用
OnStartRunning在第一次OnUpdate之前和System恢复运行的时候调用
OnUpdateSystem的Enabled为true时,每帧调用
OnStopRunningSystem的Enabled为false时,或者没有找到相对应的Entity会调用,OnDestroy前也会调用
OnDestroySystem被销毁的时候调用

紧接着World 在初始化完成之后 又会在Update中调用InitializationSystemGroupSimulationSystemGroupPresentationSystemGroup这三个脚本的 Update()

        public void Update()
        {
            GetExistingSystem<InitializationSystemGroup>()?.Update();
            GetExistingSystem<SimulationSystemGroup>()?.Update();
            GetExistingSystem<PresentationSystemGroup>()?.Update();
        #if ENABLE_UNITY_COLLECTIONS_CHECKS
            Assert.IsTrue(EntityManager.GetBuffer<WorldTimeQueue>(TimeSingleton).Length == 0, "PushTime without matching PopTime");
        #endif
        }

而上面三个系统组脚本在ECS中被分切为三个执行顺序

逻辑群组优先级
InitializationSystemGroup1
SimulationSystemGroup2
PresentationSystemGroup3

各自作用如下图
在这里插入图片描述
比如在游戏中我们有三个System,一个用来生成敌人,一个用来移动,还有一个用来处理死亡。而且我们要保证必须先生成敌人,在移动,最后死亡。

所以,当我们想控制自己的System的执行顺序时,可以通过以下三个特性进行控制

名称介绍
UpdateInGroup指定当前System在哪个分组下运行
UpdateBefore指定当前System在哪个System之前执行
UpdateAfter指定当前System在哪个System之后执行

使用方式如下:

[UpdateInGroup(typeof(InitializationSystemGroup))]
public class EnemyCreateSystem : ComponentSystem{}
[UpdateInGroup(typeof(SimulationSystemGroup))]
public class EnemyMoveSystem : ComponentSystem{}
[UpdateInGroup(typeof(PresentationSystemGroup))]
public class EnemyNomalSystem : ComponentSystem{}

注意:该特性只能控制我们的 OnUpdate 更新接口,OnCreate和OnDestroy以及其他的接口由ECS系统内部控制,不受特性控制!

4.ECS使用介绍

EntityManager常用方法介绍
CreateEntity()该方法可以为我们创建一个Entity实体,相当于New GameObjcet
CreateArchetype()该方法可以创建我们的Entity的原型,声名出生时附带的数据组件
SetComponentData()该方法则是可以设置Entity数据组件的初始值
SetSharedComponentData()该方法用来设置共享数据,比如Mesh、Material等
DestroyEntity()该方法用来销毁Enitity实体

下面就以一个实际案例来介绍下ECS的基本使用

1.Components数据组件

纯数据,不含有其他逻辑行为。 例如:旋转速度,缩放大小之类的。

using Unity.Entities;

public struct MoveSpeedComponent : IComponentData
{
    public float mMoveSpeed;//移动速度
}

2. System

主要逻辑所在,根据组件的集合(Enitites)和纯数据(Components)编写对应的逻辑,例如旋转,移动等一些逻辑。

这里我们通过ForEach()方法去遍历所有Entity上的数据组件,并且修改他们的数值!

下面的移动代码种用到了quaternion旋转函数,它是依赖于Unity.Mathematics命名空间的,quaternion为我们提供了极多的关于处理旋转和角度的函数,给我们带来了很大的便利,他为我们提供的欧拉角修改的方式以及四元数修改的方式,对于用不惯四元数修改的旋转的,这无疑是个最好的方式,具体的这里就不一一介绍了。

using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
public class MoveSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        Entities.ForEach((ref MoveSpeedComponent moveCmpt ,ref Translation translation,ref Rotation rotation) =>
        {
            translation.Value.x += moveCmpt.mMoveSpeed * Time.DeltaTime;

            if (translation.Value.x>30)
            {
                //反转移动速度和旋转
                rotation.Value=quaternion.Euler(new float3(0,130,0));
                moveCmpt.mMoveSpeed = -math.abs(moveCmpt.mMoveSpeed);
            }
            else if (translation.Value.x < -30)
            {
                //反转移动速度和旋转
                rotation.Value = quaternion.Euler(new float3(0, -130, 0));
                moveCmpt.mMoveSpeed =math.abs(moveCmpt.mMoveSpeed);
            }
        });
    }
}

3.Entity

最后是Entity启动类,他为我们初始化出了我们的Entity,以及Entity原型的创建和数据设置,这个类我把他挂载到在了相机上,方便我们进行初始化。

可以看到我们通过SetComponentData()方法初始化了我们的旋转、移动速度、和出生位置。


using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Transforms;
using Unity.Rendering;
public class EntityMain : MonoBehaviour
{
    EntityManager mEentityManager;
   [SerializeField]
    private Mesh[] mFishMeshArray;//模型Mesh数组
    [SerializeField]
    private Material[] mFishMaterialArray;//模型材质数组
    void Start()
    {

         mEentityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
        //创建原型
        EntityArchetype entityArchetype = mEentityManager.CreateArchetype(
            typeof(MoveSpeedComponent),
            typeof(Translation),
            //下面三个实体一定要加不然是看不到实例化的物体的
            typeof(RenderBounds),
            typeof(RenderMesh),
            typeof(LocalToWorld),
            //添加旋转数据
            typeof(Rotation)
            );
        //创建实体数组 5000代表就生成5000个
        NativeArray<Entity> entityArray = new NativeArray<Entity>(5000, Allocator.Temp);
        //创建实体
        mEentityManager.CreateEntity(entityArchetype, entityArray);
        for (int i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];
            //设置数据
            mEentityManager.SetComponentData(entity, new Rotation { Value = quaternion.Euler(0, -130, 0) });
            mEentityManager.SetComponentData(entity, new MoveSpeedComponent { mMoveSpeed = UnityEngine.Random.Range(3, 10) });
            mEentityManager.SetComponentData(entity, new Translation { Value = new float3(UnityEngine.Random.Range(-20, 20), UnityEngine.Random.Range(-15, 15), UnityEngine.Random.Range(5, 100)) });
            int index = UnityEngine.Random.Range(0, 3);
            //设置材质和渲染网格
            mEentityManager.SetSharedComponentData(entity, new RenderMesh { mesh = mFishMeshArray[index], material = mFishMaterialArray[index] });
        }
        //一定要记得释放
        entityArray.Dispose();
    }
}

在Entity的Inspector面板我们可以看到我们的数据组件:

我们添加的数据组件都在上面,并且我们修改的数值,也有所变化
在这里插入图片描述

一共有三种鱼,每种材质都不同。
在这里插入图片描述
最后是我们的实际运行效果,性能并不是最优,因为我们只使用了ECS和Burst

并且我们的主要目的是去了解如何使用ECS。

下面是同屏1000条鱼的截图
在这里插入图片描述
下面是 标准性能 下实际运行视屏演示

注意:视屏种的不流畅是录屏软件帧率不够导致的不流畅,并不是因为游戏的帧率不够。

5000只鱼:

DOTS 5000只鱼演示(注意:录屏帧率低导致不流畅)

一万只鱼:

DOTS 10000只鱼演示(注意:录屏帧率低导致不流畅)

15000只鱼:

DOTS 15000千只鱼性能的展示

可以看到当使用三种模型材质的鱼数量一共达到15000只时,并且在位移逻辑和游动动画都在的情况下,我们还能稳定在30多帧,可以说很不错了。

并且我们只是简单写了点代码 开了Burst进行的测试。

意味着我们还有很大的提升空间。

关于ECS的简单使用就到这里了

DOTS入门视屏教程

下一篇:Unity 革命性技术DOST 入门三JobSystem系统

文章来自于铸梦老师,铸梦之路系列课程。
想了解更多框架、帧同步技术、UGUI优化相关技术可在企鹅kt搜索 铸梦xy。

在Vue3中,可以将setup函数从组件文件中分离出来,以提高代码的可维护性和可复用性。分离setup函数可以让组件的逻辑和模板更清晰地分离开来,并且可以更好地组织和重用代码。 要分离setup函数,可以将其定义在一个单独的文件中,然后将其导入到组件文件中。例如,可以创建一个名为"useSetup.js"的文件,并在其中定义setup函数。然后,在组件文件中使用import语句将其导入,并在组件的setup选项中使用。 下面是一个示例: 在"useSetup.js"文件中: ```javascript import { ref, onMounted } from 'vue'; export function useSetup() { let test = ref('123'); onMounted(() => { console.log(test); }); const fn = () => { test.value = '无法预测的舞台'; }; return { test, fn }; } ``` 在组件文件中: ```javascript import { defineComponent } from 'vue'; import { useSetup } from './useSetup.js'; export default defineComponent({ props: { name: String }, setup(props, { attrs, slots, emit, expose }) { const { test, fn } = useSetup(); return { test, fn }; } }); ``` 通过将setup函数分离到单独的文件中,可以更好地组织和管理组件的逻辑代码,并使代码更加可读和可维护。同时,由于setup函数是组件内部使用组合式API的入口点,这种分离还可以使组件更加灵活和可复用。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [[深入vue3之setup] setup与组合式 API](https://blog.csdn.net/lijiahui_/article/details/122536316)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [vue3 setup 组合式api使用 操作文件单独提出 类似后台的控制器](https://blog.csdn.net/qq_40095911/article/details/124091752)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

铸梦xy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值