LoginSignup
5
1

More than 3 years have passed since last update.

akashicでRTS的なゲームを作った際の設計の話

Last updated at Posted at 2019-12-08

この記事は、Akashic Engine Advent Calendar 2019の9日目の記事です。

はじめに

  • Akashic Engineを使えばニコ生で遊べるミニゲームを作ることができます。
    • それについての詳細はこちらを参照していただければと思います。
  • RPGやSLGなどの長時間遊べる系のゲームはあまり作られていないように思えたので、RTSを作ってみました。
  • RTS自体の作り方よりも、その時に行った設計の話が中心になります
  • 説明の際、コードはtypescriptで記載しています。

設計

大まかな設計方針

以下に、僕がRTSを作る際に意識した大まかな設計方針を記載しておきます。

  • Scene毎にg.Sceneを継承したものを用意しておく
  • 基本的にSceneにエンティティ等定義しておく
  • Sceneやエンティティのイベントハンドラに登録するイベントは、Sceneとは別に~Eventという感じのクラス(以降、Eventと呼びます)を用意してそこで定義
  • エンティティ等1つの概念としてまとめられるものは~Modelという感じのクラス(以降、Modelと呼びます)にまとめて定義 (MVC等の設計手法で出てくるModelとは全くの別物です)
  • g.Sprite等のエンティティを管理するためのRepositoryを作成
  • 各クラスで横断的に利用される定数はConfigにひとまとめにしておく

Scene

ここでは、どのような感じでSceneの設計をしたか説明していきます。
Scene毎にg.Sceneを継承するという話を前述したと思いますが、僕はそこに1つこのゲーム用の基底Sceneクラスを作成して、各Sceneでそれを継承するようにしています。
以下にゲームで使っている基底Sceneクラスのコードを記載します。

export abstract class GameSceneBase extends g.Scene {
    private _id: string; // g.Sceneにid的なものはないので、ここで用意

    constructor(param: g.SceneParameterObject, id: string) {
        super(param);
        this._id = id;
    }

    get id() {
        return this._id;
    }

    protected initialize(): void {
        this.loaded.add(() => {
            // どのSceneでも共通でロードする処理をここに記述
        });
        this.loaded.add(this.onLoaded, this);
    }

    // Scene毎の個別の初期化処理を記述するためのメソッド
    protected abstract onLoaded(): void;
}

こういった基底を作らずに各Sceneでg.Sceneを直接継承するようにしても問題はないのですが、僕は自Sceneを自動popする機能が各Sceneで欲しかったのでこのような基底を作りました(現在のAkashic Engineには指定のSceneまでpopし続けるという機能が無く、それをやるためには各Sceneでpopする必要があるため)。

次はこの基底を継承して作ったSceneについて実際のコード(多少簡略化してますが)を用いて説明していきます。

export class GameFieldScene extends GameSceneBase {
    private eventHolder: GameFieldSceneEvent = undefined; // このSceneやエンティティのイベントハンドラに登録するイベントをまとめて持つもの
    private gameItemModels: GameItemModel[] = []; // ゲーム上のアイテムに関するエンティティ等ひとまとめにしたもの
    // 以下多いので省略

    constructor(param: g.SceneParameterObject) {
        super(param, "field");
        this.initialize();
    }

    protected onLoaded(): void {
        this.gameItemModels.forEach((itemModel) => {
            const itemSprite = itemModel.sprite;
            itemSprite.update.add(this.eventHolder.getItemEvent(itemModel));
            itemSprite.pointDown.add(this.eventHolder.getShowStatusEvent(itemModel));
            this.append(itemSprite);
        });
        // 以下省略
        this.eventHolder = new GameFieldSceneEvent({
            scene: this,
            // 以下省略
        });
}

上記のようなSceneを定義しています。
テンプレートではクラス定義をせず直接g.Sceneを利用していますが、RPGやSLGのような複数のSceneが必要になるゲームではこのようにg.Sceneもしくはその子クラスを継承して各Sceneを定義した方がより分かりやすい感じになるかと思います。
コードを見ていただければ察していただけると思いますが、Sceneで使うModelの定義やイベントハンドラの登録は全てここで行います。
エンティティの状態・振る舞い自体はModel側で行いますし、イベントの内容についてはEvent側で定義しますので、おそらくScene自体それほど膨大な内容にならないと思われます。

Event

次は、Eventについて説明をしていきます。以下がゲームで使っているEventクラスの一例となります。

// Sceneで定義されている全Modelをパラメーターとして受け取る
export interface GameFieldSceneEventParameter {
    scene: g.Scene;
    // 以下省略
}

export class GameFieldSceneEvent {
    private parameter: GameFieldSceneEventParameter;
    private eventCaches: {[key: string]: (ev: any) => void}; // 一度生成したイベントを

