ネトゲ作りにMVVMパターンを応用してみたい

アバター
lriki
記事: 88
登録日時: 14年前

ネトゲ作りにMVVMパターンを応用してみたい

投稿記事 by lriki » 12年前

今までにネットワーク使ったゲームはいくつか作ってきたけど、「みんなP2PばっかりでちゃんとしたMOGとか作ったことないなー」な状態で、
今日のブドウ糖チョコレート入った思考は「よろしい、ならば設計だ。」へと迷走シフト。作る企画もないのに。
否。自分の武器を研いでおくことにはきっと意味があるはず。

と言いますか、いくつかツール作ってMVC とか MVVMパターンとかに触れて、
「コレ、ネトゲ作りに応用したら面白いんじゃない?」と考えたのが事の始まりだったり。
今作ってるエフェクトエディタは真面目にMVVMで実装してるのですが、けっこうしっくり来てるので。

そんなわけで、データの流れがイメージしやすいフレームワークというか、ツクールみたいにラフなノリで色々な
ゲームオブジェクトを管理するにはどうすればいいのか考察してみます。ついでに自分がこれまで作ってたクラスについて見直し。



まずはMVVMをもとに、使うパターンを簡単にまとめ。


■Model
データクラス。サーバに保存されるマスタデータ。
プレイヤーキャラであれば、レベルやHP等、純粋にプレイヤーキャラを表すデータだけを持つ。

ユーザーへのインターフェイスがGUIでもCUIでも、WindowsでもLinuxでも関係なく使えること。
→環境に依存する情報(HWNDとか、DxLibのグラフィックハンドルとか)はメンバに持たせない。
また、参照(ポインタ)も持たせない。必要な時は対象へのインデックスやハンドルを持つこと。

サーバでは常にインスタンスが存在し、ユーザーがログインしたり、新しいエネミーが生成された時などに
クライアントにロードする。
また、サーバとデータをやりとりするのはModelのみ。


■ViewModel
Modelをクライアント側に必要な情報を提供できるようにラッピングする。

例えば、プレイヤー名と所属パーティ名を結合した文字列を返す関数や
状態異常をアイコンIDの配列として返す関数等を実装し、Modelのデータを加工する。

インスタンスはクライアントのみに存在する。
Model と ViewModel は今回の場合は基本的に1対1で、サーバからロードされた Model は常に ViewModel にラッピングされる。


■View
画面の表示や入力など、直接ユーザーのインターフェイスとなる。
ViewModelから必要なデータを取得し、描画などを行う。
DxLibのグラフィックハンドルを持つのはこの部分。

インスタンスはクライアントのみに存在する。
ViewModel と View は 1対多。
実装にもよるけど、例えばプレイヤーキャラ1つを、あるクラスでは2Dグラフィックとして描画し、
あるクラスではGUIとしてパラメータを表示する、といったときはこの関係。


まとめは以上です。
ちなみにちゃんとしたMVVMは「WPF MVVM」等で検索…でオネガイシマス…

要は、データ本体と入力、描画部分を完全に分けてすっきりさせようって感じ。




で、こんな感じの建前で、自分の作った某ゲームの一部をぶった切ってみようと思います。
いけにえはこちら。

CODE:

// プレイヤーキャラクター
class Player
{
private:
    
    int         mHP;            // プレイヤーのHP
    int         mItems[10];     // 持ち物 (アイテム番号。10個まで)
    int         mEquipItem;     // 装備中アイテム
    Vector2     mPosition;      // 座標
    Vector2     mVelocity;      // 速度
    int         mGraphicHandle; // DxLib のグラフィックハンドル
    
public:
    
    // HP 操作
    int  GetHP() { return mHP; }
    void SetHP(int hp) { mHP = hp; }
    
    // アイテム追加
    void AddItem(int idx, int item_no)
    {
        mItems[idx] = item_no;
    }
    
    // 装備を item_idx 番目のアイテムに持ち替える
    void ChangeEquip(int item_idx)
    {
        int tmp = mEquipItem;
        mEquipItem = mItems[item_idx];
        mItems[item_idx] = tmp;
    }
    
    // 位置の取得
    Vector2 GetPosition() { return mPosition; }
    
    // フレーム更新
    void Update()
    {
        mPosition += mVelocity;
    }
    
    // 描画
    void Draw()
    {
        // キャラクターの描画
        DrawGraph(mPosition.x, mPosition.y, mGraphicHandle, TRUE);
        
        // 装備アイテム名の描画 (GetItemName() はアイテム名を取得する関数)
        DrawString(0, 0, GetItemName(mEquipItem), GetColor( 255 , 255 , 255 ));
    }
};


このPlayerのメンバ変数のうち、Modelにあたるのは
mHP
mItems
mEquipItem
の3つ。もし現在位置を保存する必要がある場合は mPosition も含まれます。

これらをまとめてModelにするわけですが、もうひとつ、通信のための通知処理を実装します。
必要なのは、
・サーバに変更を通知する
・サーバから通知された変更を適用する

実装すると以下のようなイメージ。(あくまでイメージ)

CODE:

class PlayerModel
{
private:
    
    int         mHP;            // プレイヤーのHP
    int         mItems[10];     // 持ち物 (アイテム番号。10個まで)
    int         mEquipItem;     // 装備中アイテム
    
public:
    
