`

在cocos2d-x中实践数据驱动的游戏开发

 
阅读更多

from: http://elvisco.de/2013/09/entity-component-system/

 

 

2013/09/29

(本文已发表在《程序员》杂志2013年10月刊,转载需经《程序员》杂志许可)

在2002年的GDC大会上,Scott Bilas做了一个题目叫做《A Data-Driven Game Object System》的演讲,他在自己的博客上说怀疑自己是第一个提出这个概念的人,但肯定是第一个对公众解说这个概念的人。

这个概念引起了游戏圈内广泛的讨论,至今已形成比较完整的理论,维基百科上称作Entity-component-system(ECS),或者更简便的称作Entity System。到今天,著名的游戏引擎Unity甚至完全基于ECS来构建。

然而国内社区似乎很少讨论ECS,我们常使用的Cocos2d游戏引擎默认也不支持(最近的2.1.4版本加入了简单支持,后面分析)ECS。本文 将详细探讨ECS的工作原理,优点及不足,以及它和游戏引擎的关系,还会简单分析Unity中ECS实现的方法,最后提供一个基于Cocos2d-x实现 ECS的完整的源代码实例。

  • 面向对象在游戏设计中遇到的问题

ECS的出现起源于面向对象的架构风格在游戏设计中遇到了一些问题。在现代软件设计中,通常使用面向对象的思想来抽象各种实体,数据模型,业务逻 辑,它工作的很好,我们用易于理解的名词例如Car,People等类来描述软件中的各种概念,通过继承,多态来表述同类事物的不同特征和不同行为,如下 图:

 



 图1. 《Ghost in the Sea》中部分类结构

 

然而这样的设计方式在游戏开发中却遇到了困难,例如在上面结构图中,船是不同于英雄的类,但是其实船也是可以产生资源的,它是否应该继承于“资源” 类呢?如果他们是相互独立的,意味着有部分重复代码;其次,如果设计需求中新加入一种敌人,它可以“远程”攻击轮船,用什么攻击呢,我们给它配一把 “枪”,我们有一些解决方法:

  • 使用C++中的多继承,让新加的敌人同时继承于“枪”和“远程”。
  • 从“远程”继承,复制“枪”的代码。
  • 为了清晰表达一个新的类型,干脆新建一个类,复制两个类中的代码。
  • 把代码移至父类中,不同的子类执行不同的方法。

我们看到,不管使用哪一种方法,都需要对程序进行不同程度的修改,修改的原因是为了代码重用,修改的结果却使得类的职责不再清晰。这就是面向对象在 处理交叉关系时遇到的问题,这不仅涉及到每次变更都要重构类的结构,更加重了理解和处理这些关系的负担。并且这会占据很多时间:在项目开始的时候,绞尽脑 汁把简单的数据库转化为面向对象世界复杂的关系,在每次需求变更的时候重构这种关系。

而在整个游戏开发过程中这样的变更非常频繁。每一次变更可能会影响到很多地方,对游戏的稳定性也是非常不利的。而应对这种频繁复杂的需求变更正是基于组件的架构最擅长的。

  • 基于组件的架构风格

基于组件的架构设计风格核心思想是组合优于继承(prefer composition over inheritance),即是通过将各个相对独立的数据和逻辑组织成一个组件(而不是通过继承)来实现代码重用。这样我们可以通过不同的组件组合形成不 同特征的对象,这样形成一个扁平而不是树形的结构,如下图:

 



 

图2 基于组件的扁平的架构设计风格

从上图可以看到,一个GameObject本身包含很少的信息,它仅仅是由一些组件组成,这些组件的组合决定一个特定GameObject的特征和 行为。这样就可以应对不同的需求同时保持比较稳固的架构。如果新的需求加入一种新的数据和行为,我们就定义一个新的组件,如果新的需求具有不同的行为组 合,我们就为这类对象添加不同的组件组合即可,这对已有程序不会有什么影响。

基于组件的设计还使我们将精力集中在逻辑及数据本身,而不是绞尽脑汁去抽象各种类层次结构,继承关系。软件设计关心的是数据和行为,面向对象是为了 重用代码,简化设计的一种方法,它不是程序设计的规则和原则,它也有它的局限性。因此我们可以说基于组件的设计风格和面向对象的设计风格是两种比较独立的 方法,读者有必要区分这两个概念。

  • Entity Component System中的概念

ECS是一种架构风格,所以它可以有不同的实现方式,限于篇幅,这里只介绍社区讨论和使用比较多的方法。传统的ECS架构分3部分,分别是 Entity,Component和System,以及一个用来管理Entity和Components对应关系的EntityManager,以下分别 介绍:

  • Entity:Entity泛指一个游戏对象,它仅用来标识一个对象,除此之外不包含任何信息。它可以包含或者不包含UI,这些都由它所拥有的Components来决定,不同的组件组合构成了一个特定的游戏对象:



 

System:System表示Entity的一个行为,它用Component提供的数据,根据一定的逻辑更新Entity的状态,例如HealthSystem计算英雄的血量并绘制血条。在一个冒险游戏中,MoveSystem则会移动人物不断向指定方向前进:

 



 EntityManager:EntityManager是Entity的数据库,它存储所有的Entity及每个Entity拥有的Component集合,System将从这里检索所有Entity及获取需要的Component

 



 

  • 怎样使用Entity Component System

有了ECS的一些基本概念,那么首先来看一下在cocos2d-x中怎样使用它,然后再详述ECS的工作机制。现在假设需求是要为一个游戏对象添加 一个Sprite,并绘制血条。现在我们还不太清楚ECS的工作机制,那么我们试着根据Entity-Component-System的定义来思考我们 该怎样设计。

首先,我们要用一个特定的Entity来表示一个特定的游戏对象,这里假设我们现在要处理的是轮船,我们这样写:

Entity* ship = _entityManager->createEntity();

我想我们已经完成轮船的定义了,不是吗?还记得前面说过Entity仅用来代表一个游戏对象吗?除了一个用于区别自己的Id,再没有什么其他信息。

接下来,应该定义Component。因为Component代表数据,分析需求,我们需要什么数据呢,我觉得应该需要一个UI元素,以及表示血条 相关的信息。那么我们将定义两个Component:RenderComponent和HealthComponent,这里仅分析 HealthComponent,读者自行查看源代码关于RenderComponent:

我们定义了HP,Alive等Health相关的属性,然后重写父类的虚函数getComponentType(),它用来表示一个 Component的类型。在有些设计中使用RTTI中的typeId来表示Component的类型,这里用std::string来表示类型,因为会 频繁检索。

最后就是System了,这是处理Component数据并更新游戏对象状态的地方,也就是游戏中的逻辑,这里不再贴healthSystem.cpp的代码,读者自己参照源代码:

System是游戏的循环,它应该被mainLoop调用。从HealthSystem的源代码中可以看出,在每次update的时候,它首先从 Entity中查询所有具有HealthComponent数据的Entity对象,然后遍历每个Entity,如果当前血量小于0,则将alive设置 为false,最后它还检查该Entity是否包含RenderComponent数据,如果包含则将UI元素从屏幕移除,因为该对象已经死亡。

现在我们已经准备好所有需要的数据(Component),以及处理逻辑的算法(System)了,接下来我们让ECS系统工作起来,打开HelloWorldScene.cpp文件,找到以下部分:

分析上面的代码,我们创建了一个Entity对象,用来表示船。ship对象拥有UI表现,血量等相关信息,所以我们添加RenderComponent和HealthComponent到ship对象,这样ship对象就有了UI和血量相关的数据。

然后,我们在update中调用每个System的update方法,每个System在update中将根据自身行为对数据的需求,从 EntityManager中遍历所有Entity,满足条件的Entity将对其进行处理,处理什么呢,其实就是修改Component中的数据,这样 就实现了游戏循环。

也许你现在还是很迷惑,System到底应该怎样处理Entity呢,我们再用一个比喻来看一下ECS的工作原理。

  • ECS是怎样做到数据驱动的

在StackExchange上有个叫Byte56的开发者将ECS比作锁系统,他把Entity比作一把钥匙,而每个Component是一把钥匙上的齿槽,拥有不同Component组合的Entity就形成一把不一样的钥匙:

 



 

图3 将Entity比作一把钥匙

而将System比作一把锁,如下图的MoveSystem对钥匙的定义,只要是同时拥有Position和Velocity齿槽的Entity就将会被MoveSystem处理。

 



 

图4 将System比作锁

与真实锁系统不同的是,这里System定义的锁和Entity定义的钥匙不是绝对匹配的,Entity只要包含System需要的齿槽即可,如图3表示的Entity也会被图4表示的System处理。

通过这样的机制,我们可以将游戏中的每一个行为抽象成一个独立的System,一个System封装了某一个方面的游戏逻辑单元,这个逻辑单元基本 上不可再拆分成更小的逻辑单元。而每个System需要特定的一些数据,这些数据由Component提供,满足某个System所需要的数据集合,即被 认为需要执行该System中的逻辑处理。

反过来,通过给Entity组合不同的Component,构成不同的“条件”,就自动给Entity附加上了一个不同的行为,这样就实现了数据驱动。

  • 关于数据和逻辑的分离

ECS是一个数据和逻辑高度分离的很好的例子,Component仅定义纯数据,而System中不存储任何Entity的数据,最多包含一个循环 内的临时数据,由于系统中每个System只存在一个实例,它也约束着我们不能在System中存储游戏对象的数据,因此完全成为一个逻辑的封装。

数据和逻辑的分离从一定程度上降低了耦合,因为耦合的原因一般都是一个对象希望访问另一个对象中的数据,如果仅是访问一个纯算法,这是完全可以排除 这部分耦合的,就是因为包含了特定的数据,才使得两个类之间有直接的关联,因此如果将数据存储至一个公共的地方,不同的算法都从这里读取数据,这样就能排 除耦合,在ECS中每个System都是纯算法,相互之间没有什么耦合的部分。

 



 

图5 ECS中数据和逻辑完全分离

  • ECS和游戏引擎的关系

通过对ECS的学习,我们明白它是一种架构风格,更具体一点,它指导我们应该怎样设计算法和使用数据,因此它是和绘制无关的,它需要和游戏引擎的绘制系统一起工作。

那么它到底需不需要引擎支持?确实有引擎如Unity(我们后面会分析)基于ECS系统来设计引擎,然而从ECS的概念来看,它基本上可以和游戏引 擎一起工作,而不需要引擎提供专门的支持,因为它仅包含行为和数据,与绘制无关。就像Box2D物理引擎,它包含的也仅仅是算法,它可以和大多数游戏引擎 例如Cocos2d一起工作。

值得注意的是,在实践ECS的时候,问的比较多的问题是关于输入(触摸,鼠标,重力感应等)应该怎样处理,是应该在System还是在 Component中获取系统事件。其实输入属于游戏引擎的基础设施,就像UI元素的树形结构,这些和游戏行为无关的事情不应该放入到ECS中去,这些都 应该转化成数据存储在Component中。

所以,不是所有代码都应该ECS化,ECS应该是和游戏引擎结合起来使用,例如元素的层级结构,深度,触摸响应,动画,地图等等都应该在引擎层面来 处理,然后将之转化为Component数据供System使用。记住,ECS只包含游戏行为和游戏数据,ECS帮助我们将这部分逻辑从引擎中抽取出来。

当然,ECS也可以被集成进游戏引擎中,它跟引擎之间就能形成了一个比较好的协作,它甚至能帮助引擎实现某些功能,我们来分析一下Unity引擎中的组件系统。

  • Unity中的ECS分析

我们一直强调ECS是一种架构风格,它可以有不同的实现方式,Unity在组件系统的实现上较标准的ECS系统有几点不一样:

  • 将System转化为实例,附加到每个GameObject中,而不是集中一个System处理所有GameObject。这样做的好处 是:System中的逻辑处理可以和UI结构相对应,标准的ECS系统是忽略UI结构的;其次是支持可视化设计,通过引擎本身提供一些标准的绘制相关的 Component就可以很好的支持设计器,开发者自定义Perfab的时候只是组合元素之间的父子关系,并修改这些标准的UI组件的属性,设计器可以应 付任何可视化设计。
  • 因为System成为了GameObject的集合,而Component也是GameObject的集合,它们之间的区别就仅仅是一个处理逻 辑,一个存储数据,所以Unity就将这两者简化成一个东西:Component。这给引擎设计带来一致,简便,然而却给开发者留下一些模糊的概念,如果 学习Unity之初不熟悉ECS系统,则很难实现数据和逻辑的分离,数据和逻辑混在一个Component就带来组件之间频繁通信,因为一个组件需要访问 另一个组件中的数据。这个时候的表现往往是大量使用Message之类的,Unity支持向GameObject发送消息,这些消息会被 Component中对应的方法处理。
  • 由于将System集合化,这也使得寻找Component的任务落到GameObject上,System的update需要由 GameObject自身来驱动,所以整个ECS也必须由引擎支持,相应地GameObject就需要在全局范围内根据name任意查找,否则整个系统就 工作不了,因为你无法找到Entity,无法和其他Entity之间交互。



 

图6 Unity中的组件系统

  • Cocos2d-x引擎提供的组件支持

说Cocos2d-x引擎不支持组件系统其实是不准确的,在2.1.4版本中给CCNode加入了组件支持:

 



 

每个CCNode新包含一个m_pComponentContainer的容器,它存储每个CCNode所拥有的所有Component集合,然后 在CCNode的update循环中调用每个Component的update方法。这和Unity的设计思路是类似的,然而因为cocos2d-x缺乏 全局范围内检索游戏对象,使得引擎提供的组件系统使用起来有点困难。

通过前面的分析,我们知道标准的ECS不需要了解游戏元素的UI结构,Entity之间的交互全是通过Component数据来确定的,例如判断两 个Entity是否发生碰撞,首先ColliderSystem会从EntityManager检查所有包含ColliderComponent的 Entity,然后判断和修改transform相关的属性,它不需要和UI树打交道。

单纯一个Component融合了数据和行为,这不是数据驱动的方式。因此也就不存在EntityManager,因此Unity提供全局查找 GameObject的方法,而Cocos2d-x目前是不能全局查找CCNode,所以我们就只能通过parent去搜索UI树,这会在 Component中引入大量getParent,getChild之类的跟UI树关系紧密的代码,而一旦UI结构发生变化,则会影响这部分代码。同时数 据和行为混在一起对组件间消息传递的需求比较大,目前cocos2d-x中的组件也没有提供这样的机制。

所以cocos2d-x目前的组件系统适合做一些简单的算法方面的使用,不适宜用来构建整个系统,或者你也可以按ECS的方式,将数据抽取出来,建 立自己的EntityManager,同样可以做到数据驱动,读者可以自己去尝试。然而cocos2d正在朝着组件做一些努力,我们期待cocos2d- x 3.0能有高效简便的组件系统支持。

  • Entity Component System总结

至此,我们应该对ECS有一定的认识了,它是一种软件架构风格,与面向对象中继承的方式相反,ECS通过组合的方式重用代码,它能实现行为和数据的 完全分离,从而降低耦合,并通过定义一个System需要满足的条件(Component构成的数据组合)来达到数据驱动。并且它几乎是可以和任何绘制引 擎一起工作的。

ECS有很多优点,比如数据驱动,游戏设计师在不太多依赖程序员的情况下就可以通过数据变更游戏行为(当然需要将Component的组合转化为读取外部配置文件),最重要的是,它可以非常灵活地满足频繁变更的需求。

当然ECS也有一些缺点,社区讨论最多的是每一帧频繁查询大量Entity,如果这里用到了一些RTTI的方法可能会比较明显的影响性能,而且 System的每次处理都需要判断太多的变量,因为System本身并不保存任何数据,它的算法的数据完全需要Component来提供,并且为了减少 System之间的耦合,会大量增加一些变量在Component中。

社区也讨论了很多优化方案,感兴趣的读者可以继续阅读相关信息。总之,Entity Component System作为一种优秀的架构思想,它能为你带来愉悦的开发体验,减少你大量的痛苦。

 

  • 大小: 41.7 KB
  • 大小: 45.7 KB
  • 大小: 22.8 KB
  • 大小: 29.2 KB
  • 大小: 99.1 KB
  • 大小: 16.4 KB
  • 大小: 16.3 KB
  • 大小: 53.6 KB
  • 大小: 56.1 KB
  • 大小: 49.8 KB
分享到:
评论

相关推荐

    Cocos2d-x实战:JS卷——Cocos2d-JS开发

    资源名称:Cocos2d-x实战:JS卷——Cocos2d-JS开发内容简介:本书是介绍Cocos2d-x游戏编程和开发技术书籍,介绍了使用Cocos2d-JS中核心类、瓦片地图、物理引擎、音乐音效、数据持久化、网络通信、性能优化、多平台...

    大富翁手机游戏开发实战基于Cocos2d-x3.2引擎

    资源名称:大富翁手机游戏开发实战基于Cocos2d-x3.2引擎内容简介:李德国编著的《大富翁手机游戏开发实战(基于 Cocos2d-x3.2引擎)》使用Cocos2d-x游戏引擎技术,带领读者一步一步从零开始进行大富翁移动游戏的开发...

    Cocos2d-x 3.x游戏开发实战pdf含目录

    Cocos2d-x 3.x游戏开发实战pdf含目录,内容详细,强烈推荐给大家。

    Cocos2d-x-3.x游戏开发之旅

    Cocos2d-x-3.x游戏开发之旅-钟迪龙著 全新pdf版和附书代码(代码为工程文件,可复制) 附带目录标签

    Cocos2D-X游戏开发技术精解

    资源名称:Cocos2D-X游戏开发技术精解内容简介:Cocos2D-X是一款支持多平台的 2D手机游戏引擎,支持iOS、Android、BlackBerry等众多平台。当前,很多移动平台流行的游戏,都是基于Cocos2D-X开发的。 《Cocos2D-X...

    Cocos2d-x高级开发教程

    书中汇聚了热门手机游戏《捕鱼达人》开发的实战经验,作者从最基础的内容开始,逐步深入地介绍了Cocos2d-x的相关知识点。此外,书中的教学资源获得《捕鱼达人》手机游戏的授权,读者可以从一流游戏开发中高起点地...

    精通COCOS2D-X游戏开发 基础卷_2016.4-P399-13961841.pdf

    精通COCOS2D-X游戏开发 精通COCOS2D-X游戏开发 精通COCOS2D-X游戏开发 精通COCOS2D-X游戏开发 精通COCOS2D-X游戏开发

    cocos2d-x 3.x游戏开发实战光盘

    cocos2d-x 3.x游戏开发实战光盘

    Cocos2D-X游戏开发技术精解.pdf

    《Cocos2D-X游戏开发技术精解》详细介绍如何使用Cocos2D-X引擎开发自己的移动平台游戏。全书共15章,主要内容包括:Cocos2D-X引擎简介;如何建立跨平台的开发环境;引擎的核心模块——渲染框架;如何实现动态画面和...

    cocos2d-x事件类

    在使用cocos2d-x开发游戏的过程中,为了实现逻辑和显示相分离。 在下通宵了一个晚上,写出了该事件类。 谨记,该事件只能用于cocos2d-x中。 事件发送者需要继承EventDispatcher类 事件接收者需要继承EventHandle类...

    cocos2d-x-2.1.5

    cocos2d-x-2.1.5

    Cocos2d-x 3.x游戏开发之旅

    本书是《Cocos2d-x 游戏开发之旅》的升级版,修改了2.0版进阶到3.0版后的一些内容...通过2到3个游戏实例介绍Cocos2d-x在实际开发中的应用;手机网络游戏开发入门;介绍在实际的手游开发过程中遇到的问题以及解决方法。

    Cocos2d-x 3.x游戏开发之旅教程及完整源码下载

    Cocos2d-x 3.x游戏开发之旅教程及完整源码下载,使用最新cocos2d-x-3.14版本,在xcode7.3上已编译通过。 解决相关问题 1、解决源程序在高版本上无法编译问题 2、解决源程序中文注释部分,xcode上显示乱码问题 使用...

    精通Cocos2d-x游戏开发(进阶卷)源代码

    精通Cocos2d-x游戏开发(进阶卷)源代码 精通Cocos2d-x游戏开发(进阶卷)源代码 精通Cocos2d-x游戏开发(进阶卷)源代码

    大富翁手机游戏开发实战 基于Cocos2d-x3.2引擎

    本书理论和实践相结合,避免空泛的原理讲解,在理解了原理之上紧接着根据大富翁项目展开实际代码编写,从中能让读者领悟Cocos2d-x的神奇魅力,从而更加深入地理解和掌握Cocos2dx引擎,更能让读者深刻理解消息驱动...

    cocos2d-x游戏开发实战精解

    本光盘是《Cocos2d-x游戏开发实战精解》一书的配书光盘,内容介绍如下。 (1)本书教学视频:该文件夹收录了本书的配套多媒体教学视频,可用暴风影音等视频播放器播放。 (2)本书源文件:该文件夹收录了本书涉及...

    Cocos2d-x3.2大富翁游戏项目开发apk测试版

    Cocos2d-x 3.2 大富翁游戏项目开发apk测试版

    实例妙解Cocos2D-X游戏开发

    一线资深游戏开发工程师根据Cocos2D-X 最新版本撰写,Cocos2D...完全通过真实游戏案例驱动,不仅将Cocos2D-X的各种功能、原理、技巧融入其中,而且还详细讲解了空战类、塔防类、物理类游戏的开发过程和方法,实战性极强

    cocos2d-x 3.x 游戏开发实战光盘源码

    cocos2d-x 3.x 游戏开发实战光盘源码

    Cocos2d-X 3.X 游戏案例开发大全随书光盘源码,只要1分

    由于《Cocos2d-X 3.X 游戏案例开发大全》的随书光盘,听说好多人坏了,这里分享出一个网盘下载资源。我也是需要这个资源的一员,所以1分分享出来,希望能帮到你们。 里面总有9个项目的代码和资源,还有一个cocos2d-X...

Global site tag (gtag.js) - Google Analytics