什么是游戏开发的实体系统框架

关于游戏中的实体系统 --- Component-Based Entity System 首先关于游戏实体,最早有这个概念是看了这篇文章---游戏Entity设计不完全整理 最早接触游戏编程的时候,看的是老师的代码,类似于早期的游戏,使用的是最传统的继承模式的实体系统。当时就觉得继承就是面向对象的一切,认为只要不断的继承就能解决所有问题。到后来才发现,继承的一个严重的缺陷是你必须知道所有父类的具体实现,到了最后一个实体也变得非常臃肿且难以修改。后来接触了设计模式,知道了策略模式这东西 阅读详情

原文地址:http://blog.youkuaiyun.com/aisajiajiao/article/details/19011259

上周我发布了Ash,一个Actionscript游戏开发实体系统框架,许多人问我这个问题"什么是实体系统框架?"本文就是我详尽的答案.

实体系统逐渐流行起来了,比较著名的有像Unity,不太知名的框架像Actionscript框架Ember2,Xember和我自己的Ash.使用这个有一个很好的理由:它们简化了游戏架构,鼓励在你的代码中进行清晰的职责分工而且用起来很有趣.

在本文中我将向你展示一个基于实体的架构从经典而流行的游戏循环演变而来.这可能会花点时间.例子将使用Actionscript因为这正好是我当前使用的语言,但其架构理念可以用于所有的编程语言.

这基于我在2011年在try{harder}上的一次展示

例子

贯穿整篇文章,我将使用一个简单的小行星游戏作为一个例子.我比较喜欢使用小行星游戏是因为涉及到了大游戏中很多系统的简化版本-渲染,物理,ai,用户所控制的角色和npc.

游戏循环

要理解为什么我们使用实体系统,你得了解经典且流行的游戏循环.小行星游戏的游戏循环可能会类似下面展示的:

  1. function update(time:Number):void  
  2. {  
  3.     game.update(time);  
  4.     spaceship.updateInputs(time);  
  5.     for each (var flyingSaucer:FlyingSaucer in flyingSaucers)  
  6.     {  
  7.         flyingSaucer.updateAI(time);  
  8.     }  
  9.     spaceship.update(time);  
  10.     for each (var flyingSaucer:FlyingSaucer in flyingSaucers)  
  11.     {  
  12.         flyingSaucer.update(time);  
  13.     }  
  14.     for each (var asteroid:Asteroid in asteroids)  
  15.     {  
  16.         asteroid.update(time);  
  17.     }  
  18.     for each (var bullet:Bullet in bullets)  
  19.     {  
  20.         bullet.update(time);  
  21.     }  
  22.     collisionManager.update(time);  
  23.     spaceship.render();  
  24.     for each (var flyingSaucer:FlyingSaucer in flyingSaucers)  
  25.     {  
  26.         flyingSaucer.render();  
  27.     }  
  28.     for each (var asteroid:Asteroid in asteroids)  
  29.     {  
  30.         asteroid.render();  
  31.     }  
  32.     for each (var bullet:Bullet in bullets)  
  33.     {  
  34.         bullet.render();  
  35.     }  
  36. }  

游戏循环在固定的间隔被调用,通常是每秒60次或者每秒30次来更新游戏.当我们在每帧更新各种各样的游戏对象检查它们之间的碰撞然后将他们全部绘制时循环中的操作顺序是很重要的.

这是一个非常简单的游戏循环.它简单是因为:

  1. 游戏很简单
  2. 游戏只有一个状态
在以前我曾开发一些平台游戏(console games)其游戏循环,一个简单的函数超过了3000行代码.这不漂亮也不聪明.游戏就是这么开发的而且我们不得不忍受它.

实体系统架构衍生于解决游戏循环弊病的一种尝试.它将游戏循环看做游戏的核心,且预先假设在现代游戏架构中简化游戏循环比其他事物重要得多.例如,比视图和控制器分离重要得多.

进程

在这种演变中第一步要考虑的是被称作进程的对象.这些对象可被初始化,定期更新以及销毁.一个进程的接口类似如下所示:

  1. interface IProcess  
  2. {  
  3.     function start():Boolean;  
  4.     function update(time:Number):void;  
  5.     function end():void;  
  6. }  

如果我们将游戏循环拆分为许多进程来处理我们可以简化游戏循环,例如,渲染,移动,碰撞检测.要管理这些进程我们创建一个进程管理器.

  1. class ProcessManager  
  2. {  
  3.     private var processes:PriorirtiesdList;  
  4.       
  5.     public function addProcess(process:IProcess,priority:int):void  
  6.     {  
  7.         if(process.start())  
  8.         {  
  9.             processes.add(process,priority);  
  10.             return true;  
  11.         }  
  12.         return false;  
  13.     }  
  14.       
  15.     public function update(time:Number):void  
  16.     {  
  17.         for each(var process:IProcess in processes)  
  18.         {  
  19.             process.update(time);  
  20.         }  
  21.     }  
  22.       
  23.     public function removeProcess(process:IProcess):void  
  24.     {  
  25.         process.end();  
  26.         processes.remove(process);  
  27.     }  
  28. }  
这是一个稍微简化版的进程管理器.特别是我们要确定我们以正确的顺序更新进程(顺序由add方法中的priority参数指定)并且我们需要处理在更新循环中移除进程的情况.但你已经了解了概念.如果我们的游戏循环被拆分为多个进程,那我们进程管理器的更新方法是我们新的游戏循环且进程变成了游戏的核心.

渲染进程

