找回密码
 立即注册
查看: 328|回复: 0

[简易教程] Unity ECS入门学习笔记

[复制链接]
发表于 2021-11-19 11:04 | 显示全部楼层 |阅读模式


ECS Sample - Boids演示

引言

最近在学习A*寻路算法时,无意间发现Unity在2019年推出了一个新的框架ECS。了解片刻后,被这个框架带来的极大的性能提升所震撼,于是打算认真的学一下这个框架。这篇文章简单总结一下我近一周的学习心得,本人技术有限,如果文章有所疏漏也烦请指正!
ECS也就是Entity Component System的缩写,ECS采用了面向数据编程的思想,将之前笨重的GameObject拆分成值类型的Entity和Component,然后再使用System进行统一管理和运行。该框架亦引入了多线程编程技术,这样大大利用了设备CPU的性能。
这篇文章会包含如下内容:
    为什么Unity要开发ECSECS是如何带来性能提升的什么情况要使用ECSECS实例
为什么Unity要开发ECS




芯片性能增速变缓

根据Unity自己的说法,目前芯片的性能迭代早已不遵循摩尔定律,即每18个月芯片性能提高一倍。所以最大化的利用的芯片性能是之后是开发者需要关注的重中之重。
在我看来,其实Unity早就应该提出这一套开发框架了。。本身C#的性能就比C++低不少,而且Unity的MonoBehavior class也非常笨重,这一次的框架更新真的可以给未来基于Unity的游戏带来更多的可能。
ECS是如何带来性能提升的

根据Unity的文档解释,和我个人的理解,我认为有两点。
1. 提高了内存读写速度
但我们运行游戏内的逻辑时,CPU需要在内存里读取数据并放到缓存中等待读取,可是通常数据在内存里的存放顺序是随机的,所以这会很大降低内存寻址的效率。



数据在内存上的分布是随机的

但如果数据是连续存放的,那么内存寻址时的错误率会大大降低,从而减少CPU等待内存寻址的时间,进而提高了CPU的利用率。Unity ECS引入了Archetype和Chuck两个概念,Archetype即为Entity对应的所有组件的一个组合,然后多个Archetypes会打包成一个个Archetype chunk,按照顺序放在内存里,当一个chunck满了,会在接下来的内存上的位置创建一个新的chunk。因此,这样的设计在CPU寻址时就会更容易找到Entity相关的component。



Archetype示意图



Chunk示意图

2. 多线程的Job system提高了CPU利用率
这个比较好理解,Job system会把entities的分发到CPU的线程上,每个线程都会处理一系列任务。
什么情况要使用ECS

我个人认为并不是所有的项目/人都适合采用ECS架构,首先ECS的学习曲线较为曲折,太多新的方法,另外编程思想也不一样,整体学习的体验像是学一门新的语言,因此并不建议新手一上来就学ECS架构。
其次ECS主要解决的是CPU瓶颈,如果你的项目更多的是GPU瓶颈,那么没有必要把项目完全改成ECS框架,请考虑采用图形学方面的优化。
ECS比较适用于场景中有大量独立的agent逻辑的场景,比如RTS,模拟经营这类游戏就很适合用ECS进行优化。
ECS实例

实例下载:https://github.com/moecia/UnityECS
前言
在开始之前提一下刚刚没有讲到的2个ECS用的工具。
1. Burst Complier:Burst会把C#代码编译成更高效的机器语言,提高游戏运行速度

2. JobSystem:Unity开发的无痛多线程代码运行库。在最新的ECS 0.17.0版本中,似乎不需要再额外写专门的Job struct来实现多线程,只需要让我们的System类继承SystemBase class,并在OnUpdate的Entites.ForEach loop后,加上一个extension method即可实现多线程运行。

因为Unity ECS的更新比较频繁,所以在我学习过程中发现不少教程的方法都已经deprecate了。。所以我这个教程在1,2年后也难免变得不可用。

