ECS架构笔记

ECS由三个部分构成:
Entity:实体、个体。由多个component组成
Component:组件、数据。由数据组成
System:系统。由逻辑组成

Component:

组件Component是一个基类,有几百个子类,每个子类都有System执行Behavior时所需的成员变量。这里的多态是用来重写Create,以及使用虚析构函数管理声明周期,帮助回收垃圾。另外可能有访问内部状态的helper函数,除此之外Component不应该有Behavior。

class Component{
virtual void Create(resource* m_resource);
virtual ~Component();
}

Entity:

Entity结构
一个Entity其实像是一个对象,可以代表游戏世界中的任意对象,而其内部包含了若干个Component,拥有全局唯一的EntityID,用于标识Entity本身,ID是一个32位无符号整数。

class Entity{
 unsigned int ID;
 vector<Component> components;
}

System

而System则是系统,是行为,用来制定游戏规则。System本身没有数据,只有方法。在应用中,System之间不可以直接通信,并且一个System只关心某一个固定的Component组合,这个组合称为tuple。
**使用一个System的条件:**System会遍历所有Entity,若Entity当中,拥有System中tuple指定的所有Component,则对该Entity进行处理。
System本身并不关心Entity是谁,它只关心Entity中包含的component。

class System{
public:
  virtual void update(f32 timeStep)=0;
  virtual ~System();
}
class HitSystem:public System{
vector<Component> tuple;
public:
 virtual void update(f32 timeStep){//其实这段代码干了什么我真不清楚
   for(DerpComponent* d:ComponentItr<DerpComponent>(m_admin)){
    d->m_timeAccu+=timeStep;
    if(d->m_timeAccu>d->m_timeToHerp) this->HerpYourDerp(d,d->Sibling<HerpComponent>());
  }
 }
  for(Entity &entity:world.entitys)
   {
      if(/*entity中有tuple中的所有component*/)
      {//对应处理:
      }
   }
 }
}

实际上的System和tuple的例子:

//tuple
struct PhysicsTuple
{
 DynamicPhysicsComponent* m_dynamicPhysics;
 TransformComponent* m_transform;
 ContactListComponent* m_contacts;
}
//system
void PhysicsSystem::Tick(f32 timeStep)
{
   IPhysicWorld* pw=GetPhysicsWorld();
   pw->Update(timestep);
   //write transeforms of dynamic physics objects
   for(PhysicsTuple& t:getPhysicsTuples())
   {
    IPhysicsProxy* proxy=pw->GetProxy(t.m_dynamicPhysics->m_proxy);
    CopyTransform(t.m_transform,proxy);
    CopyContacts(t.m_contacts,proxy);
   }
}

但是这里我有一点疑问,每一次行为如果需要遍历所有的Entity,那么其复杂度就是tuple的component个数乘以Entity个数乘以Entity内component的个数,这个复杂度会不会很大?

World

(图来自视频截图)
在这里插入图片描述
World即是图中的EntityAdmin,其中包含了所有System,用哈希表存的Entity,以及所有Component。 而World里面有Update函数。

void EntityAdmin::Update(f32 timeStep)
{
 for(System* s:m_systems) s->Update(timeStep);
}

为什么要用ECS

参考:https://www.zhihu.com/search?type=content&q=Dots
前置知识点:
CPU自身有三级缓存,第1级最快,容量最小,第3级最慢,容量最大,而CPU访问内存的速率又远小于三级缓存的速率,在操作数据时,会先从1,2,3级缓存中取得数据,但是有些情况下请求的数据并不在这3级缓存中(缺页中断),就需要以寻址的方式去内存中,将一整块数据存放到缓存里,并把目标数据放到3级缓存中,提高下一次的访问速度。
ECS的数据组织与使用形式
ECS架构在执行逻辑时,只会操作需要用到的数据,而E和C这两者配合把相关数据紧密的排列在一起,并且通过Filter组件过滤掉不需要的数据,这样就减少了缺页中断的次数,从整体上提高了程序的效率。而另外现代CPU使用了数据对齐技术(自动矢量化:SIMD)与这种数据密集的架构相性较好,可以进一步提升性能。
ECS的优势与劣势
https://www.zhihu.com/question/286963885/answer/1162365997
——游戏科学联合创始人 招招 如是评论:
对cache友好

事实上在很多情况下,设计对cache友好的代码非常困难,如果游戏像是守望先锋那样,可以触及到底层,能够对数据密集的代码进行优化(寻路、碰撞、移动等等),这些地方很适合用for循环来高效遍历。
做一次伤害结算,你需要读取敌我双方的各种属性、状态,有些复杂的情况甚至还要计算双方的距离、甚至牵连到AI状态。然后一次伤害又可能产生各种附带的事件,比如触发一个buff、触发了死亡、等等等需要处理的逻辑。你需要对数据精心设计和拆分,才可能做到数据连续读写。稍有不注意,可能某个人加了一些功能就会破坏原来连续的存储访问,导致丧失cache友好的优势。
Cache友好的编程方式真正牛逼的地方是用于解决数据单一而密集的问题,比如frostbite在做culling的时候就分享过放弃对场景做树状划分,直接暴力遍历,反而因为cache友好而获得更好的性能以及更简单的代码。这是因为culling时候数据结构单一,逻辑也单一,才获得了好的效果。目前来说,移动逻辑、寻路逻辑,这些相对容易简化的密集运算,才真正适合cache友好的写法。而AI、技能则要视乎项目,往往多数项目在这块需求极其复杂,几乎无法构建出真正cache友好的数据结构。