作为一个例子我们看下渲染进程.我们需要将渲染代码从原来的游戏循环中抽出来并将其放置在一个进程中,代码类似如下:

  1. class RenderProcess implements IProcess  
  2. {  
  3.     public function start():Boolean  
  4.     {  
  5.         //initialize render system  
  6.         return true;  
  7.     }  
  8.       
  9.     public function update(time:Number):void  
  10.     {  
  11.         spaceship.render();  
  12.         for each(var flyingSaucer:FlyingSaucer in flyingSaucers)  
  13.         {  
  14.             flyingSaucer.render();  
  15.         }  
  16.           
  17.         for each(var asteriod:FlyingSaucer in asteriods)  
  18.         {  
  19.             asteriod.render();  
  20.         }  
  21.           
  22.         for each(var bullet:Bullet in bullets)  
  23.         {  
  24.             bullet.render();  
  25.         }  
  26.     }  
  27.       
  28.     public function end():void  
  29.     {  
  30.         //clean-up render system  
  31.     }  
  32. }  

使用一个接口
但这个并不是很有效率.我们仍旧必须手动渲染不同类型的游戏对象.如果对于所有的可渲染对象我们有一个通用接口,我们可以简化很多事情.

  1. interface IRenderable  
  2. {  
  3.     function render():void;  
  4. }  
  1. class RenderProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<IRenderable>;  
  4.       
  5.     public function start():Boolean  
  6.     {  
  7.         //initialize render system  
  8.         return true;  
  9.     }  
  10.       
  11.     public function update(time:Number):void  
  12.     {  
  13.         for each(var target:IRenderable in targets)  
  14.         {  
  15.             target.render();  
  16.         }  
  17.     }  
  18.       
  19.     public function end():void  
  20.     {  
  21.         //clean-up render system  
  22.     }  
  23. }  
之后我们的飞船类将包含一些类似下面的代码

  1. class Spaceship implements IRenderable  
  2. {  
  3.     public var view:DisplayObject;  
  4.     public var position:Point;  
  5.     public var rotation:Number;  
  6.       
  7.     public function render():void  
  8.     {  
  9.         view.x = position.x;  
  10.         view.y = position.y;  
  11.         view.rotation = rotation;  
  12.     }  
  13. }  
代码基于flash显示列表.如果我们使用blitting(早期创建像素游戏的一种做法)或者使用stage3d,可能会有些不同,但原理是相同的.我们需要要渲染的图形,渲染图形的位置和渲染值.渲染函数进行渲染.

使用基类和继承

事实上,这段代码中没有任何东西使其与飞船(spaceship)不同.所有的代码都被可渲染对象共享.它两唯一的不同之处是哪个显示对象被赋值给view属性以及位置和旋转值.因此我们将这代码封装(wrap)到基类并使用继承.

  1. class Renderable implements IRenderable  
  2. {  
  3.     public var view:DisplayObject;  
  4.     public var position:Point;  
  5.     public var rotation:Number;  
  6.       
  7.     public function render()  
  8.     {  
  9.         view.x = position.x;  
  10.         view.y = position.y;  
  11.         view.rotation = rotation;  
  12.     }  
  13. }  
  1. class Spaceship extends Renderable  
  2. {  
  3. }  
当然,所有的可渲染物件都会继承自renderable类,所以我们得到如下图所示的一个类层级关系图.


移动进程

要理解下一步,我们首先需要看下另外一个进程和其影响的类.因此我们尝试一下移动进程,它用于更新物体的位置.

  1. interface IMoveable  
  2. {  
  3.     function move(time:Number);  
  4. }  
  1. class MoveProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<IMoveable>;  
  4.       
  5.     public function start():Boolean  
  6.     {  
  7.         return true;  
  8.     }  
  9.       
  10.     public function update(time:Number):void  
  11.     {  
  12.         for each(var target:IMoveable in targets)  
  13.         {  
  14.             target.move(time);  
  15.         }  
  16.     }  
  17.       
  18.     public function end():void  
  19.     {  
  20.           
  21.     }  
  22. }  
  1. class Moveable implements IMoveable  
  2. {  
  3.     public var position:Point;  
  4.     public var rotation:Number;  
  5.     public var velocity:Point;  
  6.     public var angularVelocity:Number;  
  7.       
  8.     public function move(time:Number):void  
  9.     {  
  10.         position.x += velocity.x;  
  11.         position.y += velocity.y;  
  12.         rotation += angularVelocity;  
  13.     }  
  14. }  
  1. class Spaceship extends Moveable  
  2. {  
  3.       
  4. }  

多重继承

这还算好,但不幸的是我们需要我们的飞船既可以移动也可以被渲染出来,并且许多现代程序设计语言不允许多重继承.

即使是在这些允许多重继承的语言里,我们也有这样的问题,这个问题是Moveable类中的position与rotation应该与Renderable类中的position和rotation相等.

一个常用的解决方案是使用继承链,以便Moveable继承自Renderable

  1. class Moveable extends Renderable implements IMoveable  
  2. {  
  3.     public var velocity:Point;  
  4.     public var angularVelocity:Number;  
  5.       
  6.     public function move(time:Number):void  
  7.     {  
  8.         position.x += velocity.x;  
  9.         position.y += velocity.y;  
  10.         rotation += angularVelocity;  
  11.     }  
  12. }  
  1. class Spaceship extends Moveable  
  2. {  
  3. }  
现在飞船既可以移动又可以渲染.我们可将同样的原则应用于其他的游戏对象来得到这样的类层级


我们甚至可以有只继承自Renderable的静态对象.


可移动但是不可渲染(Moveable but not Renderable)

但如果我们想要一个可移动却不可渲染的对象将会怎样?比如,一个无形的游戏对象?这时我们的类层级失效了,我们需要一种Moveable接口不继承自Renderable的替代方案.

  1. class InvisibleMoveable implements IMoveable  
  2. {  
  3.     public var position:Point;  
  4.     public var rotation:Number;  
  5.     public var velocity:Point;  
  6.     public var angularVelocity:Number;  
  7.       
  8.     public function move(time:Number):void  
  9.     {  
  10.         position.x += velocity.x;  
  11.         position.y += velocity.y;  
  12.         rotation += angularVelocity;  
  13.     }  
  14. }  

