Pythonで ECS がしたいときに使いたい ECS + シーンマネージャーが1つになったライブラリを公開した

## Pythonで ECS がしたいときに使いたい ECS + シーンマネージャーが1つになったライブラリを公開した

開発言語を Python にしたゲーム開発で ECS (Entity Component System) を採用したいケースはごく稀だとは思います。普通は Unity や Unreal エンジンにネイティブで実装されている ECS 機能を使うはずです。また Rust では Bevy のような ECS ライブラリもあります。

ただプログラミング学習者やなんらかの要件で Python で開発することになった場合の選択肢は少ないです。

また自分も個人開発をするうえで、ECS ライブラリにあーこの機能もついていると嬉しいんだけどなと思った経験がありました。

そこで
1. ECS フレーム
1. 必要最小限のシーンマネージャー
1. 必要最小限のゲームエンジン依存の物理システムを吸収してくれる

が1つのライブラリとして提供されている Python 製のライブラリを開発して <strong>Pigframe</strong> として公開しました。

![Pigframe](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1419783/064ce5c7-8f76-acdb-f096-e29ff6ee7863.jpeg)

https://github.com/passive-radio/pigframe

Pygame, Pyxel などの Python のゲームエンジンを使って開発する個人開発者、インディーデベロッパーの利用を想定しています。

まずインストール方法ですが、PyPI に公開していますので pip でインストールできます。pigframe が依存しているライブラリはありません。

```bash
pip install -U pigframe
```

## ◼ やってくれること
- 一般的な ECS の処理
- ステート管理
- ゲームエンジン依存の物理システムを吸収

この3つです。

