Что такое entity framework
Это не дословный перевод статьи Ричарда Лорда которая мне понравилась.
--------------------------------------------
Entity system-ы растут в популярности, например широко извесный юнити и менее известные as3 библиотеки Ember2, Xember и моя собственная Ash. Причиной тому то что они упрощают игровую архитектуру и позволяют четко разделять обязанности в коде.
В этой статье я расскажу о эволюции олдскульной game loop арихитекуры в entity-образную. Запаситесь терпением.
Примеры
В качестве примера я буду использовать игру Asteroids, она идеально подходить так как содержит простые версии понятий встречающихся в более серьёзных играх - рендеринг, физика, AI, персонаж, NPC (non player character)
Game loop
Для понимания того что из себя представляет entity-система, следует представлять что такое обыкновеннвый game loop (игровой цикл).
Game loop для игры Asteroids может выглядеть примерно так:
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(); } }
Этот цикл простой так как 1) игра простая 2) в игре только одно состояние, но если игра будет сложнее, то этот цикл станет обрастать условными операторами и рано или поздно превратится в кашу (3000 строчек не предел)
Entity-система пытается решить эту проблему. Пускай даже в ущерб некоторым другим важным вещам, вроде отделения представления от контроллера.
Эволюция
Первый шаг на пути эволюции это добавление понятия процесс. Процессы это объекты которые могут быть запущены, могут протекать и могут быть остановлены. Интерфейс процесса выглядит примерно так:
interface IProcess{ function start():Boolean; function update( time:Number ):void; function end():void; }
class ProcessManager{ private var processes:PrioritisedList; public function addProcess( process:IProcess, priority:int ):Boolean{ 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 ); } }
Процесс визуализации (rendering)
Давайте, в качестве примера, посмотрим на отрисовку.
Если просто запихнуть часть кода из бывшего game loop в render-процесс то будет что-то вроде:
class RenderProcess implements IProcess{ public function start() : Boolean{ // initialise render system return true; } public function update( time:Number ):void{ 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(); } } public function end():void{ // clean-up render system } }
class RenderProcess implements IProcess{ private var targets:Vector.<IRenderable>; public function start() : Boolean{ // initialise 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) а также позиция и поворот, всё остальное может быть общим для всех игровых объектов. Поэтому давайте вынесем общую функциональность в базовый класс используя наследование:
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; } }
Процесс дивежения
Для дальнейшего понимания нужен еще один процесс поэтому давайте рассмотрим MoveProcess отвечающий за движение игровых объектов:
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 * time; position.y += velocity.y * time; rotation += angularVelocity * time; } }
То что у нас получилось - прекрасно, но к сожалению есть одна сложность: наш корабль должен быть однорвеменно moveable и renderable, однако большинство современных языков программирования, включая AS3 не разрешают множественное наследование
Эту проблему можно решить цепочкой наследования, можно сделать 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 * time; position.y += velocity.y * time; rotation += angularVelocity * time; } }
также мы можем создавать неподвижные видимые объекты:
Moveable но не 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 * time; position.y += velocity.y * time; rotation += angularVelocity * time; } }
В простой игре такое решение не элегантно но терпимо, но в игре посложнее это может обернутся большими сложностями, добавление каждого нового объекта который противоречит общему принципу наследования, вроде силового поля в этой ситуации, превратится в проблему.
Композиция предпочтительнее наследования
OOP принцип предпочтения композиции наследованию - широко известен. Использование этого принципа спасёт нас от возможной путаницы с наследованием.
Нам по прежнему понадобятся раздельные Renderable и Moveable классы, но вместо наследования от них мы создадим космический корабль который будет содержать инстансы каждого из них:
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 * time; position.y += velocity.y * time; rotation += angularVelocity * time; } }
Объекты объединяющие разные виды поведений, такие как: корабль, астероид, пуля, летающая тарелка, силовое поле и называются сущностями (Entity)
Код процессов остаётся неизменным:
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 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; }
Общие данные
Значения позиции и поворота в экземпляре Renderable должны совпадать с значениями в экземпляре класса Moveable, ведь RenderProcess должен отрисвать объект в том месте куда его подвинул MoveProcess.
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 * time; position.y += velocity.y * time; rotation += angularVelocity * time; } }
Для этого мы вводим еще одно понятие Сomponents (составляющие) - это просто value object-ы хранящие некоторые данные для совместного использования разными процессами
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 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 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(); } }
Отличный момент для передышки
К этому моменту у нас есть изящное разделение задач. Игровой цикл разделён на процессы. Каждый процесс управляет набором объектов-действий. Каждое из этих действий выполняет одно конкретное действие с конкретным игровым объектом. Через систему компоенетов(составляющих) эти действия совместно используют данные игрового объекта. Таким образом несколько процессов одновременно могут производить сложные действия с объектами игрового мира и при этом каждый процесс остаётся достаточно простым.
Такая архитектура лежит в основе большого количества entity-фрэймворков для разработки игр. Такая архитектура следует хорошим практикам OOP и прекрасно работает.
Но это еще не всё, дальше немного безумия.
Отказ от хороших практик OOP
В данный момент наша архитектура использует такие важные принципы ооп как инкапсуляция, принцип single responsibility (имплементации IRenderable и IMoveable инкапсулируют данные и логику необходимые для выполнения единственной задачи) и сomposition over inheritance (Игровой объект ракета это комбинация имплементаций IRenderable и IMoveable)
Следующий шаг на пути эволюции entity-системы нарушает один из базовых принципов OOP. Мы нарушим инкапсуляцию данных и логики Renderable и Moveable, в частности мы удалим логику из этих классов и целиком перенесём её в процесс.
Таким образом это:
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; } } }
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 MoveProcess 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{ 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; } } }
Первым последствием этого изменения является то что все игровые объекты теперь должны использовать одинаковый способ визуализации, т.к. отрисовкой занимается процесс а он один на всех. Но это нас никак не ограничивает ведь если нам например понадобится часть объектов отрисовать в stage3d а часть используя дисплей лист, то мы всегда можем добавить ещё один рендерящий процесс, и оба процесса смогут работать параллельно, так что гибкость мы не потеряли.
Чего мы добились так это возможности значительо изменить архитектуру иговых объектов и упростить их конфигурирование.
До сих пор ракета была устроена так:
она содержала 2 объекта с данными
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 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:VelocityComponent; 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; public var angularVelocity:Number; }
Systems & Nodes
Следующим шагом мы перекладываем задачу получения данных необходимых процессу для работы на entity-фрэймворк. Для исключения путаницы вводим новое понятие Node. Node - это выборка данных игрового объекта которую фрэймвок предоставляет процессу.
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{ for each( var target:RenderNode in targets ){ target.display.view.x = target.position.x; target.display.view.y = target.position.y; target.display.view.rotation = target.position.rotation; } } }
Так как ракета ничего не делает кроме как хранит состояние, и её устройство абсолютно не отличается от всех остальных игровых объектов. Том мы переименуем её в Entity и унифицируем процесс добавления/удаления/получения данных.
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 ):Object{ 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 ); game.add( spaceship ); }
Не забудем и о менеджере систем, который раньше назывался менеджером процессов
class SystemManager{ private var systems:PrioritisedList; public function addSystem( system:ISystem, priority:int ):void{ systems.add( system, priority ); system.start(); } public function update( time:Number ):void{ for each( var system:ISystem in systemes ){ system.update( time ); } } public function removeSystem( system:ISystem ):void{ system.end(); systems.remove( system ); } }
(возможно переведу позже)
This will be enhanced and will sit at the heart of our entity framework. We’ll add to it the functionality mentioned above to dynamically create nodes for the systems.
The entities only care about components, and the systems only care about nodes. So to complete the entity framework, we need code to watch the entities and, as they change, add and remove their components to the node collections used by the systems. Because this is the one bit of code that knows about both entities and systems, we might consider it central to the game. In Ash, I call this the Game class, and it is an enhanced version of the system manager.
Every entity and every system is added to and removed from the Game class when you start using it and stop using it. The Game class keeps track of the components on the entities and creates and destroys nodes as necessary, adding those nodes to the node collections. The Game class also provides a way for the systems to get the collections they require.
public class Game{ private var entities:EntityList; private var systems:SystemList; 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{ // destroy nodes containing 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 systemes ){ system.update( time ); } } }
To see one implementation of this architecture, checkout the source code for Ash, and see the example Asteroids implementation there too.
Conclusion
So, to summarise, entity systems originate from a desire to simplify the game loop. From that comes an architecture of entities, which represent the state of the game, and systems, which operate on the state of the game. Systems are updated every frame – this is the game loop. Entities are made up of components, and systems operate on the entities that have the components they are interested in. The game monitors the systems and the entities and ensures each system has access to a collection of all the entities that have the appropriate components.
However, systems don’t generally care about the entity as a whole, just the specific components they require. So, to optimise the architecture and provide additional clarity, the systems operate on statically typed node objects that contain the appropriate components, where those components all belong to the same entity.
An entity framework provides the basic scaffolding and core management for this architecture, without providing any actual entity or system classes. You create your game by creating the appropriate entities and systems.
An entity based game engine will provide many standard systems and entities on top of the basic framework.
Three entity frameworks for Actionscript are my own Ash, Ember2 by Tom Davies and Xember by Alec McEachran. Artemis is an entity framework for Java, that has also been ported to C#.
Всего комментариев 10
Комментарии
12.09.2012 19:11 | |
картинки залей прям в блог как вложения.
|
12.09.2012 19:17 | |
поправил
|
|
Обновил(-а) artcraft 12.09.2012 в 19:33
|
12.09.2012 21:47 | |
Спасибо за перевод!
|
13.09.2012 01:15 | |
Цитата:
А я ничего не понял, с того момента, как начали разрушать ООП. И еще не понял, чем же так entity-framework хорош?
Цитата:
не понял, чем же так entity-framework хорош?
ответ на ваш вопрос тут: Why use an entity framework for game development? Цитата:
А еще пример ужасный, из него понятно, что все в космосе может летать, но непонятно, кто управляет взаимодействием и как? Вот летят две ентити друг на друга и?
Цитата:
Ну и очепятки
|
13.09.2012 01:58 | |
Код больше радует и объясняет, чем статья. Но за перевод спасибо. Возможно вам надо было от себя рассказать, как поняли.
|
17.09.2012 07:50 | |
Никто не заметил, что в примере с процессом метод с типом void возвращает булево значение?
Эта же ошибка и у автора оригинала |
17.09.2012 13:06 | |
поправил
|
Последние записи от artcraft
- Что такое entity framework (12.09.2012)
- Подводные камни Dictionary (04.09.2012)
- Волшебное превращение Object --> Class (04.09.2012)
- Инверсия контроля (14.04.2012)
- Loose coupling (10.01.2012)