在一个简单的游戏中,这样做虽然有点笨但还是掌控的,但在一个使用继承来将进程应用于对象的复杂的游戏中就会迅速变得不可控制,因为你很快发现游戏里的对象不适应一个简单的线性继承树,就像上面的force-field一样.

偏好使用合成超过继承(favour composition over inheritance)

长久以来面向对象编程的一条好原则就是多使用合成.在此处应用该原则可避免我们陷入潜在的继承紊乱.

我们仍旧需要Renderable和Moveable类,我们将创建包含这两个类实例的飞船类而不是继承这些类来创建飞船类.

  1. class Renderable implements IRenderable  
  2. {  
  3.     public var view:DisplayObject;  
  4.     public var position:Point;  
  5.     public var rotation:Number;  
  6.       
  7.     public function render(time:Number):void  
  8.     {  
  9.         view.x = position.x;  
  10.         view.y = position.y;  
  11.         view.rotation = rotation;  
  12.     }  
  13. }  
  1. class Moveable implements IMoveable  
  2. {  
  3.     public var position:Point;  
  4.     public var rotation:Number;  
  5.     public var velocity:Number;  
  6.     public var angularVelocity:Number;  
  7.       
  8.     public function move(time:Number):void  
  9.     {  
  10.         position.x += velocity.x;  
  11.         position.y += velocity.y;  
  12.         rotation += angularVelocity;  
  13.     }  
  14. }  
  1. class Spaceship  
  2. {  
  3.     public var renderData:IRenderable;  
  4.     public var moveData:IMoveable;  
  5. }  
使用这种方法,我们可以以我们想要的任何方式来合并多种行为(behaviours)而不用遭遇继承问题.


通过这种合成生成的对象,静态对象,太空船,飞碟,小行星,子弹和force field都叫做实体(entities).

我们的线程依旧没有变化

  1. interface IRenderable  
  2. {  
  3.     function render():void;  
  4. }  
  1. class RenderProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<IRenderable>;  
  4.   
  5.     public function update(time:Number):void  
  6.     {  
  7.         for each(var target:IRenderable in targets)  
  8.         {  
  9.             target.render();  
  10.         }  
  11.     }  
  12. }  
  1. interface IMoveable  
  2. {  
  3.     function move():void;  
  4. }  
  1. class MoveProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<IMoveable>;  
  4.       
  5.     public function update(time:Number):void  
  6.     {  
  7.         for each(var target:IMoveable in targets)  
  8.         {  
  9.             target.move(time);  
  10.         }  
  11.     }  
  12. }  
但我们不将飞船实体添加到每个线程而是添加飞船实体的成员.因此当我们创建飞船时所做如下:

  1. public function createSpaceship():Spaceship  
  2. {  
  3.     var spaceship:Spaceship = new Spaceship();  
  4.     ...  
  5.     renderProcess.addItem(spaceship.renderData);  
  6.     moveProcess.addItem(spaceship.moveData);  
  7.     ...  
  8.     return spaceship;  
  9. }  
这种方法看起来很棒.它使我们可以自由打乱和匹配不同游戏对象之间的进程支持而无需陷入继承链或者重复造轮子,但这有一个问题.

共享数据怎么样(What about the shared data?)

由于移动线程将会改变Moveable实例的值且渲染线程要使用Renderable实例的值,Renderable类实例中的position属性和rotation属性需要与Moveable类实例中的position和rotation属性数值相同.

  1. class Renderable implements IRenderable  
  2. {  
  3.     public var view:DisplayObject;  
  4.     public var position:Point;  
  5.     public var rotation:Number;  
  6.       
  7.     public function render():void  
  8.     {  
  9.         view.x = position.x;  
  10.         view.y = position.y;  
  11.         view.rotation = rotation;  
  12.     }  
  13. }  
  1. class Moveable implements IMoveable  
  2. {  
  3.     public var position:Point;  
  4.     public var rotation:Number;  
  5.     public var velocity:Point;  
  6.     public var angularVelocity:Number;  
  7.   
  8.     public function move(time:Number):void  
  9.     {  
  10.         position.x += velocity.x;  
  11.         position.y += velocity.y;  
  12.         rotation += angularVelocity;  
  13.     }  
  14. }  
  1. class Spaceship  
  2. {  
  3.     public var renderData:IRenderable;  
  4.     public var moveData:IMoveable;  
  5. }  
要解决这个问题,我们需要确保这两个类实例都指向这些属性的相同实例.在Actionscript中这意味着这些属性必须是对象,因为对象可以按引用传递而基础类型是按值传递的.

所以我们引入另一组类,我们称其为组件(components).这些组件是一些值对象,值对象将属性包裹在(wrap)对象中以便在进程间共享.

  1. class PositionComponent  
  2. {  
  3.     public var x:Number;  
  4.     public var y:Number;  
  5.     public var rotation:Number;  
  6. }  
  1. class VelocityComponent  
  2. {  
  3.     public var velocityX:Number;  
  4.     public var velocityY:Number;  
  5.     public var angularVelocity:Number;  
  6. }  
  1. class DisplayComponent  
  2. {  
  3.     public var view:DisplayObject;  
  4. }  
  1. class Renderable implements IRenderable  
  2. {  
  3.     public var display:DisplayComponent;  
  4.     public var position:PositionComponent;  
  5.       
  6.     public function render():void  
  7.     {  
  8.         display.view.x = position.x;  
  9.         display.view.y = position.y;  
  10.         display.view.rotation = position.rotation;  
  11.     }  
  12. }  
  1. class Moveable implements IMoveable  
  2. {  
  3.     public var position:PositionComponent;  
  4.     public var velocity:VelocityComponent;  
  5.       
  6.     public function move(time:Number):void  
  7.     {  
  8.         position.x += velocity.velocityX * time;  
  9.         position.y += velocity.velocityY * time;  
  10.         position.rotation += velocity.angularVelocity * time;  
  11.     }  
  12. }  
