Entity System Frameworkとは何ですか?なぜゲーム開発に必要なのですか?

Habréには、ゲームをゼロから作成することに関する興味深い記事がすでに多数ありますが、お気に入りのクラフトがフランケンシュタインにならず、健全な状態に到達するために何をすべきかを説明する記事はほとんどありません。 元の記事の著者は、ゲームエンジンのコードが、メインループのジャンクヒープから、Entity Component Systemを使用した綿密に考えられた拡張可能なアーキテクチャに進化した様子を語っています。 記事には多くのコードがありますが、ほとんどの場合繰り返されますが、著者が何を変更し、これが構造全体にどのように影響するかがより明確になるので、私はそれを捨てませんでした。 この記事は、私のような初心者を対象としていますが、既に初心者は「邪悪な」プロトタイプをいくつか作成していますが、今ではコードを理解する方法がわかりません。



翻訳者のメモ:
私は翻訳者ではありません。 私は彼のゲームをリファクタリングする方法を探していた普通の舌で縛られたプログラマーであり、このパターンにつまずいて、ハブにほとんど言及されていないことに驚いた 大きなテキストを翻訳したことは一度もありません。簡単で理解しやすい英語のテキストを通常のロシア語に翻訳するのはとても難しいことだと知ったのは啓示でした。 要するに、この記事が不器用で曲がって書かれているように見える場合、私は謝罪します。 正直言って:)





先週、私はAsh -Entity SystemフレームワークをリリースしてActionScriptゲームを開発しましたが、多くの人から「Entity System Frameworkとは何ですか?」という質問がありました。 ここに私の長い答えがあります。



エンティティシステムは、UnityやEmber2、Xember、および自分のAshなどのあまり知られていないActionScriptフレームワークのような身近なもののおかげで人気を博しています。 これには十分な理由があります。コード内の責任の分離を促進するゲームの単純化されたアーキテクチャは使いやすいです。



この投稿では、エンティティベースのアーキテクチャが昔ながらのゲームループからどのように出現するかを一貫して示します。 少し時間がかかります。 上記の例はActionscript上にあります。これは現時点でまさに使用しているものですが、アーキテクチャ自体はあらゆるプログラミング言語に適しています。





この記事では、例として単純なゲームAsteroidsを使用します。 レンダリングシステム、物理学、AI、オブジェクトのプレイヤーコントロール、非制御オブジェクトなど、ビッグゲームに必要なものの多くが含まれているため、例として小惑星を使用しています。



ゲームサイクル



エンティティシステムを使用する理由を本当に理解するには、古き良きゲームサイクルがどのように機能するかを明確に理解する必要があります。 小惑星の場合、次のようになります。

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回、定期的に呼び出されます。 ループ内の操作の順序は重要です。なぜなら、さまざまなゲームオブジェクトを更新し、それらの間の衝突をチェックしてから、それらをすべて描画するからです。 すべてのフレーム。

これは、次の理由で非常に単純なゲームループです。

1.ゲーム自体はシンプルです。

2.ゲームには1つの状態しかありません。

過去に、私はコンソールゲームに取り組んでおり、唯一の機能であるゲームループは3,000行を超えるコードで構成されていました。 それは美しくなく、愚かでした。 しかし、それがゲームの作成方法であり、私たちはそれと共に生きなければなりませんでした。

エンティティシステムアーキテクチャは、ゲームループの問題を解決しようとすることから生まれました。 これにより、ゲームループがゲームの中心となり、ゲームループの合理化が現代のゲームのアーキテクチャにおいて他の何よりも重要であることが示唆されます。 これは、たとえば、ビューをコントローラーから分離するよりも重要です。



プロセス



この進化の最初のステップは、プロセスと呼ばれるオブジェクトです。 これらは、初期化、更新、および破棄できるオブジェクトです。 プロセスインターフェイスは次のようになります。

 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 ); } }
      
      





