Edited at

【ゲームで学ぶオブジェクト思考】状態をどう作る?

More than 1 year has passed since last update.


はじめに


免責事項

記事中のコードはc++を基本として書いていますが

あくまで考え方を示すサンプル的な立ち位置です。

動作確認はしていないので、コードがおかしいところは脳内補間していただければと思います。


前に書いた記事

【ゲームで学ぶオブジェクト思考】HP(ヒットポイント)をどう作る?

よければこちらもご覧ください。


状態とは

キャプチャ.PNG

こんな感じのやつについてです。

毒ってると体力が徐々に減ったり、眠ってると行動とれなくなったり

というのを判断するために状態について考えていこう。


シンプルに作ってみる

ゲーム作りはじめのころはこんな感じで状態を作っていました。


各種状態のフラグを用意

bool isPalsy  = false; // 麻痺

bool isPoison = false; // 毒
bool isSleep = false; // 睡眠
bool isAnger = false; // 怒り
bool isSad = false; // 悲しみ


麻痺ってる処理はこう

// 麻痺ったら、麻痺フラグをtrue

isPalsy = true;

// 麻痺ってるね?
if(isPalsy){
// Yes I do!
}


毒ってる処理はこう

// 毒ったら、毒フラグをtrue

isPoison = true;

// 毒ってるね?
if(isPosion){
// Yes I do!
}

いいかんじ、いけてるいけてる。


他にもありそうな処理を考えてみよう。

// 状態異常にかかってないよね?を調べるならこう

if(!isPalsy && !isPoison && !isSleep && !isAnger && !isSad) {
// I am good!
}

// 万能薬を使った時は状態異常を全部直すぞ
isPalsy = isPoison = isSleep = isAnger = isSad = false;

やばい、これ新しい状態が増える度に修正入るパティーン


状態毎にいちいちフラグ作りたくないぞ

このままだと



  • 沈黙とか新しい状態増えるとフラグ新しく作らないといけない。

  • 万能薬の処理とか判定も書き直さないといけない。

  • バグらせる自信がつく。

こんな時はあれだな、あれ、BITフラグ


BITフラグとは?

これはオブジェクト思考ではなくて

プログラムのテクニック的な話ですが簡単に説明をしておきます。

char state = 0;

例えばこのstateですが、パソコンの内部ではどうなってるでしょうか?

はい、こうなってます。

キャプチャ2.PNG

パソコンは内部ではデータを0と1だけで表現しています。

つまるところ、char型の変数というのは0か1を入れられる箱が8個あるのです。

これってbool型のフラグが8個あるのと同じですよねっていう。

キャプチャ3.PNG

この1bitをフラグとして使おうっていうのがBITフラグです。

さて、という事でBITを扱う事を前提に状態を扱いやすいモノにしていこう。


状態をクラスにしよう

まずは状態(State)クラスを作るよ。

※なおbitの操作方法については本筋から外れるので割愛します。

class State 

{
private:
char mFlag; // 状態を管理するフラグ(8bitなので8種類まで管理できる)

public:
// コンストラクタではFlagを0に初期化(不定値対策)
State() :mFlag(0) {}

// ここから色んなメソッドを作っていくよ。
};


状態の種類を定義しよう。

// 状態の種類(列挙型使うとわかりやすいね)

struct Kind {
enum {
Palsy = 1 << 0, // 00000001 麻痺
Poison = 1 << 1, // 00000010 毒
Sleep = 1 << 2, // 00000100 睡眠
Anger = 1 << 3, // 00001000 怒り
Sad = 1 << 4, // 00010000 悲しみ
All = 0xFF, // 11111111 全て
};
};

状態の種類を列挙型で定義してみました。

それぞれ、bitを1桁ずつずらして1になるように。


状態を変更するメソッドを作っていこう。

// 状態をリセット(健康にする)

void reset() {
mFlag = 0; // 0にするだけ
}

// 指定された状態をON(有効)にする
void on(char flag)
{
mFlag |= flag; // 論理和をとればいいね。
}

// 指定された状態をOFF(無効)にする
void off(char flag)
{
mFlag &= ~flag; // BIT反転(チルダ記号)させて論理積とればいいね。
}


状態を判定するメソッドを作っていこう

// 聞かれた状態と一致する?

bool is(char flag)
{
return (mFlag == flag); // 完全一致なら比較するだけだね。
}

// 聞かれた状態のいずれかが該当する?
bool either(char flag) {
return (mFlag & flag) != 0; // 論理積の結果が0じゃないときだね
}

// 健康です。(悪いとこない)
bool isHealthy() {
return is(0);
}


使うときのイメージ


麻痺らせたり、毒らせたり

// 状態を用意。

State state;

state.on(State::Kind::Palsy); // 麻痺らせたぜ
state.on(State::Kind::Poison); // 毒らせたぜ


一発でいろんな状態にしたり

// 麻痺、毒、睡眠状態にしてやったぜ(bitなので論理和できる)

