はじめに
免責事項
記事中のコードはc++を基本として書いていますが
あくまで考え方を示すサンプル的な立ち位置です。
動作確認はしていないので、コードがおかしいところは脳内補間していただければと思います。
前に書いた記事
【ゲームで学ぶオブジェクト思考】HP(ヒットポイント)をどう作る?
よければこちらもご覧ください。
状態とは
こんな感じのやつについてです。
毒ってると体力が徐々に減ったり、眠ってると行動とれなくなったり
というのを判断するために状態について考えていこう。
シンプルに作ってみる
ゲーム作りはじめのころはこんな感じで状態を作っていました。
各種状態のフラグを用意
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ですが、パソコンの内部ではどうなってるでしょうか?
はい、こうなってます。
パソコンは内部ではデータを0と1だけで表現しています。
つまるところ、char型の変数というのは0か1を入れられる箱が8個あるのです。
これってbool型のフラグが8個あるのと同じですよねっていう。
この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 全て
};
};
列挙型に追加するだけで済むね。
いい感じ?
だいぶ長くなりましたが、使いやすい状態クラスが出来てきました。
これにて完成!
としたいところですが、ただ状態をクラスにしただけで
オブジェクト思考的としてはまだ考える余地があります。
状態クラスをよく見てみよう
状態クラスはこんな感じです。
状態の種類はこんなのがあるよーっていう部分と
状態を覚えさせるためのスイッチ(フラグ)部分です。
正直このスイッチ部分って他でも使えると思いませんか?
状態とスイッチを分離しよう。
いままで状態クラスが持っていたBitの操作をしている部分を抜き出して
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を管理するモノってのが
オブジェクトなの?って感じですよね。
でもこれもオブジェクト思考。
お粗末様でした。