これは、プロセスマネージャーの簡易バージョンです。 特に、プロセスを正しい順序(addメソッドのpriorityパラメーターによって決定される)で更新し、ゲームサイクル中にプロセスが削除される状況を処理する必要があります。 しかし、この単純化されたバージョンはアイデアそのものを伝えています。 ゲームサイクルが複数のプロセスに分割されている場合、プロセスマネージャーの更新方法は新しいゲームサイクルであり、プロセスは既にゲームのコアになっています。



レンダリングプロセス



たとえば、レンダリングプロセスを見てみましょう。 ゲームループからレンダーコードを引き出してプロセスに配置するだけで、次のようになります。

 class RenderProcess implements IProcess { public function start() : Boolean { //    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 { //    } }
      
      







インターフェースを使用します



しかし、あまり効果的ではありません。 可能なすべての種類のゲームオブジェクトを手動で描画する必要があります。 すべての可視オブジェクトの共通インターフェースがある場合、多くのことを単純化できます

 interface IRenderable { function render(); } class RenderProcess implements IProcess { private var targets:Vector.<IRenderable>; public function start() : Boolean { //    return true; } public function update( time:Number ):void { for each( var target:IRenderable in targets ) { target.render(); } } public function end() : void { //    } }
      
      





次に、宇宙船のクラスに同様のコードが含まれます。

 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; } }
      
      





このコードは、Flashディスプレイリストに基づいています。 バッファまたはstage3dを使用した場合、それは異なりますが、原則は同じです。 レンダリングする画像、レンダリングする位置と回転が必要です。 そして、画面への出力を実行するレンダリング関数。



基本クラスと継承を使用します



実際、船のコードにはユニークなものは何もありません。 そのコードはすべて、すべての可視オブジェクトで使用できます。 それらを区別する唯一のものは、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; } } 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 * time; position.y += velocity.y * time; rotation += angularVelocity * time; } } class Spaceship extends Moveable { }
      
      







多重継承



これはすべて良いように見えますが、残念ながら、船を動かしてレンダリングしたいのですが、多くのプログラミング言語は多重継承を許可していません。 そして、それをサポートする言語でさえ、Movableクラスの位置と回転がRenderableクラスと同じでなければならないときに問題が発生します。

解決策は、Movableが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; } } class Spaceship extends Moveable { }
      
      





現在、私たちの宇宙船は移動とレンダリングの両方が可能です。 同じ原則を他のゲームオブジェクトに適用して、このクラス階層を取得できます。

画像



Renderableを単純に拡張する静的オブジェクトも取得できます。

画像



移動可能だがレンダリング不可



しかし、描画すべきではない移動オブジェクトを作成したい場合はどうでしょうか? たとえば、非表示のゲームオブジェクトですか? ここで、クラス階層が壊れており、Renderableから継承されないMovableインターフェイスの代替実装が必要です。

 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クラスとMovableクラスは引き続き必要ですが、それらを継承して宇宙船クラスを作成する代わりに、これらの各クラスのインスタンスを含むshipクラスを作成します。

 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; } } class Spaceship { public var renderData:IRenderable; public var moveData:IMoveable; }
      
      





このようにして、継承の問題を発生させることなく、さまざまな動作を任意の方法で組み合わせることができます。

画像

この構成を使用して作成されたこれらのオブジェクト:静的オブジェクト、宇宙船、空飛ぶ円盤、小惑星、および力場-総称してエンティティと呼ばれます。

私たちのプロセスは変わりません。

 interface IRenderable { function render(); } 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(); } 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 ); } } }
      
      





ただし、各プロセスにshipオブジェクトを追加するのではなく、そのコンポーネントを追加します。 そして、次のようなものが得られます。

 public function createSpaceship():Spaceship { var spaceship:Spaceship = new Spaceship(); ... renderProcess.addItem( spaceship.renderData ); moveProcess.addItem( spaceship.moveData ); ... return spaceship; }
      
      





このアプローチは良さそうです。 さまざまな継承や自己反復チェーンなしで、さまざまなゲームオブジェクトでプロセスサポートを自由に組み合わせたり組み合わせたりすることができます。 しかし、1つの問題があります。



一般的な情報をどうしますか?