当我们创建飞船我们确保Moveable和Renderable实例共享相同的PositionComponent实例.

  1. class Spaceship  
  2. {  
  3.     public function Spaceship()  
  4.     {  
  5.         moveData = new Moveable();  
  6.         renderData = new Renderable();  
  7.         moveData.position = new PositionComponent();  
  8.         moveData.velocity = new VelocityComponent();  
  9.         renderData.position = moveData.position;  
  10.         renderData.display = new DisplayComponent();  
  11.     }  
  12. }  
进程仍没有受到该改变的影响.

暂停的好地方(A good place to pause)

到目前为止我们有了整齐分开的任务.游戏循环(game loop)循环遍历(cycle through)进程,调用每个进程上的更新方法.每个进程包含了一个对象集合,集合中的对象实现了其操作的接口并将调用这些对象的适当方法.这些对象每个都对其数据做单一而重要的作业(task).通过组件系统,这些对象能够共享数据,因此多个线程的组合能够在游戏实体中产生复杂的更新同时保证每个进程相对简单.

这个架构类似于游戏开发中的许多实体系统.架构遵循了良好的面向对象程序设计原则且能够运转.但在更多事情到来之前,我们先开始疯一把

放弃好的面向对象实践

当前的架构使用了良好的面向对象实践,像封装和单一职责原则(single responsibility)——IRenderable和IMoveable的实现封装了在每帧中游戏实体更新的单一职责的数据和逻辑——而合成——Spaceship实体是通过组合IRenderable和IMoveable接口的实现而创建的.通过组件系统(system of components)我们确保在恰当的地方数据在实体的不同数据类之间共享.

实体系统改革中的下一步或多或少有点不直观,打破了面向对象程序设计的核心教条之一.我们在Renderable和Moveable实现中打破了数据和逻辑的封装.特别是我们将逻辑从这些类中抽出反而将其放置在进程中

所以这个

  1. interface IRenderable  
  2. {  
  3.     function render();  
  4. }  
  1. class Renderable implements IRenderable  
  2. {  
  3.     public var display:DisplayComponent;  
  4.     public var position:PositionComponent;  
  5.   
  6.     public function render():void  
  7.     {  
  8.         display.view.x = position.x;  
  9.         display.view.y = position.y;  
  10.         display.view.rotation = position.rotation;  
  11.     }  
  12. }  
  1. class RenderProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<IRenderable>;  
  4.   
  5.     public function update(time:Number):void  
  6.     {  
  7.         for each (var target:IRenderable in targets)  
  8.         {  
  9.             target.render();  
  10.         }  
  11.     }  
  12. }  
变成了这个

  1. class RenderData  
  2. {  
  3.     public var display:DisplayComponent;  
  4.     public var position:PositionComponent;  
  5. }  
  1. class RenderProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<RenderData>;  
  4.       
  5.     public function update(time:Number):void  
  6.     {  
  7.         for each(var target:RenderData in targets)  
  8.         {  
  9.             target.display.view.x = target.position.x;  
  10.             target.display.view.y = target.position.y;  
  11.             target.display.view.rotation = target.position.rotation;  
  12.         }  
  13.     }  
  14. }  
而这个

  1. interface IMoveable  
  2. {  
  3.     function move(time:Number):void;  
  4. }  
  1. class Moveable implements IMoveable  
  2. {  
  3.     public var position:PositionComponent;  
  4.     public var velocity:VelocityComponent;  
  5.       
  6.     public function move(time:Number):void  
  7.     {  
  8.         position.x += velocity.velocityX * time;  
  9.         position.y += velocity.velocityY * time;  
  10.         position.rotation += velocity.angularVelocity * time;  
  11.     }  
  12. }  
  1. class MoveProcesss implements IProcess  
  2. {  
  3.     private var targets:Vector.<IMoveable>;  
  4.       
  5.     public function move(time:Number):void  
  6.     {  
  7.         for each(var target:Moveable in targets)  
  8.         {  
  9.             target.move(time);  
  10.         }  
  11.     }  
  12. }  
变成了这样

  1. class MoveData  
  2. {  
  3.     public var position:PositionComponent;  
  4.     public var velocity:VelocityComponent;  
  5. }  
  1. class MoveProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<MoveData>;  
  4.       
  5.     public function move(time:Number):void  
  6.     {  
  7.         target.position.x += target.velocity.velocityX * time;  
  8.         target.position.y += target.velocity.velocityY * time;  
  9.         target.position.rotation += target.velocity.angularVelocity * time;  
  10.     }  
  11. }  
(可能)不会立即明白为什么我们这么做,但是包涵一下.从表面上看,我们移除了接口的必要(removed the need for the interface),并且我们给线程更重要的事情来做——而不是简单的将其工作委托给IRenderable或者IMoveable的实现,它自己要做事(it does the work itself).

这样第一个明显的结果是所有的实体必须使用相同的渲染方法,因为渲染代码现在在RenderProcess里.但事实上并不这么回事.例如,我们能够有两个进程比如RenderMoveClip和RenderBitmap且它们能够处理不同的实体集.所以我们并没有丧失任何灵活性.

我们所得到的是重构我们实体的重要能力来产生一个架构,该架构有着明确的分工和简单的配置.重构以一个问题开始.

我们需要数据类吗?(Do we need the data classes)

当前,我们的实体

  1. class Spaceship  
  2. {  
  3.     public var moveData:MoveData;  
  4.     public var renderData:RenderData;  
  5. }  
包含两个数据类

  1. class MoveData  
  2. {  
  3.     public var position:PositionComponent;  
  4.     public var velocity:VelocityComponent;  
  5. }  
  1. class RenderData  
  2. {  
  3.     public var display:DisplayComponent;  
  4.     public var position:PositionComponent;  
  5. }  
这些数据类反过来包含三个组件

  1. class PositionComponent  
  2. {  
  3.     public var x:Number;  
  4.     public var y:Number;  
  5.     public var rotation:Number;  
  6. }  
  1. class VelocityComponent  
  2. {  
  3.     public var velocityX:Number;  
  4.     public var velocityY:Number;  
  5.     public var angularVelocity:Number;  
  6. }  
  1. class DisplayComponent  
  2. {  
  3.     public var view:DisplayObject;  
  4. }  