    // HP 操作
    int  GetHP() { return mHP; }
    void SetHP(int hp)
    {
        mHP = hp;
        OnPropertyChanged("HP", mHP);
    }
    
    // Item 操作
    int  GetItem(int idx) { return mItems[idx]; }
    void SetItem(int idx, int item_no)
    {
        mItems[idx] = item_no;
        OnPropertyChanged("Items", mItems, sizeof(mItems));
    }
    
    // Equip 操作
    int  GetEquipItem() { return mEquipItem; }
    void SetEquipItem(int item_no)
    {
        mEquipItem = item_no;
        OnPropertyChanged("EquipItem", mEquipItem);
    }
   
public:
    
    // データの更新
    void OnUpdateProperty(const char* name, 色々 value)
    {
        switch (name)
        {
            case "HP"       : mHP = value; break;
            case "Items"    : mItems = value; break;
            case "EquipItem": mEquipItem = value; break;
        }
    }
    
protected:
    
    // データ変更の通知
    void OnPropertyChanged(const char* name, 色々 value)
    {
        // サーバに name と value を送信する処理
    }
};

ちょっとイメージしにくいと思いますが、Set~() を呼んだら以下のように流しましょう、という感じです。
121230.png
121230.png (6.81 KiB) 閲覧数: 257 回
Model は OnUpdateProperty() と OnPropertyChanged() 以外の関数は全部 Getter Setter に統一するのがいいかも。



次に ViewModel。
上記Modelを利用して以下のように実装します。
Player クラスから Model に取り出した部分を、Getter Setter に書き換えただけです。

CODE:

class ViewModel
{
private:
    
    PlayerModel*    mModel;
    Vector2         mPosition;      // 座標
    Vector2         mVelocity;      // 速度
    
public:
 
    // [追加]初期化
    void Initialize(PlayerModel* model)
    {
        mModel = model;
    }
    
    // [追加]装備アイテムの取得
    int GetEquipItem() { return mModel->GetEquipItem(); }
    
    // アイテム追加
    void AddItem(int idx, int item_no)
    {
        mModel->SetItem(idx, item_no);
    }
    
    // 装備を item_idx 番目のアイテムに持ち替える
    void ChangeEquip(int item_idx)
    {
        int tmp = mModel->GetEquipItem();
        mModel->SetEquipItem(mModel->GetItem(item_idx));
        mModel->SetItem(item_idx, tmp);
    }
    
    // 位置の取得
    Vector2 GetPosition() { return mPosition; }
    
    // フレーム更新
    void Update()
    {
        mPosition += mVelocity;
    }
};

最後に View です。

CODE:

class PlayerView
{
private:
    
    ViewModel* mViewModel;
    
public:
    
    // 初期化
    void Initialize(ViewModel* view_model)
    {
        mViewModel = view_model;
    }
    
    // 描画
    void Draw()
    {
        // キャラクターの描画
        DrawGraph(mViewModel->GetPosition().x, mViewModel->GetPosition().y, mGraphicHandle, TRUE);
        
        // 装備アイテム名の描画 (GetItemName() はアイテム名を取得する関数)
        DrawString(0, 0, GetItemName(mViewModel->GetEquipItem()), GetColor( 255 , 255 , 255 ));
    }
};
今回の場合、View はただ座って ViewModel の状態を見てるだけいうレベルまでシンプル。



以上、Model、ViewModel、View の実装例です。

細かい部分を端折りまくってますが、このように実装することで
通信周りの複雑になりがちな部分は全部 Model 内で完結できますし、
DxLib から別の描画ライブラリへの乗せ替えも View を書き換えるだけでできたりと、
作業ごとに頭を集中させる部分を限定できるし、変更に強いシステムもできるんじゃないかと思います。





・・・なんか見直してみてめちゃくちゃ恥ずかしくなってきましたけど、せっかく書いたので投稿します。

「こんなもの使い物になるかァ!!」ドゴォン!!
「いいか若造!いまの現場じゃなぁ…!」
等々、ご意見がありましたら是非おねがいします。

長文、読んでいただいてありがとうございました。

アバター
h2so5
副管理人
記事: 2212
登録日時: 14年前

RE: ネトゲ作りにMVVMパターンを応用してみたい

投稿記事 by h2so5 » 12年前

サーバーに永続的には保存されないが同期が必要なデータの扱いはどうなるのでしょうか。
(例えば座標データは保存する必要がない場合もあると思います)

Modelはサーバに保存されるデータを扱うようですが、
一方でサーバとデータをやりとりするのはModelのみとなっているのでModelに含まれないデータは同期できません。

アバター
lriki
記事: 88
登録日時: 14年前

Re: ネトゲ作りにMVVMパターンを応用してみたい

投稿記事 by lriki » 12年前

早速のコメントありがとうございます。


>サーバーに永続的には保存されないが同期が必要なデータの扱いはどうなるのでしょうか。
うぅむ…浅はかでした…。
とりあえず全部Modelに…とか考えましたが、これだとサーバを圧迫するだけですよね・・・。


>Modelはサーバに保存されるデータを扱うようですが
確かに「保存」って定義すると制約がありすぎますね・・・。
これもちゃんと考え直さないと。


全体的に一時データの扱いが甘すぎていますね・・・。
ご指摘、ありがとうございました。すごく助かりました。