Moveプロセスは値を変更する必要があり、Renderプロセスはそれらを描画する必要があるため、Renderableクラスのオブジェクトの位置と回転のプロパティは、Movableオブジェクトの回転がある位置と同じ値を持つ必要があります。

 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; } } class Spaceship { public var renderData:IRenderable; public var moveData:IMoveable; }
      
      





この問題を解決するには、両方のオブジェクトがこれらのプロパティの同じインスタンスを参照していることを確認する必要があります。 ActionScriptでは、これは、これらのプロパティがオブジェクトでなければならないことを意味します。オブジェクトは参照で渡すことができ、プリミティブ型は値で渡すためです。

そのため、コンポーネントと呼ぶ別のクラスのセットを提示します。 これらのコンポーネントはプロパティ値のラッパーであるため、プロセス間で共有できます。

 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; } }
      
      





宇宙船クラスを作成するとき、MovableオブジェクトとRenderableオブジェクトがPositionComponentの同じインスタンスを参照することを確認する必要があります。

 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(); } }
      
      





この変更はプロセスには影響しません。



そして、これは休息するのに良い場所です。



これで、タスクが明確に分割されました。 ゲームループはプロセスを「ねじり」、それぞれの更新メソッドを呼び出します。 各プロセスは、オブジェクトとの対話を可能にするインターフェイスを実装するオブジェクトのコレクションで構成され、(プロセス)はこれらのオブジェクトに必要なメソッドを呼び出します。 このようなオブジェクトは、情報を使用して単一のタスクを実行します。 コンポーネントを使用して、これらのオブジェクトは共通の情報を持ち、さまざまなプロセスの組み合わせにより、各プロセスを比較的シンプルに保ちながら、ゲームオブジェクト間の複雑な相互作用を作成できます。

このアーキテクチャは、ゲーム開発における多くのエンティティシステムに似ています。 OOPの原則をうまく実装し、機能します。 しかし、あなたを夢中にさせる何か他のものがあります。



適切なオブジェクト指向の実践を避ける



現在のアーキテクチャは、 カプセル化責任の分離などのオブジェクト指向プログラミングの原則を使用しています。 責任ごとにIRenderableおよびIMovableの近い値とロジックを使用し、フレームごとにゲームオブジェクトを更新します。 構成 -宇宙船オブジェクトは、IRenderableおよびIMovableインターフェースの実装を組み合わせて作成されます。 コンポーネントシステムを使用すると、必要に応じて、データクラスのさまざまなオブジェクトの値に等しくアクセスできます。



オブジェクトのシステムの進化における次のステップは、直感的に理解できず、オブジェクト指向プログラミングの本質におけるトレンドを破壊するように見えるかもしれません。 RenderableおよびMovableの実装における情報とロジックのカプセル化を解除します。 特に、これらのクラスからプロセスにロジックを移動します。



これは:

 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 ); } 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; } } }
      
      







なぜこれをしたのかはすぐにはわからないかもしれませんが、私を信頼してください。 インターフェイスの必要性をなくし、プロセスはより重要なことをしています。IRenderableまたはIMovableの実装で作業を委任する代わりに、彼は自分で作業を行います。



最初の明白な結論は、レンダーコードがRenderProcessにあるため、すべてのエンティティが同じレンダリングメソッドを持っている必要があるということです。 しかし、それだけではありません。 たとえば、RenderMovieClipとRenderBitmapの2つのプロセスを作成できます。これらのプロセスは、さまざまなエンティティセットで操作できます。 したがって、コードの柔軟性が失われることはありません。



エンティティを大幅にリファクタリングして、より理解しやすい分離とシンプルな構成のアーキテクチャを実現することができます。 リファクタリングは質問から始まります。



値クラスが必要ですか?



現時点では、私たちの本質

 class Spaceship { public var moveData:MoveData; public var renderData:RenderData; }
      
      







2つのクラスが含まれています

 class MoveData { public var position:PositionComponent; public var velocity:VelocityComponent; } class RenderData { public var display:DisplayComponent; public var position:PositionComponent; }
      
      







これらのデータクラスには、3つのコンポーネントが含まれます。

 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; }
      
      







そして、これらの値クラスは2つのプロセスで使用されます。

 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; } } }
      
      