    constructor(parameter: GameFieldSceneEventParameter) {
        this.parameter = parameter;
        this.eventCaches = {};
    }

    getItemEvent(model: GameItemModel) {
        const event = () => {
            // ゲームのアイテムについて実際に行われる処理を記述
        };
        eventCaches[`item_${model.id}`] = event;
        return event;
    }
}

EventクラスはSceneクラスと1対1対応していて、そのSceneやScene上で利用されるエンティティに関する全てのイベントを定義するようにしています(そのため、量が膨大になり得る可能性もあるのですが。。)。
また、Sceneで定義された全Modelをコンストラクタの引数で受け取るようにしています。これは、あるエンティティに関するイベントで別のエンティティに干渉することが多々あるためです(複雑度が増してしまう危険性はありますが。。)。
あと、これは全てのEventクラスで行っているわけではないのですが、イベントハンドラから一度登録したイベントを削除したいこともあり得るので、その時は上記コードのeventCachesのようなものにイベントを作成する度にキャッシュし、そこからイベントを取得して削除するということも可能です。

Model

次はModelです。
説明のため、ゲームで使用しているものとは若干異なるModelを例として以下に記載します。

export interface GameCharacterModelParameters {
    sprite: g.Sprite;
    hp: number;
    affiliation?: GameTeamType;
    playerId?: string;
}

export class GameCharacterModel {
    protected currentHp: number; // このキャラクターの現在のHP
    protected maxHp: number; // このキャラクターのMAXのHP
    protected hpBarRect: g.FilledRect = undefined; // キャラクターに表示されるHPバー
    protected affiliationRect: g.FilledRect = undefined; // キャラクターのバックに表示される所属を意味する色

    private _sprite: g.Sprite;
    private _affiliation: string|null; // このオブジェクトの所属
    private _playerId: string|null; // このオブジェクトを所有するプレイヤーのID

    constructor() {
        // メンバ変数を初期化する処理等
    }
}

上記Modelのようにエンティティやプリミティブ変数など、1つの概念の状態としてひとまとまりにできるものをメンバ変数として保持しています。
ちなみに、このModelは以下の画像のキャラクターを1つのModelとして一纏めにしています(概念とは言っていますが、表示の都合上で1つにまとめてしまうこともあります)。
houkaisensou_chara_screenshot.png

Repository

次はRepositoryです。ここではg.SpriteのRepositoryのコードを例に説明していきます。

export interface SpriteParameter {
    // 具体的な内容は長いので省略
    // 主にg.Spriteで使うパラメータを登録
}

export class SpriteRepository {
    private static _instance: SpriteRepository;
    private spriteParameters: {[key: string]: SpriteParameter};

    private constructor() {
        this.spriteParameters = {};
    }

    public static get instance(): SpriteRepository {
        if (!this._instance) {
            this._instance = new SpriteRepository();
        }

        return this._instance;
    }

    register(id: string, parameter: SpriteParameter) {
        this.spriteParameters[id] = parameter;
    }

    generate(id: string, scene: g.Scene, targetCameras: g.Camera2D[] = []): g.Sprite {
        const parameter: SpriteParameter = this.spriteParameters[id];
        return new g.Sprite({
            scene: scene,
            // parameterの内容を記述
        });
    }

}

これには、同じ内容のg.Spriteを複数回生成する必要がある時に、生成処理の記述を省略することができるという利点があります(g.Spriteの生成回数自体は変わらないので処理スピード自体は早くなりませんが。。)。
g.Sprite以外のAkashicで定義されているオブジェクトにも同様のRepositoryを用意することで記述量の省略等が期待できると思います(特に大量のパラメーターを必要とするオブジェクトには有効なのではと思っています)。

Config

最後にConfigについてですが、やっていることは以下のようなどこからでも参照できる定数オブジェクトを用意することのみですので、特筆すべきことはありません。

export const config: any = {
    "title": {
        "labels": {
            "title": {...},
            "current_status": {...},
            "push_message": {...}
        },
        "asset_ids": [...]
    },
    "game_over": {
        "labels": {
            "main": {...},
            "sub": {...}
        },
        "asset_ids": [...]
    }
};

まとめ

今回はakashicでRTS的ゲームを作った時に用いた設計について説明してきました。
ただ、ここで紹介した設計はakashicでRPGやSLGの大きめのゲームを作る際に最適なものというわけではなく(むしろ間違いも多い気がしますが。。)、ほんの一例ですので、参考程度に捉えていただけると幸いです。

コード・ゲーム

上記のような設計で作成したコードとゲームは以下のようになります。
ゲームについては、バランス調整を盛大にミスったり、UIが分かりづらい、敵がスクロールをしないと見えない所からいきなり攻撃してきたり等ゲーム自体の根本的な設計が間違っていて酷い出来となっていますが。。(これはRTSについてちゃんと勉強しろというお話ですね。。)

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1