C++
オブジェクト指向
プログラミング教育
ゲームプログラミング

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

はじめに

免責事項

記事中のコードは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を管理するモノってのが
オブジェクトなの?って感じですよね。
でもこれもオブジェクト思考。

お粗末様でした。