数据类由两个进程使用

  1. class MoveProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<MoveData>;  
  4.   
  5.     public function move(time:Number):void  
  6.     {  
  7.         for each(var target:MoveData in targets)  
  8.         {  
  9.             target.position.x += target.velocity.velocityX * time;  
  10.             target.position.y += target.velocity.velocityY * time;  
  11.             target.position.rotation += target.velocity.angularVelocity * time;  
  12.         }  
  13.     }  
  14. }  
  1. class RenderProcess implements IProcess  
  2. {  
  3.     private var targets:Vector.<RenderData>;  
  4.   
  5.     public function update(time:Number):void  
  6.     {  
  7.         for each(var target:RenderData in targets)  
  8.         {  
  9.             target.display.view.x = target.position.x;  
  10.             target.display.view.y = target.position.y;  
  11.             target.display.view.rotation = target.position.rotation;  
  12.         }  
  13.     }  
  14. }  
但是实体不应该关心数据类.组件共同(collectively)包含实体的状态.数据类为进程的方便而存在.因此我们重构代码以便spaceship实体包含组件(components)而不是数据类

  1. class Spaceship  
  2. {  
  3.     public var position:PositionComponent;  
  4.     public var velocity:PositionComponent;  
  5.     public var display:DisplayComponent;  
  6. }  
  1. class PositionComponent  
  2. {  
  3.     public var x:Number;  
  4.     public var y:Number;  
  5.     public var rotation:Number;  
  6. }  
  1. class VelocityComponent  
  2. {  
  3.     public var velocityX:Number;  
  4.     public var velocityY:Number;  
  5. }  
  1. class DisplayComponent  
  2. {  
  3.     public var view:DisplayObject;  
  4. }  
通过移除数据类,使用构成组件(constituent components)替代来定义spaceship,我们移除了spaceship实体需要知道那个进程或许会对其起作用的必要(we have removed any need for the spaceship entity to know what process may act on it).spaceship现在包含定义其状态的组件.任何为进程要求合并这些组件到其他数据类都是别的类的事儿.

系统与节点

当进程需要时,实体系统框架(我们很快会接触到)中的一些核心代码会动态的创建这些数据对象.在这个简化的语境(reduced context)中,数据类将只是由进程使用的集合(数组,链表或者别的依据实现而不同)中的节点.所以为弄清楚这我们将它们重命名为节点.

  1. class MoveNode  
  2. {  
  3.     public var position:PositionComponent;  
  4.     public var velocity:VelocityComponent;  
  5. }  
  1. class RenderNode  
  2. {  
  3.     public var display:DisplayComponent;  
  4.     public var position:PositionComponent;  
  5. }  
进程并没有改变,但是为了保持共同的命名约定我也将它们的名称改变并称其为系统.

  1. class MoveSystem implements ISystem  
  2. {  
  3.     private var targets:Vector.<MoveNode>;  
  4.       
  5.     public function update(time:Number):void  
  6.     {  
  7.         for each(var target:MoveNode in targets)  
  8.         {  
  9.             target.position.x += target.velocity.velocityX * time;  
  10.             target.position.y += target.velocity.velocityY * time;  
  11.             target.position.rotation += target.velocity.angularVelocity * time;  
  12.         }  
  13.     }  
  14. }  
  1. class RenderSystem implements ISystem  
  2. {  
  3.     private var targets:Vector.<RenderNode>;  
  4.       
  5.     public function update(time:Number):void  
  6.     {  
  7.         target.display.view.x = target.position.x;  
  8.         target.display.view.y = target.position.y;  
  9.         target.display.view.rotation = target.position.rotation;  
  10.     }  
  11. }  
  1. interface ISystem  
  2. {  
  3.     function update(time:Number):void;  
  4. }  

那实体又是什么呢?(And what is an entity?)

最后一点改变--Spaceship类没有什么特别的.他只是一个组件容器.所以我们将只称其为实体并给它一个组件集合.我们将根据这些组件的类类型(class type)来访问它们.

  1. class Entity  
  2. {  
  3.     private var components:Dictionary;  
  4.       
  5.     public function add(component:Object):void  
  6.     {  
  7.         var componentClass:Class = component.constructor;  
  8.         components[componentClass] = component;  
  9.     }  
  10.       
  11.     public function remove(componentClass:Class):void  
  12.     {  
  13.         delete components[componentClass];  
  14.     }  
  15.       
  16.     public function get(componentClass:Class):void  
  17.     {  
  18.         return components[componentClass];  
  19.     }  
  20. }  
所以我们像这样创建我们的spaceship

  1. public function createSpaceship():void  
  2. {  
  3.     var spaceship:Entity = new Entity();  
  4.     var position:PositionComponent = new PositionComponent();  
  5.     position.x = Stage.stageWidth / 2;  
  6.     position.y = Stage.stageHeight / 2;  
  7.     position.rotation = 0;  
  8.     spaceship.add(position);  
  9.     var display:DisplayComponent = new DisplayComponent();  
  10.     display.view = new SpaceshipImage();  
  11.     spaceship.add(display);  
  12.     engine.add(spaceship);  
  13. }  

核心的引擎类

我们决不能忘记系统管理器,之前叫做进程管理器

  1. class SystemManager  
  2. {  
  3.     private var systems:PriorirtiesdList;  
  4.       
  5.     public function addSystem(system:ISystem,priority:int):void  
  6.     {  
  7.         systems.add(system,priority);  
  8.         system.start();  
  9.     }  
  10.       
  11.     public function update():void  
  12.     {  
  13.         for each(var system:ISystem in systems)  
  14.         {  
  15.             system.update(time);  
  16.         }  
  17.     }  
  18.       
  19.     public function removeSystem(system:ISystem):void  
  20.     {  
  21.         system.end();  
  22.         systems.remove(system);  
  23.     }  
  24. }  

