ECS架构的理解

建议先看这篇: 

游戏架构之继承对象模型和组件对象模型-CSDN博客
Entity Component System (ECS) 是一种以属性为中心的软件架构风格,它遵循合成优于继承(Composition over inheritance)的思想,采用数据库类似的结构来存储游戏对象。ECS架构能够实现高度的数据驱动,通常ECS都会和数据驱动结合起来使用。本节将要讲述ECS 实现的一些机制和策略,以及怎样实现数据驱动。

属性结构
在ECS中共有3种不同类型和职责的元素。
●Entity: 表示一个游戏对象,它通常只包含一个表示对象ID的唯一 标识符。它拥有一个Component的集合,这些不同类型的Component集合构成一个特定类型的游戏对象。此外,Entity 也可用来作为属性间通信的枢纽。
●Component: 表示一个属性的数据部分,例如HealthComponent 表示一 “个血量属性的数据。通常Component是不包含实例方法的,除了一些方便的get/set,或者内部数据处理方法。
.● System: 表示一个属性的行为部分,例如HealthSystem表示血量属性的行为,它会检测HealthComponent的值,如果其血量小于或等于0时则向Entity发出死亡通知事件。System 通常由游戏循环驱动来修改游戏对象的状态,典型的System 包含一个update (更新)方法。
所以,一个属性由Component和System构成,分别表示属性的数据和行为,如图16.6所示。Component和System几乎是一一对应的,但是也有可能某些属性只包含数据,而没有System实例与它对应。

 从这里可以看出,ECS有些类似于组件模型,但是与一个组件同时包含数据和行为不同的是,ECS将数据和行为分开。虽然组件模型也可以这么做,但是在下一节我们将看到ECS真正将数据和逻辑分开的原因是能够改善计算性能。
Entity与Component的关系可以用图16.7 所示的表格来表示。在图16.7中,每一列代表一个 Component,每一行则表示一个 Entity对象,不同的Component组合构成一个特定的Entity类型。例如一个hero对象由一个render, gun和collision属性组成,分别表示其外观、可攻击敌人及参与碰撞检测的行为。Component 在内存中是按类似数据库一样的结构存储的。

ECS的优势:

1. 性能优势:

  • 自定义内存布局:在C/C++/C#语言中,开发者可以通过自定义内存布局来优化数据在内存中的存储方式。通过将相似类型的数据(组件)连续存储在内存中,可以显著提高CPU缓存命中率,从而减少内存访问的开销,提升整体性能。
  • 数据局部性:由于ECS将组件紧密存储在一起,处理系统(Systems)可以高效地遍历和操作组件数据,而不会频繁地进行随机内存访问。这种数据局部性对于大型数据集的操作非常有利,进一步增强了性能。

2.解耦

  • E -- Entity 实体,本质上是存放组件的容器
  • C -- Component 组件,游戏所需的所有数据结构
  • S -- System 系统,System 是纯方法组合,它自己没有内部状态。它要么做成无副作用的纯函数,根据它所能见到的对象 Component 组合计算出某种结果;要么用来更新特定 Component 的状态。

这里需要强调一下,Componet组件只能存放数据,不能实现任何处理状态相关的函数,而System系统不可以自己去记录维护任何状态。说的通俗点,就是组件放数据,系统来处理。这么做的好处,就是为了尽可能地让数据与逻辑进行解耦。与此同时,一个良好的数据结构设计,也会以增加CPU缓存命中的形式来提升性能表现。但我认为,推动ECS流行的更深层次原因,是它的解耦理念,性能上还只是其次。

C 和 S 是这个框架的核心。一个System 系统,也就是一个功能模块。对于游戏来说,每个模块应该专注于干好一件事,而每件事要么是作用于游戏世界里同类的一组对象的每单个个体的,要么是关心这类对象的某种特定的交互行为。比如碰撞系统,就只关心对象的体积和位置,不关心对象的名字,连接状态,音效、敌对关系等。它也不一定关心游戏世界中的所有对象,比如关心那些不参与碰撞的装饰物。所以对每个子系统来说,筛选出系统关心的对象子集,以及只给系统展示它所关心的数据就是框架的责任了。我们在开发的时候,可以定义一个 System 关心某一个固定 Component 的组合;那么框架就会把游戏世界中满足有这个组合的 Entity 都筛选出来供这个 System 遍历,如果一个 Entity 只具备这组 Component 中的一部分,就不会进入这个筛选集合,也就不被这个 System 所关心了。

 ECS,相当于回到了C语言的开发模式,否定了面向对象。System来处理Enity, 就是算法来处理数据结构。

ECS遵循组合优于继承原则,游戏内的每一个基本单元都是一个实体,每个实体又由一个或多个组件构成,每个组件仅仅包含代表其特性的数据(即在组件中没有任何方法),例如:移动相关的组件MoveComponent包含速度、位置、朝向等属性,一旦一个实体拥有了MoveComponent组件便可以认为它拥有了移动的能力,系统便是来处理拥有一个或多个相同组件的实体集合的工具,其只拥有行为(即在系统中没有任何数据),在这个例子中,处理移动的系统仅仅关心拥有移动能力的实体,它会遍历所有拥有MoveComponent组件实体,并根据相关的数据(速度、位置、朝向等),更新实体的位置。

实体组件是一个一对多的关系,实体拥有怎样的能力,完全是取决于其拥有哪些组件,通过动态添加或删除组件,可以在(游戏)运行时改变实体的行为。

Component 只包含属性,System 只包含行为。System 是不保存状态的,Component 才是状态的真正持有者。

