原文地址:http://blog.csdn.net/aisajiajiao/article/details/19011259
上周我发布了Ash,一个Actionscript游戏开发实体系统框架,许多人问我这个问题"什么是实体系统框架?"本文就是我详尽的答案.
实体系统逐渐流行起来了,比较著名的有像Unity,不太知名的框架像Actionscript框架Ember2,Xember和我自己的Ash.使用这个有一个很好的理由:它们简化了游戏架构,鼓励在你的代码中进行清晰的职责分工而且用起来很有趣.
在本文中我将向你展示一个基于实体的架构从经典而流行的游戏循环演变而来.这可能会花点时间.例子将使用Actionscript因为这正好是我当前使用的语言,但其架构理念可以用于所有的编程语言.
这基于我在2011年在try{harder}上的一次展示
例子
贯穿整篇文章,我将使用一个简单的小行星游戏作为一个例子.我比较喜欢使用小行星游戏是因为涉及到了大游戏中很多系统的简化版本-渲染,物理,ai,用户所控制的角色和npc.
游戏循环
要理解为什么我们使用实体系统,你得了解经典且流行的游戏循环.小行星游戏的游戏循环可能会类似下面展示的:
- function update(time:Number):void
- {
- game.update(time);
- spaceship.updateInputs(time);
- for each (var flyingSaucer:FlyingSaucer in flyingSaucers)
- {
- flyingSaucer.updateAI(time);
- }
- spaceship.update(time);
- for each (var flyingSaucer:FlyingSaucer in flyingSaucers)
- {
- flyingSaucer.update(time);
- }
- for each (var asteroid:Asteroid in asteroids)
- {
- asteroid.update(time);
- }
- for each (var bullet:Bullet in bullets)
- {
- bullet.update(time);
- }
- collisionManager.update(time);
- spaceship.render();
- for each (var flyingSaucer:FlyingSaucer in flyingSaucers)
- {
- flyingSaucer.render();
- }
- for each (var asteroid:Asteroid in asteroids)
- {
- asteroid.render();
- }
- for each (var bullet:Bullet in bullets)
- {
- bullet.render();
- }
- }
游戏循环在固定的间隔被调用,通常是每秒60次或者每秒30次来更新游戏.当我们在每帧更新各种各样的游戏对象检查它们之间的碰撞然后将他们全部绘制时循环中的操作顺序是很重要的.
这是一个非常简单的游戏循环.它简单是因为:
- 游戏很简单
- 游戏只有一个状态
实体系统架构衍生于解决游戏循环弊病的一种尝试.它将游戏循环看做游戏的核心,且预先假设在现代游戏架构中简化游戏循环比其他事物重要得多.例如,比视图和控制器分离重要得多.
进程
在这种演变中第一步要考虑的是被称作进程的对象.这些对象可被初始化,定期更新以及销毁.一个进程的接口类似如下所示:
- interface IProcess
- {
- function start():Boolean;
- function update(time:Number):void;
- function end():void;
- }
如果我们将游戏循环拆分为许多进程来处理我们可以简化游戏循环,例如,渲染,移动,碰撞检测.要管理这些进程我们创建一个进程管理器.
- class ProcessManager
- {
- private var processes:PriorirtiesdList;
- public function addProcess(process:IProcess,priority:int):void
- {
- if(process.start())
- {
- processes.add(process,priority);
- return true;
- }
- return false;
- }
- public function update(time:Number):void
- {
- for each(var process:IProcess in processes)
- {
- process.update(time);
- }
- }
- public function removeProcess(process:IProcess):void
- {
- process.end();
- processes.remove(process);
- }
- }
渲染进程
作为一个例子我们看下渲染进程.我们需要将渲染代码从原来的游戏循环中抽出来并将其放置在一个进程中,代码类似如下:
- class RenderProcess implements IProcess
- {
- public function start():Boolean
- {
- //initialize render system
- return true;
- }
- public function update(time:Number):void
- {
- spaceship.render();
- for each(var flyingSaucer:FlyingSaucer in flyingSaucers)
- {
- flyingSaucer.render();
- }
- for each(var asteriod:FlyingSaucer in asteriods)
- {
- asteriod.render();
- }
- for each(var bullet:Bullet in bullets)
- {
- bullet.render();
- }
- }
- public function end():void
- {
- //clean-up render system
- }
- }
使用一个接口
但这个并不是很有效率.我们仍旧必须手动渲染不同类型的游戏对象.如果对于所有的可渲染对象我们有一个通用接口,我们可以简化很多事情.
- interface IRenderable
- {
- function render():void;
- }
- class RenderProcess implements IProcess
- {
- private var targets:Vector.<IRenderable>;
- public function start():Boolean
- {
- //initialize render system
- return true;
- }
- public function update(time:Number):void
- {
- for each(var target:IRenderable in targets)
- {
- target.render();
- }
- }
- public function end():void
- {
- //clean-up render system
- }
- }
- class Spaceship implements IRenderable
- {
- public var view:DisplayObject;
- public var position:Point;
- public var rotation:Number;
- public function render():void
- {
- view.x = position.x;
- view.y = position.y;
- view.rotation = rotation;
- }
- }
使用基类和继承
事实上,这段代码中没有任何东西使其与飞船(spaceship)不同.所有的代码都被可渲染对象共享.它两唯一的不同之处是哪个显示对象被赋值给view属性以及位置和旋转值.因此我们将这代码封装(wrap)到基类并使用继承.
- class Renderable implements IRenderable
- {
- public var view:DisplayObject;
- public var position:Point;
- public var rotation:Number;
- public function render()
- {
- view.x = position.x;
- view.y = position.y;
- view.rotation = rotation;
- }
- }
- class Spaceship extends Renderable
- {
- }
移动进程
要理解下一步,我们首先需要看下另外一个进程和其影响的类.因此我们尝试一下移动进程,它用于更新物体的位置.
- interface IMoveable
- {
- function move(time:Number);
- }
- class MoveProcess implements IProcess
- {
- private var targets:Vector.<IMoveable>;
- public function start():Boolean
- {
- return true;
- }
- public function update(time:Number):void
- {
- for each(var target:IMoveable in targets)
- {
- target.move(time);
- }
- }
- public function end():void
- {
- }
- }
- class Moveable implements IMoveable
- {
- public var position:Point;
- public var rotation:Number;
- public var velocity:Point;
- public var angularVelocity:Number;
- public function move(time:Number):void
- {
- position.x += velocity.x;
- position.y += velocity.y;
- rotation += angularVelocity;
- }
- }
- class Spaceship extends Moveable
- {
- }
多重继承
这还算好,但不幸的是我们需要我们的飞船既可以移动也可以被渲染出来,并且许多现代程序设计语言不允许多重继承.
即使是在这些允许多重继承的语言里,我们也有这样的问题,这个问题是Moveable类中的position与rotation应该与Renderable类中的position和rotation相等.
一个常用的解决方案是使用继承链,以便Moveable继承自Renderable
- class Moveable extends Renderable implements IMoveable
- {
- public var velocity:Point;
- public var angularVelocity:Number;
- public function move(time:Number):void
- {
- position.x += velocity.x;
- position.y += velocity.y;
- rotation += angularVelocity;
- }
- }
- class Spaceship extends Moveable
- {
- }
我们甚至可以有只继承自Renderable的静态对象.
可移动但是不可渲染(Moveable but not Renderable)
但如果我们想要一个可移动却不可渲染的对象将会怎样?比如,一个无形的游戏对象?这时我们的类层级失效了,我们需要一种Moveable接口不继承自Renderable的替代方案.
- class InvisibleMoveable implements IMoveable
- {
- public var position:Point;
- public var rotation:Number;
- public var velocity:Point;
- public var angularVelocity:Number;
- public function move(time:Number):void
- {
- position.x += velocity.x;
- position.y += velocity.y;
- rotation += angularVelocity;
- }
- }
在一个简单的游戏中,这样做虽然有点笨但还是掌控的,但在一个使用继承来将进程应用于对象的复杂的游戏中就会迅速变得不可控制,因为你很快发现游戏里的对象不适应一个简单的线性继承树,就像上面的force-field一样.
偏好使用合成超过继承(favour composition over inheritance)
长久以来面向对象编程的一条好原则就是多使用合成.在此处应用该原则可避免我们陷入潜在的继承紊乱.
我们仍旧需要Renderable和Moveable类,我们将创建包含这两个类实例的飞船类而不是继承这些类来创建飞船类.
- class Renderable implements IRenderable
- {
- public var view:DisplayObject;
- public var position:Point;
- public var rotation:Number;
- public function render(time:Number):void
- {
- view.x = position.x;
- view.y = position.y;
- view.rotation = rotation;
- }
- }
- class Moveable implements IMoveable
- {
- public var position:Point;
- public var rotation:Number;
- public var velocity:Number;
- public var angularVelocity:Number;
- public function move(time:Number):void
- {
- position.x += velocity.x;
- position.y += velocity.y;
- rotation += angularVelocity;
- }
- }
- class Spaceship
- {
- public var renderData:IRenderable;
- public var moveData:IMoveable;
- }
通过这种合成生成的对象,静态对象,太空船,飞碟,小行星,子弹和force field都叫做实体(entities).
我们的线程依旧没有变化
- interface IRenderable
- {
- function render():void;
- }
- class RenderProcess implements IProcess
- {
- private var targets:Vector.<IRenderable>;
- public function update(time:Number):void
- {
- for each(var target:IRenderable in targets)
- {
- target.render();
- }
- }
- }
- interface IMoveable
- {
- function move():void;
- }
- class MoveProcess implements IProcess
- {
- private var targets:Vector.<IMoveable>;
- public function update(time:Number):void
- {
- for each(var target:IMoveable in targets)
- {
- target.move(time);
- }
- }
- }
- public function createSpaceship():Spaceship
- {
- var spaceship:Spaceship = new Spaceship();
- ...
- renderProcess.addItem(spaceship.renderData);
- moveProcess.addItem(spaceship.moveData);
- ...
- return spaceship;
- }
共享数据怎么样(What about the shared data?)
由于移动线程将会改变Moveable实例的值且渲染线程要使用Renderable实例的值,Renderable类实例中的position属性和rotation属性需要与Moveable类实例中的position和rotation属性数值相同.
- class Renderable implements IRenderable
- {
- public var view:DisplayObject;
- public var position:Point;
- public var rotation:Number;
- public function render():void
- {
- view.x = position.x;
- view.y = position.y;
- view.rotation = rotation;
- }
- }
- class Moveable implements IMoveable
- {
- public var position:Point;
- public var rotation:Number;
- public var velocity:Point;
- public var angularVelocity:Number;
- public function move(time:Number):void
- {
- position.x += velocity.x;
- position.y += velocity.y;
- rotation += angularVelocity;
- }
- }
- class Spaceship
- {
- public var renderData:IRenderable;
- public var moveData:IMoveable;
- }
所以我们引入另一组类,我们称其为组件(components).这些组件是一些值对象,值对象将属性包裹在(wrap)对象中以便在进程间共享.
- class PositionComponent
- {
- public var x:Number;
- public var y:Number;
- public var rotation:Number;
- }
- class VelocityComponent
- {
- public var velocityX:Number;
- public var velocityY:Number;
- public var angularVelocity:Number;
- }
- class DisplayComponent
- {
- public var view:DisplayObject;
- }
- class Renderable implements IRenderable
- {
- public var display:DisplayComponent;
- public var position:PositionComponent;
- public function render():void
- {
- display.view.x = position.x;
- display.view.y = position.y;
- display.view.rotation = position.rotation;
- }
- }
- class Moveable implements IMoveable
- {
- public var position:PositionComponent;
- public var velocity:VelocityComponent;
- public function move(time:Number):void
- {
- position.x += velocity.velocityX * time;
- position.y += velocity.velocityY * time;
- position.rotation += velocity.angularVelocity * time;
- }
- }
- class Spaceship
- {
- public function Spaceship()
- {
- moveData = new Moveable();
- renderData = new Renderable();
- moveData.position = new PositionComponent();
- moveData.velocity = new VelocityComponent();
- renderData.position = moveData.position;
- renderData.display = new DisplayComponent();
- }
- }
暂停的好地方(A good place to pause)
到目前为止我们有了整齐分开的任务.游戏循环(game loop)循环遍历(cycle through)进程,调用每个进程上的更新方法.每个进程包含了一个对象集合,集合中的对象实现了其操作的接口并将调用这些对象的适当方法.这些对象每个都对其数据做单一而重要的作业(task).通过组件系统,这些对象能够共享数据,因此多个线程的组合能够在游戏实体中产生复杂的更新同时保证每个进程相对简单.
这个架构类似于游戏开发中的许多实体系统.架构遵循了良好的面向对象程序设计原则且能够运转.但在更多事情到来之前,我们先开始疯一把
放弃好的面向对象实践
当前的架构使用了良好的面向对象实践,像封装和单一职责原则(single responsibility)——IRenderable和IMoveable的实现封装了在每帧中游戏实体更新的单一职责的数据和逻辑——而合成——Spaceship实体是通过组合IRenderable和IMoveable接口的实现而创建的.通过组件系统(system of components)我们确保在恰当的地方数据在实体的不同数据类之间共享.
实体系统改革中的下一步或多或少有点不直观,打破了面向对象程序设计的核心教条之一.我们在Renderable和Moveable实现中打破了数据和逻辑的封装.特别是我们将逻辑从这些类中抽出反而将其放置在进程中
所以这个
- interface IRenderable
- {
- function render();
- }
- class Renderable implements IRenderable
- {
- public var display:DisplayComponent;
- public var position:PositionComponent;
- public function render():void
- {
- display.view.x = position.x;
- display.view.y = position.y;
- display.view.rotation = position.rotation;
- }
- }
- class RenderProcess implements IProcess
- {
- private var targets:Vector.<IRenderable>;
- public function update(time:Number):void
- {
- for each (var target:IRenderable in targets)
- {
- target.render();
- }
- }
- }
- class RenderData
- {
- public var display:DisplayComponent;
- public var position:PositionComponent;
- }
- class RenderProcess implements IProcess
- {
- private var targets:Vector.<RenderData>;
- public function update(time:Number):void
- {
- for each(var target:RenderData in targets)
- {
- target.display.view.x = target.position.x;
- target.display.view.y = target.position.y;
- target.display.view.rotation = target.position.rotation;
- }
- }
- }
- interface IMoveable
- {
- function move(time:Number):void;
- }
- class Moveable implements IMoveable
- {
- public var position:PositionComponent;
- public var velocity:VelocityComponent;
- public function move(time:Number):void
- {
- position.x += velocity.velocityX * time;
- position.y += velocity.velocityY * time;
- position.rotation += velocity.angularVelocity * time;
- }
- }
- class MoveProcesss implements IProcess
- {
- private var targets:Vector.<IMoveable>;
- public function move(time:Number):void
- {
- for each(var target:Moveable in targets)
- {
- target.move(time);
- }
- }
- }
- class MoveData
- {
- public var position:PositionComponent;
- public var velocity:VelocityComponent;
- }
- class MoveProcess implements IProcess
- {
- private var targets:Vector.<MoveData>;
- public function move(time:Number):void
- {
- target.position.x += target.velocity.velocityX * time;
- target.position.y += target.velocity.velocityY * time;
- target.position.rotation += target.velocity.angularVelocity * time;
- }
- }
这样第一个明显的结果是所有的实体必须使用相同的渲染方法,因为渲染代码现在在RenderProcess里.但事实上并不这么回事.例如,我们能够有两个进程比如RenderMoveClip和RenderBitmap且它们能够处理不同的实体集.所以我们并没有丧失任何灵活性.
我们所得到的是重构我们实体的重要能力来产生一个架构,该架构有着明确的分工和简单的配置.重构以一个问题开始.
我们需要数据类吗?(Do we need the data classes)
当前,我们的实体
- class Spaceship
- {
- public var moveData:MoveData;
- public var renderData:RenderData;
- }
- class MoveData
- {
- public var position:PositionComponent;
- public var velocity:VelocityComponent;
- }
- class RenderData
- {
- public var display:DisplayComponent;
- public var position:PositionComponent;
- }
- class PositionComponent
- {
- public var x:Number;
- public var y:Number;
- public var rotation:Number;
- }
- class VelocityComponent
- {
- public var velocityX:Number;
- public var velocityY:Number;
- public var angularVelocity:Number;
- }
- class DisplayComponent
- {
- public var view:DisplayObject;
- }
- class MoveProcess implements IProcess
- {
- private var targets:Vector.<MoveData>;
- public function move(time:Number):void
- {
- for each(var target:MoveData in targets)
- {
- target.position.x += target.velocity.velocityX * time;
- target.position.y += target.velocity.velocityY * time;
- target.position.rotation += target.velocity.angularVelocity * time;
- }
- }
- }
- class RenderProcess implements IProcess
- {
- private var targets:Vector.<RenderData>;
- public function update(time:Number):void
- {
- for each(var target:RenderData in targets)
- {
- target.display.view.x = target.position.x;
- target.display.view.y = target.position.y;
- target.display.view.rotation = target.position.rotation;
- }
- }
- }
- class Spaceship
- {
- public var position:PositionComponent;
- public var velocity:PositionComponent;
- public var display:DisplayComponent;
- }
- class PositionComponent
- {
- public var x:Number;
- public var y:Number;
- public var rotation:Number;
- }
- class VelocityComponent
- {
- public var velocityX:Number;
- public var velocityY:Number;
- }
- class DisplayComponent
- {
- public var view:DisplayObject;
- }
系统与节点
当进程需要时,实体系统框架(我们很快会接触到)中的一些核心代码会动态的创建这些数据对象.在这个简化的语境(reduced context)中,数据类将只是由进程使用的集合(数组,链表或者别的依据实现而不同)中的节点.所以为弄清楚这我们将它们重命名为节点.
- class MoveNode
- {
- public var position:PositionComponent;
- public var velocity:VelocityComponent;
- }
- class RenderNode
- {
- public var display:DisplayComponent;
- public var position:PositionComponent;
- }
- class MoveSystem implements ISystem
- {
- private var targets:Vector.<MoveNode>;
- public function update(time:Number):void
- {
- for each(var target:MoveNode in targets)
- {
- target.position.x += target.velocity.velocityX * time;
- target.position.y += target.velocity.velocityY * time;
- target.position.rotation += target.velocity.angularVelocity * time;
- }
- }
- }
- class RenderSystem implements ISystem
- {
- private var targets:Vector.<RenderNode>;
- public function update(time:Number):void
- {
- target.display.view.x = target.position.x;
- target.display.view.y = target.position.y;
- target.display.view.rotation = target.position.rotation;
- }
- }
- interface ISystem
- {
- function update(time:Number):void;
- }
那实体又是什么呢?(And what is an entity?)
最后一点改变--Spaceship类没有什么特别的.他只是一个组件容器.所以我们将只称其为实体并给它一个组件集合.我们将根据这些组件的类类型(class type)来访问它们.
- class Entity
- {
- private var components:Dictionary;
- public function add(component:Object):void
- {
- var componentClass:Class = component.constructor;
- components[componentClass] = component;
- }
- public function remove(componentClass:Class):void
- {
- delete components[componentClass];
- }
- public function get(componentClass:Class):void
- {
- return components[componentClass];
- }
- }
- public function createSpaceship():void
- {
- var spaceship:Entity = new Entity();
- var position:PositionComponent = new PositionComponent();
- position.x = Stage.stageWidth / 2;
- position.y = Stage.stageHeight / 2;
- position.rotation = 0;
- spaceship.add(position);
- var display:DisplayComponent = new DisplayComponent();
- display.view = new SpaceshipImage();
- spaceship.add(display);
- engine.add(spaceship);
- }
核心的引擎类
我们决不能忘记系统管理器,之前叫做进程管理器
- class SystemManager
- {
- private var systems:PriorirtiesdList;
- public function addSystem(system:ISystem,priority:int):void
- {
- systems.add(system,priority);
- system.start();
- }
- public function update():void
- {
- for each(var system:ISystem in systems)
- {
- system.update(time);
- }
- }
- public function removeSystem(system:ISystem):void
- {
- system.end();
- systems.remove(system);
- }
- }
这将会被加强(enhanced)并处于我们实体系统框架的核心.我们将为其添加我们之前提到的功能以便为系统动态创建节点.
实体只关心组件,系统只关心节点.因此,要完成实体系统框架,当实体改变,为系统所用的节点结合添加或者删除组件式,我们需要监视实体的代码.因为这是实体和系统都知道的那一点代码,我们可能考虑将其作为游戏的中心.在Ash中,我称这为Engine类,它是系统管理器的一个增强版.
当你开始使用或停止使用Engine类时,每个实体和系统都被添加到或者从Engine类删除.Engine类跟踪实体上的组件并创建(和在必要时销毁)节点,将这些节点添加到节点集合中.Engine类也为系统提供了一种方法来得到其需要的集合.
- public class Engine
- {
- private var entities:EntityList;
- private var systems:EntityList;
- private var nodeLists:Dictionary;
- public function addEntity(entity:Entity):void
- {
- entities.add(entity);
- //create nodes from this entity's components and add them to node lists
- //also watch for later addition and removal of components from the entity so
- //you can adjust its derived nodes accordingly
- }
- public function removeEntity(entity:Entity):void
- {
- //destory nodes from this entity's components
- //and remove them from the node lists
- entities.remove(entity);
- }
- public function addSystem(system:System,priority:int):void
- {
- systems.add(system,priority);
- system.start();
- }
- public function removeSystem(system:System):void
- {
- system.end();
- systems.remove(system);
- }
- public function getNodeList(nodeClass:Class):NodeList
- {
- var nodes:NodeList = new NodeList();
- nodeLists[nodeClass] = nodes;
- //create the nodes from the current set of entities
- //and populate the node list
- return nodes;
- }
- public function update(time:Number):void
- {
- for each(var system:ISystem in systems)
- {
- system.update(time);
- }
- }
- }
要查看该架构的一个实现,checkout Ash实体系统框架并看看例子小行星游戏的实现.
结论
所以,总结一下,实体系统发源于想要简化游戏循环.从哪里衍生出了实体架构,它代表着游戏的状态和系统,系统作用于游戏状态.系统在每帧中都会更新--这就是游戏循环.实体由组件(components)构成,系统作用于含有它们感兴趣的组件的实体.引擎管理者系统和实体并确保每个系统能够访问到一组有对应组件的实体集合.
然而,系统通常不关心作为整体的实体,只关心它们需要的特定组件.因此,要优化该架构并提供额外的净化度(additional clarity),系统作用于静态类型的包含对应组件的节点对象,在节点对象中这些组件都属于同一个实体.
一个实体系统框架为这种架构提供了基本的架子(scaffolding)和核心管理,没有提供任何实际的实体或者系统类.你通过创建对应的实体和系统来创建游戏.
一个基于实体的游戏引擎将在基本的框架上提供许多标准的系统和实体.
3个Actionscript实体系统框架是我自己的Ash,Tom Davies的Ember2和Alec McEachran的Xember.Artemis是一个java的实体系统框架,且已经移植到了C#.