目前无论是ComponentSystem还是JobSystem,都被Unity宣布弃用。(不少老教程还在用这两个Class)所以我这里一律会使用SystemBase。

SystemBase的和ComponentSystem和JobSystem的主要区别是,在OnUpdate的Entites.ForEach loop后,必须要选择一个extension method,主要有3个常用的extension method,这里目前我还不太完全理解3个的具体区别(因为性能上表现比较一致)所以我这里仅复制粘贴文档,不做解释...
1. Run() : Runs the job immediately on the current thread.
2. Schedul(): Adds an IJobEntityBatchWithIndex instance to the job scheduler queue for sequential (non-parallel) execution.
3. ScheduleParallel(): Adds an IJobEntityBatchWithIndex instance to the job scheduler queue for parallel execution.
代码部分:
MoveSpeed.cs
using Unity.Entities;

namespace EcsSample
{
    public struct MoveSpeedComponent : IComponentData
    {
        public float MoveSpeed;
    }
}MoveSystem.cs
using Unity.Entities;
using Unity.Transforms;

namespace EcsSample
{
    public class MoveSystem : SystemBase
    {
        protected override void OnUpdate()
        {
            var dt = Time.DeltaTime;
            Entities.ForEach((ref Translation translation, ref MoveSpeedComponent moveSpeedComponent) =>
            {
                translation.Value.y += moveSpeedComponent.MoveSpeed * dt;
                if (translation.Value.y > 5f || translation.Value.y < -5f)
                {
                    moveSpeedComponent.MoveSpeed *= -1;
                }
            }).ScheduleParallel();
        }
    }
}TestController.cs
这里额外提下用到的SetSharedComponentData()方法。这个方法用于共享的component,然后会把所有share component打包到一个chunk,这样当需要更新share component时,因为它们内存中的存放位置时连续的,所以访问速度会更快。
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;
using Random = UnityEngine.Random;

namespace EcsSample
{
    public class TestController : MonoBehaviour
    {
        [SerializeField] private Mesh mesh;
        [SerializeField] private Material material;
        void Start()
        {

            var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
            // Add entity achetype
            var entityArchetype = entityManager.CreateArchetype(
                typeof(MoveSpeedComponent),
                typeof(Translation),
                typeof(RenderMesh),
                typeof(LocalToWorld),
                typeof(RenderBounds));
            var entityArray = new NativeArray<Entity>(500000, Allocator.Temp);
            // Instantiate entities
            entityManager.CreateEntity(entityArchetype, entityArray);
            for (int i = 0; i < entityArray.Length; i++)
            {
                var entity = entityArray;
                entityManager.SetComponentData(entity, new MoveSpeedComponent { MoveSpeed = Random.Range(1f, 2f) });
                entityManager.SetComponentData(entity, new Translation { Value = new float3(Random.Range(-8f, 8f), Random.Range(-5f, 5f), 0) });

                entityManager.SetSharedComponentData(entity, new RenderMesh
                {
                    mesh = this.mesh,
                    material = this.material
                });
            }

            entityArray.Dispose();
        }
    }
}代码结构比较简单,MoveSpeed.cs就是一个移动速度的Component class,第二个MoveSystem就是控制Entity移动的,当entity移动到屏幕地图顶上就会往下移动,反之到了底部就会往上移动。最后的TestController.cs就是用于生成Entity。
欢迎关注我的博客和B站:

B站:BN的日记的个人空间_哔哩哔哩_Bilibili
博客:https://moecia.github.io/
参考资料

官方文档:
- https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/index.html
官方入门教学系列:
- https://learn.unity.com/tutorial/editor-scripting
官方样例:
- https://github.com/Unity-Technologies/EntityComponentSystemSamples
中文入门文档:
- http://dingxiaowei.cn/2020/02/09/
- https://www.lfzxb.top/unity-dots-ecs-burst-complier-jobsystem/

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-1 06:53 , Processed in 0.301580 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表