这将会被加强(enhanced)并处于我们实体系统框架的核心.我们将为其添加我们之前提到的功能以便为系统动态创建节点.

实体只关心组件,系统只关心节点.因此,要完成实体系统框架,当实体改变,为系统所用的节点结合添加或者删除组件式,我们需要监视实体的代码.因为这是实体和系统都知道的那一点代码,我们可能考虑将其作为游戏的中心.在Ash中,我称这为Engine类,它是系统管理器的一个增强版.

当你开始使用或停止使用Engine类时,每个实体和系统都被添加到或者从Engine类删除.Engine类跟踪实体上的组件并创建(和在必要时销毁)节点,将这些节点添加到节点集合中.Engine类也为系统提供了一种方法来得到其需要的集合.

  1. public class Engine  
  2. {  
  3.     private var entities:EntityList;  
  4.     private var systems:EntityList;  
  5.     private var nodeLists:Dictionary;  
  6.   
  7.     public function addEntity(entity:Entity):void  
  8.     {  
  9.         entities.add(entity);  
  10.         //create nodes from this entity's components and add them to node lists  
  11.         //also watch for later addition and removal of components from the entity so  
  12.         //you can adjust its derived nodes accordingly  
  13.     }  
  14.   
  15.     public function removeEntity(entity:Entity):void  
  16.     {  
  17.         //destory nodes from this entity's components  
  18.         //and remove them from the node lists  
  19.         entities.remove(entity);  
  20.     }  
  21.   
  22.     public function addSystem(system:System,priority:int):void  
  23.     {  
  24.         systems.add(system,priority);  
  25.         system.start();  
  26.     }  
  27.   
  28.     public function removeSystem(system:System):void  
  29.     {  
  30.         system.end();  
  31.         systems.remove(system);  
  32.     }  
  33.   
  34.     public function getNodeList(nodeClass:Class):NodeList  
  35.     {  
  36.         var nodes:NodeList = new NodeList();  
  37.         nodeLists[nodeClass] = nodes;  
  38.         //create the nodes from the current set of entities  
  39.         //and populate the node list  
  40.         return nodes;  
  41.     }  
  42.   
  43.     public function update(time:Number):void  
  44.     {  
  45.         for each(var system:ISystem in systems)  
  46.         {  
  47.             system.update(time);  
  48.         }  
  49.     }  
  50. }  

要查看该架构的一个实现,checkout Ash实体系统框架并看看例子小行星游戏的实现.

结论

所以,总结一下,实体系统发源于想要简化游戏循环.从哪里衍生出了实体架构,它代表着游戏的状态和系统,系统作用于游戏状态.系统在每帧中都会更新--这就是游戏循环.实体由组件(components)构成,系统作用于含有它们感兴趣的组件的实体.引擎管理者系统和实体并确保每个系统能够访问到一组有对应组件的实体集合.

然而,系统通常不关心作为整体的实体,只关心它们需要的特定组件.因此,要优化该架构并提供额外的净化度(additional clarity),系统作用于静态类型的包含对应组件的节点对象,在节点对象中这些组件都属于同一个实体.

一个实体系统框架为这种架构提供了基本的架子(scaffolding)和核心管理,没有提供任何实际的实体或者系统类.你通过创建对应的实体和系统来创建游戏.

一个基于实体的游戏引擎将在基本的框架上提供许多标准的系统和实体.

3个Actionscript实体系统框架是我自己的Ash,Tom Davies的Ember2和Alec McEachran的Xember.Artemis是一个java的实体系统框架,且已经移植到了C#.

那些年对支付宝的研究[整理] 支付宝功能强大.他内部各个功能间的跳转是通过Uri 实现的.而使用window.location.href 即可.跟浏览器一样的协议.很方便 H5启动支付宝: alipays://platformapi/startapp?appId=20000067&url=网址 alipays://platformapi/startapp?appId=20000691&url=网址 ali... 阅读详情

相关推荐

jQuery 版本怎么选?别一上来就用最新版,老项目里这个坑很常见

jQuery 是一个封装好的 JavaScript 函数库,简称 JQ它的核心理念是jQuery 有 1.x、2.x、3.x 三个主要版本系列2.0 以上版本不再支持 IE6、IE7、IE8版本选择不能只看“新不新”,而要看项目兼容性和实际需求.min.js是压缩版文件,生产环境常用使用 jQuery 前必须先通过<script>正确引入基本就是没引入或引入失败可以快速绑定点击事件.animate()可以快速实现动画效果,但定位属性要设置好。

Web安全工具库 386

什么是游戏开发的实体系统框架 What is an entity system framework for game development

游戏循环 要理解我们为什么要使用实体系统,你确实需要理解老式的游戏循环.游戏小行星的游戏循环可能看起来如下所示:

7062

手把手教你用Excel自动化处理G14大额风险暴露报表(含211版模板)

本文针对G14大额风险暴露报表(含211版)手工填报效率低、易出错的核心痛点,提供了一套基于Excel VBA的完整自动化解决方案。文章详细讲解了如何构建从数据采集、清洗、关联客户识别、风险暴露计算到自动化校验与归档的完整工作流,并附有可操作的代码示例,旨在帮助银行风控人员大幅提升报表处理效率与准确性。

weixin_29304985的博客 447

【软件架构】ECS游戏架构

通过继承联系起来的对象,一旦某个共同功能需要改动,或增加功能,就要调整类的结构。不处于同一条继承线中的对象如果有相同的功能,也无法通过继承的方式复用代码,造成了代码重复。现在我们尝试。

无数_Mirage的博客 1万+

Material Studio 2024安装避坑指南:从证书配置到权限设置的完整解决方案

本文详细解析了Material Studio 2024的安装流程,从环境准备到权限优化的完整解决方案。针对证书配置、系统权限和计算服务等常见问题,提供了12个关键风险点的避坑指南,帮助科研工作者高效完成安装,提升材料模拟工作的效率。

