Pythonで ECS がしたいときに使いたい ECS + ステート管理用のライブラリを公開した
開発言語を Python にしたゲーム開発で ECS (Entity Component System) を採用したいケースはごく稀だとは思います。普通は Unity や Unreal エンジンにネイティブで実装されている ECS 機能を使うはずです。ただプログラミング学習者やなんらかの要件で Python で開発することになった時に使える ECS + ステート管理を1つのライブラリとして提供されている OSS が開発を始めた時点では存在しなかったので、このライブラリ Pigframe を作ってみました。
主には、Pygame, Pyxel などの Python 製のゲーム開発エンジンを使ってゲームを開発したいと思っている、開発を始めた個人開発者・学習者向けになります。
自分がゲーム開発で使っている/使いたい Pygame や Pyxel などの開発エンジンと併せて使うことを主な用途として想定しています。
PyPI に公開しているので pip でインストールできます。pigframe が依存しているライブラリはありません。
pip install pigframe
◼ やってくれること
- 一般的な ECS の処理
- ステート管理
この2つです。
以下(昨今のインターネット記事のような)冗長な説明が続きますので、さっさと使い方を見たい方は 日本語版 README を参照してください。
また ECS とはなんぞやについては以下の記事などが参考になります。
Pigframe は以下のような仕様を持っています。
機能 | 対応 | 備考 |
---|---|---|
画面の描画 | x | ゲームエンジンに任せる |
サウンドの取り扱い | x | ゲームエンジンに任せる |
テキストの描画 | x | ゲームエンジンに任せる |
RPGツクールができるようなゲームに共通したシステム、コンポーネントの実装 | x | ゲームエンジンに任せる |
ほとんど何もしてくれませんね。ブヒッ。
Pigframe には開発するゲームによって使ったり使わなかったりする機能は一切実装されていません。その代わり、どんなゲーム開発でも共通している(と思う)基本的な機能を提供しています。以下はその例です。
▼ ゲームに登場させる登場人物やオブジェクト(以下、エンティティー)を ID で管理する。
app = App()
...
entity = app.create_entity() # -> int: entity ID
▼ エンティティーが保有すべき情報(位置、速度、体力など)はエンティティーのID がキーとなっている辞書で管理する。
コンポーネントのインスタンスは内部で自動的に作成されます。(テンプレート)
app.add_component_to_entity(entity, PositionComponent, 0, 0)
# Parameters: entity ID, 紐付けるコンポーネントの型、コンポーネントのパラメータ
▼ すべての処理を System
, Event
, Screen
の3つに切り分ける。
ゲーム上の各シーン・レベルで実行する一連の処理を System (各シーンで常時実行する処理)
, Event (何かがトリガーになって1度だけ実行する処理)
, Screen (システムとは関係ない、画面に描画する用の処理)
の3種類に分けて、それぞれの System
, Event
, Screen
は単一の処理しか行わないようにしています。単一責任の法則を守りやすくしています。
システムのインスタンスは内部で自動的に作成されます。(テンプレート)
app.add_system_to_scene(SysPlayerControl, "game", priority = 0, some_system_args)
# Parameters: システムタイプ, そのシステムを実行するシーン, 実行優先度, 任意のシステムパラメータ
for system in self.scene_systems[self.current_scene]:
system.process() # そのシーンで実行するシステムのみが優先度順に実行される。イベント、スクリーンも同様。
以上はシステムの例ですがイベント、スクリーンも同様。
▼ ステート駆動。必要最低限の状態管理・遷移機能(シーン・レベルマネージャーとして使えるもの)を実装。
以下は、ユーザーがスペースキーを押すとゲームのシーンが "launch"
から "game"
に遷移する例です。
app = App()
...
app.current_scene = "launch"
# シーン・ステート遷移は遷移元、遷移先、トリガーを与えて設定する。
app.add_scene_transition(scene_from = "launch", scene_to = "game", triger = lambda: pyxel.btn(pyxel.KEY_SPACE))
# シーンで常時処理するわけではないイベントも同様。
app.add_event_to_scene(EvChangeBallColor, scene = "game", triger = lambda: pyxel.btnp(pyxel.KEY_C), priority = 0)
以上から分かるように極論言うとこのフレームワークが無くても開発はできますが、あれば仕様上の決まりごとに従って綺麗に開発ができるのではないかと思います。
◼ 使うといいのは、どんなとき?
以下で述べていることは ECS を採用する際の一般的な利点とは異なります。一般的に言われている ECS の利点は、メモリ効率が向上するため特に大量のオブジェクトを必要とするゲームのパフォーマンス改善が可能になるというものです。
しかし Python はそもそもメモリ効率を追い求めるのに適した言語ではありません。以下で利点として述べているのは、ECS をオブジェクト志向と比較した場合に開発体験上、筆者が感じた良い点というだけです。
例として話題になったスイカゲームのフォロワーゲームを作り始めたとしましょう。
- 大きさ、テクスチャの異なる幾つかの円の中から1つをランダムに選ぶ。
- 選ばれた円が画面上部にセットされる。このとき次にセットされる円もランダムに予め選ばれる。
- マウス・タップによるドラッグ操作により、画面上部で円のx座標を更新する。
- マウス・タップが離れるとx座標は固定され落下し始める。
- 落下を始めた円は操作できず、他のすでに落下していた円を含めて接触判定がなされる。(円の境界で接触を判定)
- ...
みたいな仕様のもとに作り始めたとき、もしかすると
仕様 3 と仕様 4 を同じシステムとして実装してしまうかもしれません。また仕様 3 や仕様 4 でやりたい大事な処理はユーザーのインプットがトリガーになっているので、イベントハンドリングもシステム内に実装するかもしれません。
そうすると、もしユーザーのインプットをマウスだけじゃなくてコントローラーでもやりたくなったときや、3 と 4 の間に新たな処理を追加したくなったときにコードの修正箇所が散らばったり、本来修正が不要な関数やクラスへも修正箇所が及ぶかもしれません。
またキー入力、マウス操作などの物理システム、グラフィック処理は使用するゲーム開発エンジンにほとんどが依存しますが、裏側で動いているシステムは非依存なことが多いでしょう。
Python 製のゲームを例えば Pygame から Pyxel に移植しようと考えるとき、物理システムとグラフィックは書き直す必要がありますが、それ以外の部分はコードそのままでいける、みたいなのができたら良さそうです。
もちろん OOP でもきちんと SOLID 原則を守っていれば、ここで述べている問題は発生しませんし問題にならないかと思います。
以上のことを踏まえると、ゲームの状態管理・遷移
, 状態に応じて実行するシステムの実装
, ゲームエンジンに依存する描画の実装
, 物理システムの実装
, 登場人物、アイテムなどのゲームオブジェクトの実装
などの分離をコーダーではなくフレームワークやライブラリに任せるのが良さそうです。
メモリ効率化はもちろん ECS を採用する最大のモチベーションだと思いますが、開発体験上の利点においては SOLID 原則が守りやすくなるというのがあると感じています。
ですので、このライブラリも諸々の分離をやりやすくして、機能間の連携をやっています。system.py
, event.py
, screen.py
, component.py
, spawner.py
, input.py
のように自然と切り離したくなるような仕様になっています。
Python の場合ではメモリ効率化云々ではないので、このライブラリの使い所はプロジェクトの規模が大きくなっていくとコードが滅茶苦茶になるのを防ぐことに尽きるのかなと思います。
◼ さいごに
Python で書ける軽量なレトロゲーム開発エンジン Pyxel と Pigframe を使用した例を作ったので、気になった人は覗いてみてください。
Pygame と一緒に使用した例を以下に追加しました。
また主な使い方は、日本語版 README を御覧ください。
以上、ご覧下さりありがとうございました。
まだまだ至らない点があることは認識しており、バージョンアップで破壊的変更が行われる可能性があります。