总结:ECS不适应复杂的技能机制,适合相对容易简化的密集运算。
程序结构清晰

ECS是一种反OOP的设计,其中最大的一点就是ECS没有提供天然的多态支持。多态必须通过为entity装配不同的component来实现,那这样component的设计就会变得很折腾,例如如果你有几十个AI节点或者几十种技能效果,你可能要设计很多很多的component去对应。但是,如果你想用多态来做逻辑,为什么还要用ECS呢?因为多态本身是反Cache友好的。不同逻辑之间使用的数据都不相同,自然也就破坏了数据的连续读写。这就是为什么用ECS做ui、做复杂技能特性,往往会束手束脚。
ECS还有另一点就是,没有什么很约定俗成的跨模块耦合方案。不同的框架有不同的思路。有些框架提供立即触发的事件调用其他的system。守望先锋提倡共享代码抽到util,大的side effect抽到一个单独的system通过延迟事件解决。这里要重点说一下,立即触发和延迟触发的事件,对ECS的意义,是不同的。立即触发的事件,本质上相当于在循环里直接调用另一个system。这首先是反Cache友好,因为我在一个循环里又去尝试访问其他system关注的数据,同时他大大地增加了一个循环内部的逻辑复杂度。然后这也是反多线程友好的,因为多线程必须清晰知道每个system对component的读写关系,而事件隐藏了这块的关系,很容易会造成框架对读写关系判断错误而产生线程同步问题。然后就是延迟事件,这种方式,可以解决掉上面说的两个问题。但是这不是一个万能的方案:延迟事件本身需要将事件参数进行存储,这带来了存储的消耗以及需要增加这部分的代码。大量地使用,也会让你觉得十分折腾,有时很简单的一个side effect都要加一个处理延迟事件的system逻辑来解决。而且延迟事件还要考虑清楚,延迟这个事情本身会不会产生问题。所以,不好说这两种方案的优劣,但是如果system之间如果存在大量的逻辑耦合,要么就是system不是一个好的拆分,要么就是不应该选择ECS这种模式去开发。

总结:需要精心设计跨模块耦合方案,如果需要立即触发事件,去调用其他的system,那就会发生for循环里调用另一个system(再嵌套一个for循环),虽然可以用延迟事件(按顺序调用system,只改变数据?),但需要良好的system设计。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity的ECS架构是一种基于数据的设计模式,它将游戏对象(GameObject)拆分为各个组件(Component),并且通过系统(System)来处理这些组件,以此来实现游戏逻辑的编写和管理。 在ECS架构中,数据和逻辑是分离的,每个组件只包含数据,而不包含任何逻辑。而系统则负责处理这些组件,并且根据组件的数据状态来执行相应的逻辑。 下面详细介绍Unity的ECS架构: 1. 实体(Entity) 在ECS架构中,实体(Entity)是游戏对象(GameObject)的抽象。它只是一个ID,用于标识一个游戏对象。实体没有任何的组件或者逻辑。 2. 组件(Component) 组件(Component)是游戏对象的基本元素。每个组件只包含数据,不包含任何逻辑。例如,Transform组件只包含位置、旋转和缩放等数据,而不包含任何移动或旋转的逻辑。 3. 系统(System) 系统(System)是处理组件的核心。系统会根据组件的数据状态来执行相应的逻辑。例如,移动系统会根据Transform组件的位置和速度等数据来更新游戏对象的位置。 系统可以根据需要访问一组或多组组件,并且可以通过查询语言(Query)来获取需要的组件。例如,一个移动系统可能需要访问Transform组件和Velocity组件,它可以使用查询语言来获取这些组件。 4. 状态组件(State Component) 状态组件(State Component)是一种特殊的组件,它包含游戏对象的状态信息,例如是否存活、是否受伤等。系统可以根据状态组件的数据状态来执行相应的逻辑。例如,死亡系统会根据是否存活状态组件来判断游戏对象是否死亡。 5. 事件(Event) 事件(Event)是一种可以触发系统执行逻辑的机制。例如,当游戏对象被攻击时,可以触发一个受伤事件,从而让受伤系统进行处理。 6. 工作流(Workflow) 工作流(Workflow)是一种将多个系统组合起来处理游戏逻辑的机制。例如,一个游戏对象可能需要先执行移动系统,然后再执行攻击系统,最后再执行死亡系统。工作流可以让这些系统按照一定的顺序来执行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值