moon9的博客 772

告别Modern Standby噩梦:Win11睡眠断网冻结的根源剖析与S3睡眠回归实战

本文深入剖析了Windows 11中Modern Standby(现代待机)模式导致睡眠后断网、系统冻结等问题的根源,指出其与硬件驱动、后台活动的兼容性矛盾。文章提供了通过修改注册表键值强制系统回归稳定S3睡眠状态的详细实战步骤,帮助用户彻底解决睡眠故障,实现可靠的远程访问与挂机任务。

weixin_29210727的博客 604

游戏里的实体系统

  游戏世界是一个乱哄哄的世界,乱哄哄的各种各样的人物,怪物奔跑着,发射着五花八门的子弹;各种古里古怪的地形,会吃人的建筑物;如果“上帝”愿意,随时可以刮风下雨。哦,MG!我的头脑里容不下这么多东西了,这简直是场灾难。   我希望把他们统一起来,而不是区别对待;我希望用一种逻辑来控制管理他们,而不是一万种。   可以做到吗?   很高兴,可以。   把这些游戏元素统一成一个整体,并且提供一...

weixin_30252155的博客 195

STM32F407硬件SPI驱动ADS1220避坑指南:从寄存器读不到数据到采样值异常的完整解决流程

本文详细解析了STM32F407硬件SPI驱动ADS1220的常见问题与解决方案,包括SPI配置、寄存器读取异常、采样值优化等关键环节。通过CubeMX配置技巧和Keil调试经验,帮助开发者快速排查从通信失败到数据异常的各种问题,实现高精度数据采集系统的稳定运行。

weixin_26878995的博客 157

浅谈游戏系统

浅谈游戏系统 某日,Brandon突发奇想(脑子抽了)买了《异度之刃2》开始游玩,虽然画质不如原神,但他还是沉下心来连续游玩了10个小时。”这游戏战斗系统怎么样?“ 舍友随口一问瞬间让Brandon摸不着头脑,异刃连击,御刃者连击,融合连击等各种攻击方式以及眼花缭乱的属性加成真的太多了,他脑袋里根本不知道战斗系统是什么样的。 Brandon决心学习了游戏系统,而这,就是本文的由来 基本概念 定义 “A system can be defined as a set of elements standin

m0_52534277的博客 8970

ShengBTE热导计算:利用ShengBTE计算热导显示NAN的原因是什么?算出的热导不随平均声子自由程变化

🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值 。

**My Coding Family** 963

基于阿里云物联网平台设计的实时图传系统_采用MQTT协议传输图像

当前基于MQTT协议设计了一个实时图传系统,通过这个项目来演示,两个MQTT设备如何互相订阅,进行消息流转。 在阿里云服务器上创建2个设备,分为为设备A和设备B;设备A负责采集本地摄像头画面上传,设备B负责接收设备A上传的数据然后解析显示出来。在阿里云服务器上需要配置云产品流转,让设备A的数据上传后自动发送给设备B。这样就完成了视频画面数据的流转。不过因为阿里云的最大数据限制,每次最大发送10240字节的数据。

1万+

【译】什么是游戏开发实体系统框架(一)

什么是游戏开发实体系统框架(一) 原文链接:http://www.richardlord.net/blog/what-is-an-entity-framework 原文很长,译文将分开几部分,此为第一部分。 上周我发布了Ash(一个Actionscript 游戏开发的实体系统框架),后来很多朋友问我说:“什么是实体系统框架(entity system framework...

weixin_30530339的博客 142

软件集成:Simulink与STM32联合开发

本文首先通过一个简单的Simulink模型生成代码,然后将代码copy到一个STM32工程中去编译软件。最后将软件下载到STM32F407中,通过串口输出Simulink计算的结果。 阅读本文需要有一定的STM32开发调试经验和Simulink代码生成经验。关于Simulink代码生成可以参考博主的专栏《Simulink代码生成》。 文章目录1 问题引入2 集成方案3 建模与编程过程3.1 Simulink模型搭建3.2 代码生成配置3.3 STM32工程搭建3.4 STM32的main.c文件修改3.4.

u013288925的博客 2万+

探索高效的游戏开发框架:ECS(实体组件系统)

探索高效的游戏开发框架:ECS(实体组件系统) ECSC++ single-header entity component system library项目地址:https://gitcode.com/gh_mirrors/ecs7/ECS 在这个快速发展的游戏开发领域中,寻找一款能够简化复杂性、提高性能的工具库至关重要。这就是ECS——一个简单而强大的C++头文件型安全实体组件系统库,它以其类...

gitblog_00071的博客 600

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

探索高效游戏开发新境界:Mix——轻量级实体组件系统框架

探索高效游戏开发新境界:Mix——轻量级实体组件系统框架 项目介绍 Mix是一款简约而强大的实体组件系统(ECS),专为简化游戏和复杂应用的架构设计而生。在ECS设计模式的基石上,Mix提供了一种高效管理游戏对象的方式,通过分离数据(组件)和逻辑(系统),它极大地提高了代码的可维护性和灵活性。 项目技术分析 Mix基于C++实现,其核心设计围绕实体(entity)、组件(component)和系统...

gitblog_00638的博客 713

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

【译】游戏《实体系统框架》译文——序

一直想搞个牛逼的技术博客,但发现总是半途而废、虎头蛇尾,后借口曰:时间太少,文笔太差。 某日突发灵感,自己写不了,翻译总是可以的吧?国外那么多好文章,不拿来给大大们分享岂不浪费? 遂,诞生“译文”系列,网罗自以为精彩的技术及非技术外文文章,暴力翻译之。 此为第一篇,据说是目前比较流行的一种游戏开发框架:“实体系统框架”,简单浏览有些意思,拿来练手。 先放出原文链接,...

weixin_30375427的博客 85

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