state.on(State::Kind::Palsy | State::Kind::Poison | State::Kind::Sleep);

// 全ステータス異常になぁれ
state.on(State::Kind::All);


状態を治してあげたり

state.off(State::Kind::Palsy);  // 麻痺解除だぜ

state.off(State::Kind::Poison); // 毒解除だぜ

// 万能薬のときはこれ。
state.reset();


状態を確認してあげたり

// 毒ですね?毒以外は大丈夫ですね?

if(state.is(State::Kind::Poison)) {
// はい、毒だけです。。。
}

// 毒か麻痺か睡眠かのいずれかにかかってますね?
if(state.either(State::Kind::Poison | State::Kind::Palsy | State::Kind::Sleep)){
// はい、どれかはわからないですがどれかにかかっています。。。
}

// めっちゃ元気ですか?悪いとこないですか?
if(state.isHealthy()){
// Yes! I'm Healthy
}


新しい状態異常を追加したり


struct Kind {
enum {
Palsy = 1 << 0, // 00000001 麻痺
Poison = 1 << 1, // 00000010 毒
Sleep = 1 << 2, // 00000100 睡眠
Anger = 1 << 3, // 00001000 怒り
Sad = 1 << 4, // 00010000 悲しみ
+ Burn = 1 << 5, // 00100000 火傷
All = 0xFF, // 11111111 全て
};
};

列挙型に追加するだけで済むね。


いい感じ?

だいぶ長くなりましたが、使いやすい状態クラスが出来てきました。

これにて完成!

としたいところですが、ただ状態をクラスにしただけで

オブジェクト思考的としてはまだ考える余地があります。


状態クラスをよく見てみよう

キャプチャ4.PNG

状態クラスはこんな感じです。

状態の種類はこんなのがあるよーっていう部分と

状態を覚えさせるためのスイッチ(フラグ)部分です。

正直このスイッチ部分って他でも使えると思いませんか?


状態とスイッチを分離しよう。

いままで状態クラスが持っていたBitの操作をしている部分を抜き出して

BitFlagを操作するだけのモノを作りました。


BitFlag(スイッチ)クラス

// BitFlagクラス

class BitFlag
{
private:
char mFlag;

public:
// フラグリセット
void reset() {
mFlag = 0; // 0にするだけ
}

// BITフラグON
void on(char flag)
{
mFlag |= flag; // 論理和をとればいいね。
}

// BITフラグOFF
void off(char flag)
{
mFlag &= ~flag; // 反転させたBITと論理積とればいいね。
}

// 指定されたフラグと一致する?
bool is(char flag)
{
return (mFlag == flag); // 完全一致なら比較するだけだね。
}

// 指定されたフラグのいずれかが一致する?
bool either(char flag) {
return (mFlag & flag) != 0; // 論理積の結果が0じゃないときだね
}
};


状態クラスの方は今まで行っていたBITの直接操作がなくなりました。


状態クラス

class State 

{
private:
BitFlag mFlag;

public:
// コンストラクタ
State() :mFlag() {}

// 状態の種類(ビットシフトを使うとわかりやすいね)
struct Kind {
enum {
Palsy = 1 << 0, // 00000001 麻痺
Poison = 1 << 1, // 00000010 毒
Sleep = 1 << 2, // 00000100 睡眠
Anger = 1 << 3, // 00001000 怒り
Sad = 1 << 4, // 00010000 悲しみ
All = 0xFF, // 11111111 全て
};
};

// ここから色んなメソッドを作っていくよ。
void reset() { mFlag.off(Kind::All); } // 状態をリセット(健康にする)
void on(char flag) { mFlag.on(flag); } // 指定された状態をON(有効)にする
void off(char flag) { mFlag.off(flag); } // 指定された状態をOFF(無効)にする
bool is(char flag) { return mFlag.is(flag); } // 聞かれた状態と一致する?
bool either(char flag) { return mFlag.either(flag); } // 聞かれた状態のいずれかが該当する?
bool isHealthy() { return mFlag.is(0); } // 健康です。(悪いとこない)
};


こうする事でスイッチ(BITフラグ関連の処理)と状態が分離されます。

スイッチは状態以外でも使えるモノになりました。


まとめ

ちょっと長くなりすぎました。

BITフラグを例にしてしまったのは失敗だったかも。

とはいえ、始まりはいくつかのbool変数しかなかったものが

最終的にここまで肥大化したわけです。

おそるべしオブジェクト思考、おそるべしプログラム。

しかしこれがプログラムの面白さであり、醍醐味であるとも思います。

1. 複数のフラグで状態管理する。

2. いろいろ管理がめんどくさいぞ!

3. 状態管理はBITフラグ使うとスマートに書けるじゃないの

4. 状態管理とBITフラグってそれぞれ別で考えられるよね

5. 状態管理するモノとBITを管理するモノを分けよう!

これまた状態管理するモノとBITを管理するモノってのが

オブジェクトなの?って感じですよね。

でもこれもオブジェクト思考。

お粗末様でした。