名称未設定2

ゲーム実装のデザインパターン

ぼっちgamedev Advent Calendar 2018の2日目。
・単純な実装
・MVCモデル
・構造体の活用
・継承ベースによるコンポーネント化
・コンポジションベースによるコンポーネント化
について書く。

単純な実装

スーパーマリオブラザーズのような横スクロールアクションゲームの実装を考える。

ゲームを構成する要素を簡単に列挙する。
・自機(X・Y座標)
・敵キャラ(X・Y座標)
・ステージ(各マスのブロック)
これらをデータとして考えると、以下のようなコードが書ける。

fn update(player_x: &mut i32,
          player_y: &mut i32,
          enemy_x: &mut i32,
          enemy_y: &mut i32,
          stage: &[[i32; WIDTH]; HEIGHT],
          should_quit_game: &mut bool)
{
    let input_state = input();
    move_player(input_state, player_x, player_y);
    move_enemy(enemy_x, enemy_y);
    *should_quit_game = check_game_over(player_x, player_y, enemy_x, enemy_y, stage);
}

fn render(player_x: &i32,
          player_y: &i32,
          enemy_x: &i32,
          enemy_y: &i32,
          stage: &[[i32; WIDTH]; HEIGHT])
{
    render_stage(stage);
    render_player(player_x, player_y);
    render_enemy(enemy_x, enemy_y);
}

fn main() {
    let mut player_x = 0;
    let mut player_y = 0;
    let mut enemy_x = 0;
    let mut enemy_y = 0;
    let stage = [[/* 0 or 1 */]];
    let mut should_quit_game = false;
    while !should_quit_game {
        update(&mut player_x, &mut player_y, &mut enemy_x, &mut enemy_y, &stage, &mut should_quit_game);
        render(&player_x, &player_y, &enemy_x, &enemy_y, &stage);
    }
}

敵キャラは複数存在するが、今回は簡単に考えるため1体のみとする。ステージは、ブロックの有無を0と1で表現する二次元配列とした。

この規模の例だとそこまで複雑化しないが、冗長である感じは拭えない。また、この調子で要素を増やし続けるとupdateとrenderに渡す引数がどんどん増加すると考えられる。update関数とrender関数に切り出さず、全てmain関数内で完結させれば引数問題は解決するが、やはり冗長である。

MVCモデル

Webフロントエンドの話題でよく槍玉に挙げられるMVCモデルというものがある。

MVCモデルはModel・View・Controllerの3要素からなり、矢印は処理の流れを表している。ControllerはUserからの入力を意味のある操作に変換し、Modelを操作する。Modelは内部状態やルールなど、そのアプリケーションの動作に関わる全てを表す。ViewはUserの見る画面を表す。

これをゲーム向けに言い換えると、以下のような対応関係が取れる。
User → プレイヤー
Controller → 物理コントローラからの入力をゲームの操作に変換する層
Model → ゲームのデータとUpdate関数群
View → Render関数群

上に示したコードは単純な実装と言いつつ、実はこのMVCに対応する形になっている。そもそもMVC自体が単純なモデルだ。

構造体の活用

単純なゲームの実装は基本的にMVCモデルとなる。しかし依然としてコードは冗長である。例えば自機と敵キャラのX・Y座標の概念は共通のものだ。ならば新たにX・Y座標型を定義すれば良い。X・Y座標を持つPoint構造体を作ると、update関数は以下のようになる。

struct Point {
    x: i32,
    y: i32,
}

fn update(player: &mut Point,
          enemy: &mut Point,
          stage: &[[i32; WIDTH]; HEIGHT],
          should_quit_game: &mut bool)
{
    let input_state = input();
    move_player(input_state, player);
    move_enemy(enemy);
    *should_quit_game = check_game_over(player, enemy, stage);
}

これで引数の数が半分に減り、間違えてY座標を渡すべき所にX座標を渡してしまうようなミスも未然に防げる。

ところで、自機と敵キャラが共通の要素(座標)を持つのは自明だが、両者は必ずしも同じ概念とは限らない。例えば自機にはリスポーン可能回数やコインの枚数などのデータを持たせても良い。となると、自機と敵キャラは異なるデータ構造を持つことになる。

継承ベースによるコンポーネント化

まとめた共通項(Point)=コンポーネントを活かしつつ、新たな要素を自機・敵キャラそれぞれに足していく方法として、クラスの継承が考えられる。しかしRustにはクラスという概念は無いため、一時的にJavaScriptで記述する。

class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
}

class Player extends Point {
    constructor(x, y) {
        super(x, y)
        this.respawnCount = 3
        this.coin = 0
    }
}

class Enemy extends Point {
    constructor(x, y) {
        super(x, y)
    }
}

今の状況の場合、これは正しいように見える。

しかし現実はより複雑な状況が多い。例えば自機と敵キャラを画面に表示するための画像のデータが必要だ。これらは自機と敵キャラで共通しているため、コンポーネント化したいと思うだろう。Playerの継承元にImageクラスを追加したい。

class Player extends Point, Image {
    constructor(x, y, image) {
        super(x, y)
        // ...?
    }
}

ここで気付くことは、JavaScriptが基本的に複数クラスの継承(多重継承)を許していないことだ(不可能では無い)。実は多くのプログラミング言語はクラスに相当するものの多重継承を許していない。多重継承をサポートしているのは知るところでC++とPythonとCommon Lispぐらいだ。

妥当な解決策としてよく取られるのが、RenderableObjectとして画像と座標をひとまとめに扱うことだ。

class RenderableObject {
    constructor(x, y, image) {
        this.x = x
        this.y = y
        this.image = image
    }
}

class Player extends RenderableObject {
    constructor(x, y, image) {
        this.respawnCount = 3
        this.coin = 0
    }
}

コンポジションベースによるコンポーネント化

クラスを継承したいモチベーションの1つが、そのクラスにおける操作も同時に継承したいというものだ。例えばPointは、他のPointとの距離を求める操作が考えられる。しかし本当にクラスを継承する必要があるのだろうか。

単純に、オブジェクトの中に別のオブジェクトを持たせるという方針が考えられる。これをコンポジションという。Rustでは以下のように書ける。

struct Player {
    position: Point,
    image: Image,
    spawn_count: u32,
    coin: u32,
}

struct Enemy {
    position: Point,
    image: Image,
}

また、継承ベースで紹介したRenderableObjectのようにまとめると以下のようになる。

struct RenderableObject {
    position: Point,
    image: Image,
}

struct Player {
    renderable: RenderableObject,
    spawn_count: u32,
    coin: u32,
}

struct Enemy {
    renderable: RenderableObject,
}

Player構造体とEnemy構造体にそれぞれrenderableという同名のフィールドが存在するが、renderableというフィールドを持っている構造体としてPlayerとEnemyを同一視しようとすると、以下のように書ける。

struct Character<T> {
    renderable: RenderableObject,
    prop: T,
}

struct PlayerProperty {
    spawn_count: u32,
    coin: u32
}

struct EnemyProperty {}

type Player = Character<PlayerProperty>
type Enemy = Character<EnemyProperty>

Character構造体は総称型(ジェネリクス)と呼ばれるもので、型仮引数Tを取る型として定義される。ジェネリクスは一般的なオブジェクト指向プログラミング言語で利用可能である。

まとめ

・単純なゲームはMVCモデルに従う
・ゲームはコンポーネント化無しで実装すると冗長
・構造体を活用して共通のデータをまとめる
・継承よりもコンポジションが良い
・似たコンポーネントは総称型によって同一視可能

明日は2D描画とそのライブラリについて書きたい。

この記事が気に入ったらサポートをしてみませんか?