今日のブドウ糖チョコレート入った思考は「よろしい、ならば設計だ。」へと迷走シフト。作る企画もないのに。
否。自分の武器を研いでおくことにはきっと意味があるはず。
と言いますか、いくつかツール作って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」等で検索…でオネガイシマス…
要は、データ本体と入力、描画部分を完全に分けてすっきりさせようって感じ。
で、こんな感じの建前で、自分の作った某ゲームの一部をぶった切ってみようと思います。
いけにえはこちら。
// プレイヤーキャラクター
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にするわけですが、もうひとつ、通信のための通知処理を実装します。
必要なのは、
・サーバに変更を通知する
・サーバから通知された変更を適用する
実装すると以下のようなイメージ。(あくまでイメージ)
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~() を呼んだら以下のように流しましょう、という感じです。 Model は OnUpdateProperty() と OnPropertyChanged() 以外の関数は全部 Getter Setter に統一するのがいいかも。
次に ViewModel。
上記Modelを利用して以下のように実装します。
Player クラスから Model に取り出した部分を、Getter Setter に書き換えただけです。
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 です。
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 ));
}
};
以上、Model、ViewModel、View の実装例です。
細かい部分を端折りまくってますが、このように実装することで
通信周りの複雑になりがちな部分は全部 Model 内で完結できますし、
DxLib から別の描画ライブラリへの乗せ替えも View を書き換えるだけでできたりと、
作業ごとに頭を集中させる部分を限定できるし、変更に強いシステムもできるんじゃないかと思います。
・・・なんか見直してみてめちゃくちゃ恥ずかしくなってきましたけど、せっかく書いたので投稿します。
「こんなもの使い物になるかァ!!」ドゴォン!!
「いいか若造!いまの現場じゃなぁ…!」
等々、ご意見がありましたら是非おねがいします。
長文、読んでいただいてありがとうございました。