さっさと使い方を見たい方は <strong>[日本語版 README](https://github.com/passive-radio/pigframe/blob/main/docs/README-ja.md)</strong> を参照してください。

また ECS とはなんぞやについては以下の記事などが参考になります。

https://zenn.dev/kd_gamegikenblg/articles/4ca7b1ec032329

https://www.f-sp.com/entry/2019/04/18/175747

<b>Pigframe</b> の主な機能。
| 機能 | 対応 | 備考 | 
| ---- | -------| --- |
| 基本的な ECS framework の機能 | o | |
| シーンマネージャー | △ | 小規模なインディーゲームくらいなら全然使える必要最低限の機能 |
| ゲームエンジン依存の物理システムを吸収 | △ | 〃 |
| グラフィック | x | ゲームエンジンに任せる |
| サウンド | x | 〃 |
| 衝突検知などの一般的なゲームロジック | x | ゲームエンジン、利用者に任せる |

必要最小限の ECS + シーンマネージャーが合体したライブラリであり、それ以上ではありません。
Unity とか Unreal Engine, Godot のようなリッチな統合開発環境とも言えるゲームエンジンとは使用シーンも異っていて、パフォーマンスの最適化が求められる、重い 3D グラフィックなどの処理をするゲームには全く向いていません。

基本的には Python 製のシンプルな ECS + シーンマネージャーなので機能は想像できるかもしれませんが、具体的な機能を以下で紹介します。

### ECS 関連
#### ゲームに登場させる登場人物やオブジェクトはエンティティー (厳密には entity_id) で管理する。
entity_id は int 型で特に指定がない限り World をコンストラクトしてから 0 始まりで increment されます。

```python
app = App()
entity1 = app.create_entity() # -> int: 0
entity2 = app.create_entity() # -> int: 1
```

以下のように `id` を指定して生成することも可能です。これは異なるゲームセッション間でセーブデータを元に前のセッションの状態を復元したいときに便利かと思います。

```python
entity1 = app.create_entity(100) # -> int: 100
entity2 = app.create_entity() # -> int: 101
```
    
#### アーキタイプ (エンティティーとコンポーネントデータの紐づけ)

`World` クラスが担当します。また、他の System, Event なども同様に `World` クラスが管理します。

コンポーネントのインスタンスは内部で自動的に作成されます。(テンプレート)

```python
x = 1
y = 1
app.add_component_to_entity(entity1, Position, x=x, y=y)
# Parameters: entity ID, 紐付けるコンポーネントの型、コンポーネントのパラメータ
assert app.get_entity_object(entity1)[Position] == Position(x=x, y=y) # -> True
```

#### すべての処理を `System`, `Event`, `Screen` という３種類のクラスに分けて管理

ゲーム上の各シーンで実行する一連の処理を 3種類に分けて管理するための <b>`System`</b> (登録したシーンでメインループで常時実行する処理), <b>`Event`</b> (トリガー発火時のみ実行する処理), <b>`Screen`</b> (システムとは関係ない、画面に描画する用の処理) という基底クラスを用意しています。これら3種類にシステムを分けて管理する、実装できるように設計しています。

システムのインスタンスは内部で自動的に作成されます。
```python
app.add_system_to_scene(SysPlayerControl, "game", priority = 0, some_system_args) 
# Parameters: システムタイプ, そのシステムを実行するシーン, 実行優先度, 任意のシステムパラメータ
```

内部では以下のように各シーンに登録されたシステムが優先度順に実行されます。
Event, Screen も同様です。
```python
for system in self.scene_systems[self.current_scene]:
    system.process()
```

### シーンマネージャー機能

以下は、ユーザーがスペースキーを押すとゲームのシーンが `launch` から `game` に遷移する例です。

```python
app = App()
...
app.current_scene = "launch"
# シーン・ステート遷移は遷移元、遷移先、トリガーを与えて設定する。
app.add_scene_transition(scene_from = "launch", scene_to = "game", triger = lambda: pyxel.btn(pyxel.KEY_SPACE))
```

```python
# シーンで常時処理するわけではないイベントも同様。
app.add_event_to_scene(SomeEventClass, scene = "game", triger = lambda: pyxel.btnp(pyxel.KEY_C), priority = 0)
```

### 物理システム関連

また自分があると嬉しいなと思ったので、これも必要最低限ですが物理システムをラップして使えるようにしています。以下は Pyxel と一緒に使った例です。

```python
import pyxel
from pigframe import ActionMap, World
from dataclasses import dataclass

@dataclass
class Input():
    jump: tuple = pyxel.btnp, pyxel.KEY_SPACE, pyxel.GAMEPAD1_BUTTON_A
    click: tuple = pyxel.btnp, pyxel.MOUSE_BUTTON_LEFT
    ...

class Game(World):
    ...
    def init():
        self.set_user_action_map(Input())
```

このようにゲームエンジン側で実装された物理システムを登録しておくと、以下のように System などのロジックに使うことができます。

```python
class SysJump(System):
    def process(self):
        _, velocity = self.world.get_components(Player, Velocity)[0][1]
        if self.world.actions.jump: # スペースキーが押されると発火
            velocity.y -= 1
```

これは自分で使っていても便利だなと思っています。キーコンフィグ機能を実装するときにも使えるなぁと思っています。

## ◼ 使うといいのは、どんなとき？

以下で述べていることは ECS を採用する際の一般的な利点とは異なります。一般的に言われている ECS の一番の利点は、メモリ効率が向上するため特に大量のオブジェクトを必要とするゲームのパフォーマンス改善が可能になるというものかと思います。

自分はゲーム開発会社で働いた経験はないですが、きっちとしたチームで開発する場合は以下で挙げるゲームを構成する各コンポーネントの分離は厳密にやっている印象があります。

:::note info
以下全ての処理が個人開発だと、ごっちゃになりやすい
1. ゲームの状態管理・遷移
1. 状態に応じて実行するシステムの実装
1. ゲームエンジンに依存する描画の実装
1. 物理システムの実装
1. 登場人物、アイテムなどのゲームオブジェクトの実装
:::

一方で自分のように趣味でゲームを開発していたり勉強しているような人は、ちゃんと意識しないとゲームロジックやデータ管理がごちゃ混ぜになりがちで結構しんどいと思います。

なので
1. ECS でゲームの全ては整数のオブジェクトのIDとデータとして管理して、ロジックは完全に分離できて
1. ある程度使えるシーンマネージャーも別で存在していて
1. ゲームエンジン依存の物理システムをある程度吸収してくれる機能もあって

が揃っていると、結構嬉しいと感じる個人開発者は少なくないのではと思います。

もちろん一般的にはメモリ効率の向上で大量のオブジェクトが出てくるゲームのパフォーマンスが向上するのが ECS を採用する最大のモチベーションだと思いますが。

Python は実行速度やパフォーマンスの最適化を求める言語ではないと思いますので、このライブラリの使い所はプロジェクトのある程度規模が大きくなっていっても見通しの良いコード管理、開発がしやすくなる点かなと思います。

## ◼ さいごに

Python で書ける軽量なレトロゲーム開発エンジン Pyxel と Pigframe を使用した例を作ったので、気になった人は覗いてみてください。

https://github.com/passive-radio/pigframe/tree/main/src/pigframe/examples/

:::note info
Pygame と一緒に使用した例を以下に追加しました。
:::

https://github.com/passive-radio/pigframe/tree/main/src/pigframe/examples/pygame_control_a_ball

また主な使い方は、<strong>[README](https://github.com/passive-radio/pigframe/blob/main/docs/README-ja.md)</strong> を御覧ください。

以上、ご覧下さりありがとうございました。

:::note warn
まだまだ至らない点があることは認識しており、今後のバージョンアップで破壊的変更が行われる可能性があります。
:::