しかし、エンティティはデータクラスを気にするべきではありません。 すべてのコンポーネントには、エンティティ自体の状態が含まれます。 データクラスは、プロセスの利便性のために存在します。 Spaceshipクラスにデータクラスではなくコンポーネント自体が含まれるように、コードをリファクタリングします。

 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; } class DisplayComponent { public var view:DisplayObject; }
      
      







情報クラスを取り除き、代わりに宇宙船を定義するために複合コンポーネントを使用して、どのプロセスがそれに影響を与える可能性があるかを知るエンティティの必要性をすべて取り除きました。 船には、その状態を決定するコンポーネントが含まれています。 これらのコンポーネントを他の値クラスに結合する必要は、現在他のクラスの責任です。



システムとノード。



Entity Systemフレームワークのコアの特定の部分(後で説明します)は、プロセスの必要に応じてこれらのオブジェクトを動的に作成します。 この単純化されたコンテキストでは、値クラスは、プロセスを使用するコレクション(配列、リンクリスト、またはその他)のノードまたはリーフにすぎません。 そのため、明確にするために、ノードに名前を変更します。

 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; } } } interface ISystem { function update( time:Number ):void; }
      
      







そしてその本質は何ですか?



最後の変更-宇宙船クラスについて特別なことはありません。 コンポーネントの単なるコンテナです。 したがって、単にEntityと呼び、コンポーネントの配列を指定します。 これらのコンポーネントには、クラスタイプを介してアクセスできます(そして、これをプラス-約Translatorで表示します)。

 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 ); engine.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 ); } }
      
      







このクラスは拡張され、フレームワークの中心になります。 システムのノードを動的に作成する前述の機能をその機能に追加します。



エンティティはコンポーネントのみを扱い、システムはノードのみを扱います。そして、エンティティとシステムのフレームワークを完成させるために(私を撃てますが、より良い翻訳方法はわかりません-翻訳者のメモ)、エンティティの監視をエンコードし、システムが変更されたときに使用するノードのコレクションにコンポーネントを追加または削除する必要があります。このコードはエンティティとシステムの両方を知っているため、ゲームの中心に配置します。これをエンジンクラスと呼びます。これはシステムマネージャーの拡張バージョンです。



各エンティティおよび各システムは、使用を開始および終了するときにエンジンクラスに追加および削除されます。エンジンはコンポーネントとエンティティを追跡し、必要に応じてノードを作成および削除し、これらのノードを配列に追加します。エンジンは、必要なノードのコレクションへのシステムアクセスも提供します。

 public class Engine { private var entities:EntityList; private var systems:SystemList; private var nodeLists:Dictionary; public function addEntity( entity:Entity ):void { entities.add( entity ); //            //         //        } public function removeEntity( entity:Entity ):void { //  ,    //       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 ); } } }
      
      







画像



実際にどのように見えるかを確認するには、Ash Entity System Frameworkダウンロードして、ゲームAsteroidsの実装を確認してください



おわりに



要するに、エンティティシステムは、ゲームサイクルを単純化したいという思いから生まれました。これにより、ゲームの状態を表すエンティティアーキテクチャと、ゲーム内の状態で動作するシステムが実現しました。システムはフレームごとに更新されます-これはゲームサイクルです。エンティティはコンポーネントで構成され、システムは、それらに必要なコンポーネント(システム)を持つエンティティでのみ動作します。エンジンはシステムとエンティティを監視し、各システムに必要なコンポーネントを持つエンティティのコレクションへのアクセスを提供します。



ただし、システムは実際にはエンティティ全体を処理するのではなく、エンティティを構成するコンポーネントのみを処理します。アーキテクチャを最適化し、より明確にするために、システムは必要なコンポーネントを含むノードで動作します。これらのコンポーネントはすべて同じエンティティに属します。



Entity System Frameworkは、エンティティクラスやシステムを提供せずに、そのようなアーキテクチャの最も単純なフレームワークを提供します。必要なエンティティとシステムを作成して、ゲームを構築します。



翻訳者のメモ:
- - Entity Component System C++? , . , - Artemis Entity System Framework, , .

:

, :)





オリジナル



All Articles