一个使用ECS架构开发的游戏基本结构如下图所示:

先有一个World,它是系统实体的集合,而实体就是一个ID,这个ID对应了组件的集合。组件用来存储游戏状态并且没有任何行为,系统拥有处理实体的行为但是没有状态。

Unity 的ECS:

1.Unity ECS框架里面有几个重要的概念:

Entity, ComponentData, System,Archetype, EntityManager, World;

先看图:

ComponentData:

组件数据,开发的时候,一个功能使用到的数据,需要新建一个ComponentData来存放。

Entity:

 一个游戏对象的实体,可以是游戏中玩家、BOSS、NPC等,一个Entity里面包含了一个个的ComponentData,每个ComponentData代表这个Entity拥有这个功能。

System:

具体算法逻辑的实现,System算法的数据基于Entity的中的某个ComponentData。游戏引擎每次Update的时候,System都会根据每个Entity里面的它使用的ComponentData, 来更新数据完成计算。

EntityManager:

负责Entity相关类型信息,对象实体的创建与管理。

Archetype:

Entity是由多个ComponentData组成的,每种Entity类型都会对应一个Archetype, 里面描述了这个Entity类型以及相关的布局。Archetype由EntityManager创建出来,创建出来后这个类型的Entity的大小,包含的组件数据,内存布局都确定了。

World:

世界包含多个Entity组成Entity群体,通过世界把这些Entity群体孤立起来。Unity里面可以同时拥有多个不同的世界, 每个世界独立包含EntityManager与Systems, 在世界里面创建出来的一个Entity,只属于这个世界。世界里每个System也只能迭代一个世界里面的Entity实体数据。你可以创建多个World。

总结一下Unity ECS的使用步骤与逻辑关系:

(1) 创建一个ECS的World;

(2) 使用World的EntityManager为不同Entity类型创建出来Archetype;

(3) 基于Archetype, 我们创建出来Entity内存对象, 并初始化Entity里面每个ComponentData数据;

(4)为ECS World编写System算法,World会调用System算法来迭代每个Entity里的相关ComponentData;

2: ECS 高效的内存模型与布局

作为架构师,设计框架数据结构由为关键。特别对于游戏开发,要处理迭代成百上千个游戏物体。高效的内存模型与布局如何实现? 首先要明白怎么样布局内存才会高效。内存模型布局高效主要从两个方面来考虑:

1:内存对齐。

CPU访问不同地址的内存数据的时候,如果内存地址基于2^n 数据对齐(n根据CPU而定),访问最为高效。例如我们以16字节数据对齐,那么对象内存开始排布的时候, 从地址能整除16的内存位置开始排布。高效的内存布局,对象实例内存地址要对齐,对象实例里面的每个数据成员内存地址也要对齐。Entity的内存分配与内存布局, 在创建Entity的时候,ECS系统就会注意Entity的内存对齐,同时也会注ComponentData在Entity里面排布的内存对齐,如果没有对齐,就会空一些出来。比如16字节对齐,一个ComponentData只有14个字节,实际上排布下一个ComponentData的时候是从16字节开始排布的。

2: CPU通过地址来取到内存数据的时,数据排布一起取的速度会更快。

ECS天然就具有这种优势。所有的逻辑代码迭代都是基ComponentData的,也就是算法迭代访问处理的数据都在一起,在一个ComponentData里面这样访问数据高效,避免了地址的来回跳转。

3: Entity内存分配器 Chunk的设计

大量的Entity的创建与销毁,大量不同类型的数据对象交叉创建,很容易造成内存碎片。何为内存碎片?给大家举个例子,对象A,对象B(100字节),对象C连续排布,对象B销毁,内存释放,同时又请求分配对象D(80字节),系统在原来对象B的地方把一块内存分配给对象D,还剩了一小块内存(20字节)。而这一小块内存,无法分配给当前系统的任何一个对象,这样,这小快内存就再也无法使用了,造成内存碎片,随着系统不断运行,随着更多大量的创建与释放,内存碎片越来越多,分配/释放效率也越来越低。最终让系统越来越慢。解决这种大量创建与销毁,我们一般采用Cache内存池来做。那Unity ECS架构里面内存池是如何设计的呢?接下来我们来分析ECS里面基于Chunk的内存分配器。不同的Archetype,所对应的Entity的内存不一样,系统可能有N中不同的Archetype,而且Archetype是用户根据组件数据组合创建而来。

综上Unity ECS 是这样设计内存分配缓存池:

(1) 基于固定大小的Chunk来做内存缓存池。每个Chunk大小为16KB(引用来自于UnityECS 文档)

(2) 当Archetype 确定后,Entity的内存大小就确定下来,每个Chunk能分配的当前的Entity数目就能确定下来。基于Archetype创建Entity的时候,首先从内存池里面拿一个Chunk, 然后根据每个Chunk可创建Entity数目,从Chunk里面把Entity分配出去,如果chunk分配完毕,从chunk内存缓存池里面再拿一个Chunk。由于基于Archetype创建,这个Archetype的所有Entity内存大小都是一样的,等释放的时候,我们就可以基于Archetype把Entity缓存起来,重复使用之前的Entity。

对于OS而言,ECS框架基于chunk来分配和释放内存,避免了内存碎片(大小都一样)。对于ArcheType而言,又基于特定ArcheType的Entity来做内存缓存,分配和释放都非常高效。

看完Unity ECS的架构设计与内存布局,我们在其他地方做ECS架构设计(游戏服务器)就有了一个很好的参考,设计万变不离其中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值