【亲测免费】 探索ecs:一个强大的Go语言实体组件系统框架

探索ecs:一个强大的Go语言实体组件系统框架 在游戏开发和复杂应用构建中,高效管理实体、组件和系统是至关重要的。今天,我们将深入介绍一个开源项目——ecs,这是一个用Go语言实现的实体组件系统(Entity Component System, ECS)模型。本文将详细解析ecs的项目结构、技术特点以及应用场景,帮助开发者更好地理解和利用这一工具。 项目介绍 ecs是一个独立的Go语言库,旨在提供...

gitblog_00941的博客 722

探索Flecs:高效轻量的实体组件系统框架

Flecs是一款快速且轻量级的实体组件系统(ECS)框架,专为构建游戏和模拟应用而设计。它支持数百万实体的高效管理,提供了丰富的功能和优化的性能,使得开发者能够轻松创建复杂且易于扩展的游戏和模拟项目。 ## 项目技术分析 Flecs的核心优势在于其高效的实体管理、零依赖的C99 API、现代的C++11 API、以及对实体关系的全面支持。其架构设计注重性能和可移植性,能够在多种平台上运行,包括...

gitblog_00023的博客 835

Artemis-ODB:高效Java实体-组件系统框架指南

Artemis-ODB 是一款基于Java的高性能实体-组件系统(ECS)框架,专为游戏开发设计。它不仅成熟且得到持续维护,而且是流行Artemis框架的一个延续和发展。此框架强调支持Android、HTML5和iOS平台,具有低垃圾回收影响、减少冗余代码的特点,并通过可选的自动池化组件及热点优化来进一步提升性能。Artemis-ODB还支持序列化到JSON或二进制格式,并提供了流畅接口以加快原型...

gitblog_00107的博客 608

Anax C++实体系统使用指南

Anax C++实体系统使用指南 项目介绍 Anax是一款开源的C++实体系统框架,设计得轻量级、便携且易于使用,专为游戏开发而生,但也适用于其他类型的项目。该框架采用了实体-组件-系统(ECS)的设计模式,将功能分解成三个主要部分:组件存储数据、系统包含游戏逻辑,以及实体用于描述游戏中的对象。 项目快速启动 环境需求 确保您的开发环境支持C++11标准。 安装步骤 克隆仓库: 使用Git克隆...

gitblog_00043的博客 388

ET框架完全解析:从入门到精通的高性能游戏开发指南

ET框架作为Unity3D客户端与C#服务器双端开发解决方案,正在重新定义分布式游戏开发的标准。这套开源框架通过创新的架构设计和卓越的性能表现,为开发者提供了前所未有的开发体验。 ## 项目概览与技术特色 ET框架的核心价值在于其革命性的双端一致性设计。客户端与服务器使用相同的C#语言和架构模式,大幅降低了开发复杂度。框架采用组件化实体模型,让游戏逻辑与数据完全分离,实现高度模块化的开发流程。

gitblog_00611的博客 954

FXGL:JavaFX游戏开发框架全解析

FXGL是一个基于JavaFX构建的开源游戏开发框架,提供从2D到3D游戏开发的完整解决方案。作为Java游戏框架的佼佼者,它通过简洁API和模块化设计,帮助开发者快速构建跨平台游戏应用,支持Windows、Mac、Linux、Android及Web等多平台部署。 ## 🎮 框架简介 ### 核心功能概览 FXGL作为一站式游戏开发框架,整合了游戏开发所需的各类核心功能: - **实体组件系

gitblog_01026的博客 238

推荐文章:探索高效游戏开发——tiny-ecs带你领略轻量级实体组件系统的世界

在游戏开发的浩瀚星空中,一款名为**tiny-ecs**的轻量级实体组件系统(Entity Component System)正以其独特的魅力,成为Lua开发者的新宠。尽管项目几年来几乎未见新的提交记录,但这绝非意味着它的终结;相反,它已成熟稳定,无明显bug报错,静待新老开发者探索其深邃潜能。通过GitHub,任何新问题都将得到响应。 ![Build Status](https://travi

gitblog_00589的博客 1019

【亲测免费】 Artemis-ODB: 一款强大的Java游戏开发框架

是一个高效、灵活的Java游戏开发框架,它基于组件实体系统(Component Entity System, CES)架构设计。本篇文章将向您介绍Artemis-ODB的核心特性,并展示如何利用其进行游戏开发。 ## Artemis-ODB能用来做什么? 1. 快速搭建游戏核心逻辑 2. 提供高性能的游戏循环机制 3. 支持组件化的设计模式,便于代码组织与复用 4. 灵活的角色配置及行为实现 ...

gitblog_00089的博客 945

FXGL游戏开发框架:JavaFX生态下的高性能游戏引擎解决方案

在Java游戏开发领域,开发者常常面临性能瓶颈、复杂物理模拟和跨平台兼容性等多重挑战。传统JavaFX虽然提供了丰富的UI组件,但缺乏专门针对游戏开发的优化架构。FXGL作为基于JavaFX的游戏开发框架,通过创新的实体组件系统、高性能物理引擎和现代化DSL设计,为Java开发者提供了完整的游戏开发解决方案,显著降低了2D/3D游戏开发的技术门槛。 ## 传统游戏开发的痛点与挑战 Java游戏

gitblog_00532的博客 174

ECS设计

1、老外写的一篇讲解:什么是游戏开发的实体系统框架(https://www.richardlord.net/blog/ecs/what-is-an-entity-framework.html),对应的译文:https://blog.youkuaiyun.com/aisajiajiao/article/details/19011259 2、过程比较详细的基本框架(JS实现):如何通过实体组件系统在Javasc...

Zzz的专栏 822
上一篇: 《游戏引擎架构》读书笔记(三)
下一篇: Lua学习笔记--环境配置&Hello World
puppet_master
puppet_master 领域专家: C/C++技术领域 领域专家: C/C++技术领域
博客等级 码龄12年 2695粉丝 161原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值