コンテンツにスキップ

ノードとロジックの分離

Promete では、ノードは「画面に映るもの」を表すだけにして、ゲームのロジックはノードの外に置くことを基本方針にしています。 このページでは、その考え方と理由、実際の書き分け方を説明します。

Promete 自身の機能(コルーチンなど)もこの方針で作られています。アプリを作るときにも同じ考え方を使うと、コードの見通しがよくなります。

ノードは、次の 2 つを担当します。

  • 親子関係をたどった位置・回転・拡大率の計算(変形の階層)
  • 画面に描くための描画コマンドの発行

つまりノードは「シーンの見た目」を表すデータであり、ゲームのルールや進行を決める場所ではありません。 Sprite や Text は、どんな画像・文字をどこに描くかだけを知っていれば十分です。

ロジックをノードに入れると起きること

Section titled “ロジックをノードに入れると起きること”

ノードの更新処理に、敵の移動や当たり判定などのロジックを書きたくなることがあります。小さなゲームなら動きますが、規模が大きくなると次のような問題が出てきます。

  • 動く順番が見えなくなる。 更新はノードツリーをたどりながら行われるため、順番がツリーの構造や ZIndex に左右されます。
  • 止めにくい。 ポーズやコマ送りを実装しても、ノードの中で勝手に動いている処理には効かないことがあります。
  • ロジックが散らばる。 「敵の挙動」がどのノードのどのメソッドに書かれているのか、探さないと分からなくなります。

ロジックを 1 か所にまとめ、いつ・何が動くかを自分で決められるようにしておくと、これらを避けられます。

画面ひとつ分の進行は、Scene に書きます。シーンはノードを作って画面に追加する役目も持っているので、ロジックの入り口として自然な場所です。

敵の管理、スコア計算、弾の発射のように、複数のシーンから使う、あるいはシーンが大きくなりすぎるものは、専用のクラスに切り出します。 Use<T>() で登録しておけば、シーンのコンストラクタで受け取れます。

// ゲーム内の敵を管理するサービス
public class EnemyService
{
private readonly List<Enemy> _enemies = [];
public EnemyService(PrometeApp app)
{
// 毎フレームの処理を自分で購読する
app.Update += Tick;
}
public void Spawn(Enemy enemy) => _enemies.Add(enemy);
private void Tick()
{
foreach (var enemy in _enemies)
enemy.Move();
_enemies.RemoveAll(e => e.IsDefeated);
}
}
// 敵は「状態」と「見た目のノード」を持つ普通のクラス
public class Enemy(Sprite sprite)
{
public bool IsDefeated { get; private set; }
public void Move() => sprite.Location += (1, 0);
}
var app = PrometeApp.Create()
.Use<EnemyService>()
.BuildWithOpenGLDesktop();

ノード(Sprite)は見た目だけを受け持ち、動かすのはサービスの Tick です。ポーズしたいときは、Tick の先頭で判定すれば、敵に関するロジックをまとめて止められます。

コルーチンの CoroutineManager も、この形で作られています。

  • 通常のクラスとして、Use<CoroutineManager>() で登録する
  • 自分で app.Update を購読して、毎フレームの処理を行う
  • 開始時にハンドル(Coroutine)を返し、そのハンドルで停止できる
  • シーンが切り替わるときに、不要になったものを自動で片付ける

ノードのツリーには何も追加せず、外側から時間を進めています。アニメーションの補間(Tween)や当たり判定のような、ノードを「外から動かす・調べる」機能も、同じ形で作るのが Promete の方針です。

迷ったときは、次のように考えてください。

やりたいこと置き場所
何をどこにどう描くかノード
画面ごとの進行シーン
複数のシーンやノードにまたがるルール、状態の管理サービスクラス
時間をかけて少しずつ変化させる、順番に待つコルーチン

「そのノードがなくなったら、この処理も不要になるか?」を考えるのも有効です。 不要にならない処理(スコア計算など)は、ノードの外に置くべきです。

  • 複数のサービスが同じフレームで動く場合、実行順に依存する処理は 1 つのサービスにまとめてください。サービス間の実行順に頼るコードは、後から追いにくくなります。
  • ノードが破棄されたとき、そのノードを参照しているサービス側の状態(上の例の Enemy)は自動では片付きません。IsDestroyed を確認するか、自分で破棄の通知を受け取って片付けてください。