LoginSignup
4

More than 3 years have passed since last update.

列挙体活用例

Posted at

はじめに

この記事は筆者が所属するサークルCCSアドベントカレンダー4日目として書かれたものです。

前日の記事はこちらです。

ゲーム制作におけるC, C++の列挙体の使用例をいくつか挙げてみました。
ライブラリとしてDxlibを使用していますが、それ以外のライブラリでも活用できるものも紹介しています。

目次

0 . 列挙体とは
1 . 画像ハンドル(C, Dxlib)
2 . シーン遷移(C)
3 . シーン遷移(C++)
4 . 順位付け
5 . 描画順位(C++)
6 . おわりに

0.列挙体とは

列挙体は以下のように定義します。

***.c
typedef enum {
    A,
    B,
    C,
    ...
} hoge;

***.cpp
enum hoge {
    A,
    B,
    C,
    ...
};

この時、上から順番に0, 1, ...と整数値が割り当てられるものを列挙体といいます。
これらは勝手にconst intだと思ってますが違ったらすみません。
列挙定数と呼ばれる A, B, ... やhogeは変数名や型名と同じで自由に変更できます。

また、hogeは型名として働き、

hoge a = A;

のようにhoge内で定義された整数値のみを持つように明示することができます。

(enumで定義された型からint型への変換は暗黙に定義されたものらしい)

1. 画像ハンドル(Dxlib)

ここではDxlibで使われるハンドルの扱いについてです。
画像を扱う際

int handle;
handle = LoadGraph("Glaph1.png");

のようにしてint型の変数にハンドルを格納してきました。

多くの画像を扱うようになると変数を配列化して

int handle[10];
handle[0] = LoadGraph("Glaph1.png");
...

としてきたと思います。

しかしこれだと後になってどうしても間に画像を挿入したいときにそれ以降の値をすべて書き換える必要があり、とても面倒な上ミスも起こりやすくなってしまいます。

ここで列挙体の登場です。

下のように定義します。

typedef enum {
    GRHANDLE_GLAPH1,
    GRHANDLE_GLAPH2,
    /* ..., */
    GRHANDLE_SIZE
} GrHandleName;

ここで肝となるのが最後の要素[GRHANDLE_SIZE]です。
復習すると、列挙体は上の要素から0, 1, ... のように変更不可な整数が順番に割り当てられます。
変数名からも察せるとおり[GRHANDLE_SIZE]に割り当てられる整数はその前にある[GRHANDLE_GLAPH1]等の数、つまりハンドルの格納に利用するint型の変数の配列の大きさになります。
これを用いて配列を宣言すればぴったりサイズの配列が宣言できるわけです。

文章で見てもよくわからない人もいると思うのでコードにすると

typedef enum {
    GRHANDLE_PLAYER,
    GRHANDLE_ENEMY1,
    /* ..., */
    GRHANDLE_SIZE
} GrHandleName;

int handle[GRHANDLE_SIZE];

void LoadGrData() {
    handle[GRHANDLE_PLAYER] = LoadGraph("Player.png");
    ...
}

とできるということです。

呼び出しの際も

DrawGlaph(*, *, handle[GRHANDLE_PLAYER], *);

とすればもし Player.png の前に画像を読み込みたいときに列挙体の中で[GRHANDLE_PLAYER]の前に挿入するだけでいいのでとても楽にできます。

また、Dxlibには画像を分割して読み込むLoadDivGlaph関数があります。
これのハンドルには画像の分割総数分のint型の連続したメモリ領域が必要になります。
別途変数を用意したり、ハンドルをint*型の配列にしたりといろいろ解決方法はありますが、列挙体の性質を利用してこれまでとほとんど変わらない形で解決できます。

列挙体は

typedef enum {
    A,
    B,
    C = 10,
    D
} hoge;

としたとき,

A : 0
B : 1
C : 10
D : 11

というように、[=]で値が指定されるまでは0から順番に、そのあとは指定された値から順番に値が入っていく機能があります。

これを利用すると、

typedef enum {
    GRHANDLE_PLAYER,
    GRHANDLE_ENEMY, //ここから[SIZE]分連続領域を確保する
    GRHANDLE_ENEMY_END = GRHANDLE_ENEMY + [SIZE] - 1,
    /* ..., */
    GRHANDLE_SIZE
} GrHandleName;

とでき、無事[GRHANDLE_ENEMY]から連続領域が確保できました。
[SIZE]に関しては別途 #difine か const int などで決めるか直接値を書き込むしか思いつかず恐縮ですが、エフェクト等のそうそうサイズを変更しないものに利用していただければと思います。

下に最終形をまとめておきます。

typedef enum {
    GRHANDLE_PLAYER,
    GRHANDLE_ENEMY1,
    /* ..., */
    GRHANDLE_BOMB,
    GRHANDLE_BOMBEFFECT,
    GRHANDLE_BOMBEFFECT_END = GRHANDLE_BOMBEFFECT + 32 - 1,
    /* ..., */
    GRHANDLE_SIZE
} GrHandleName;

int handle[GRHANDLE_SIZE];

void LoadGrData() {
    handle[GRHANDLE_PLAYER] = LoadGraph("Player.png");
    /* ... */
    LoadDivGraph("BombEffect.png", 32, 8, 4, 32, 32, &(handle[GRHANDLE_BOMBEFFECT]));
    /* ... */
} //メインループ前にこれを実行

2. シーン遷移(C)

よく使う方法だとは思いますが紹介しておきます。
まず、列挙体を

***.c
typedef enum {
    GAMEMODE_TITLE,
    GAMEMODE_GAME
} GameMode;

と定義します。

次に

GameMode gamemode = GAMEMODE_TITLE;

をメインループ前で定義、初期化します。
そしてメインループでは、

while(true){
    switch(gamemode){
    case GAMEMODE_TITLE:
        //タイトル画面での処理
        //gamemodeの値変更でシーン遷移
        break;
    case GAMEMODE_GAME;
        //ゲーム画面での処理
        //gamemodeの値変更でシーン遷移
        break;
    }
}

のようにしてシーン遷移が行えます。
画像ハンドルの扱いと同様、途中にシーンを追加挿入してもgamemodeの変更を[GAMEMODE_TITLE]等で行えば列挙体に追加し、対応するcase文を追加するだけで追加可能です。

関数ポインタを利用したシーン遷移もあります。

列挙体を

typedef enum {
    GAMEMODE_TITLE,
    GAMEMODE_GAME,
    GAMEMODE_SIZE
} GameMode;

に変更し、メインループ前に

GameMode (*run[GAMEMODE_SIZE])();

を追加します。

各シーンごとに

GameMode title();

のような関数を作り、そのポインタを対応する配列の要素に追加します。

run[GAMEMODE_TITLE] = title;

こうすることによってメインループが

while(gamemode != GAMEMODE_SIZE){
    gamemode = (*run[gamemode])();
}

とでき、スッキリしました。
switch文では終了処理を別途書く必要がありましたが、この場合はGAMEMODE_SIZEを返してくればよいのでシーンごとの変更が楽です。
(Escキーを押したときの終了処理など、どのシーンでも変わらない処理はメインループ内に直接書いちゃった方が楽だったりはしますが)

下に最終形をまとめておきます

switch.c
typedef enum {
    GAMEMODE_TITLE,
    GAMEMODE_GAME /*,
    ... */
} GameMode;

int main() {
    GameMode gamemode = GAMEMODE_TITLE;
    int flag = false;

    while(true){
        switch(gamemode) {
        case GAMEMODE_TITLE:
            //タイトル画面での処理
            break;
        case GAMEMODE_GAME:
            //ゲーム画面での処理
            break;
        /* ... */
        }
        if(flag) break;
    }

    return 0;
}
FunctionPointer.c
typedef enum {
    GAMEMODE_TITLE,
    GAMEMODE_GAME,
    /* ..., */
    GAMEMODE_SIZE
} GameMode;

GameMode title(); //これらに具体的な処理を書く
GameMode game();
/* ... */

int main() {
    GameMode gamemode = GAMEMODE_TITLE;

    GameMode (*run[GAMEMODE_SIZE])();

    run[GAMEMODE_TITLE] = title;
    run[GAMEMODE_GAME] = game;
    /* ... */

    while(gamemode != GAMEMODE_SIZE) {
        gamemode = (*run[gamemode])();
    }

    return 0;
}

3. シーン遷移(C++)

前節で関数ポインタを利用した話をしたときに察した人もいると思いますが、クラスを利用してシーン遷移させることもできます。

まず列挙体ですが、ここではC++を使うので少し簡単な書き方をして

***.cpp
enum GameMode {
    GAMEMODE_TITLE,
    GAMEMODE_GAME,
    /* ..., */
    GAMEMODE_SIZE
};

と定義します。

次に、複数の異なったクラスを一つのポインタの配列であらわしたいので基底クラスを作り、今後これを継承していくことにします。

class Manager {
public:
    Manager() {}
    virtual ~Manager() = default;
    virtual GameMode run() = 0;
};

実際の処理を書くクラスはこれを継承して

class Title : public Manager {
public:
    Title() :
        Manager()
    {}
    virtual ~Title() = default;
    virtual GameMode run(); //ここに実際の処理を書く
};

のようにします。

メインループ前で

Manager *manager[GAMEMODE_SIZE];

として基底クラスのポインタの配列を確保し、

manager[GAMEMODE_TITLE] = new Title();
manager[GAMEMODE_GAME] = new Game();
...

として派生クラスのインスタンスをnewで生成し、そのポインタを対応するところに代入していきます。

前節と同様に

GameMode gamemode = GAMEMODE_TITLE;

としてgamemodeを定義、初期化しておきます。

メインループ内では

while(gamemode != GAMEMODE_SIZE) {
    gamemode = manager[gamemode]->run();
}

とでき、非常にスッキリした形になりました。

通常各シーンごとにインスタンスは一つなのでシングルトンを利用したほうがいい気もしますが今回はパス

しかし、このままでは不十分です。

例えばクラス間の値の共有をどうするかという問題です。
関数ポインタを利用した際はグローバル変数化するか、引数に構造体のポインタを指定してその構造体に引き継ぐ値を保存するくらいしか解決策がありませんでした。(筆者が知らないだけかもしれませんが)
もちろんクラスでも上のような方法で解決はできますが、せっかく高級アセンブラから脱却したのでもう少し方法を考えたいものです。

解決方法の一つとして基底クラス(今回はManagerクラス)に値をコピーする関数(今回はinit関数)を作ってそこの中で値の引継ぎを行う方法です。

class Manager {
    static Manager **manager;
    virtual void init() = 0; //シーンを移るときに実行する関数
};

class Title : public Manager {
public:
    int difficult;
};

class Game : public Manager {
public:
    virtual void init() override {
        difficult = manager[GAMEMODE_TITLE]->difficult; //Titleクラスのdifficultをコピー
    } //Titleのrun関数内でシーン遷移時に実行
    int difficult;
};

これでとりあえず引継ぎができました。
しかしこれではシーン遷移時に莫大な量の変数の引継ぎをしたい際にすべてコピーしているので莫大な時間がかかってしまいます。
さらに逆方向のシーン遷移時に値を渡すことができないことも大きな欠点です。(ポインタをコピーすればできることはできますが)

これらを解決するものといえば参照型です。

参照型は初期化時に対応する変数を決めてあげなければいけないので上と全く同じ処理はできません。
しかしこれを利用して各シーンのインスタンス生成時に紐づけを行うことができるので遷移時の負荷を軽減できます。

文章で見てもよくわからないと思うのでコードを見てください。

class Title : public Manager {
public:
    int difficulty;
}

class Game : public Manager {
public:
    Game(Title* title) :
        Manager(),
        difficulty(title->difficulty) //参照型を初期化
    {}

    int &difficulty; //参照型

    //Gameクラスからは値を変更したくないときはconstを付けて
    // const int &difficulty;
    //とできる
}

int main(void) {
    Manager *manager[GAMEMODE_SIZE];
    Title *title = new Title();
    manager[GAMEMODE_TITLE] = title;
    Game *game = new Game(title);
    manager[GAMEMODE_GAME] = game;

    ...
}

以降引き継ぎたいものができた場合、どちらかのクラスでpublicのメンバ変数として定義し、もう一つの方で参照型の変数を用意して初期化子に追加すればいいのです。

この方法の注意点として、参照型の紐付け元の変数がメモリ上不変である必要がある点です。

参照型は初期化時に元の変数と同じポインタをもつだけの変数です。(たぶん)
例えば元の変数がvector等の要素の一部でメモリ再確保などによって元の変数のポインタが変わってしまっても参照型の変数のポインタは変わりません。

今回のように参照型の指す先がメインループ前にnewで確保され、メインループの後に一括してdeleteするようなものの場合は問題ないのですが、そうでない場合は注意が必要です。
やっぱりシングルトンにすべきでは……??
シーン遷移を管理するクラスも作成するべきだった気もする

今回紹介した感じだと常にすべてのシーンのインスタンスが存在しているのでスタック構造的に作れたらなーとも思ったり、このままの方がいいのかなーとも思ったりしてます。(アイデアご存知の方教えて下さい)

4. 順位付け

さて、やや脱線しましたが話をちょっともどします。

列挙体の特徴は何といっても列挙定数に割り当てられる値をコンパイル時に勝手に順番通りにしてくれるところです。
この特徴を利用すれば順位付けに便利に使えることがわかります。

順位付けにint型を利用してしまった場合、もし4番目のものと5番目のものの間に新しく追加したくなったらどうするか考えてみてください。
新しいものを5番とし、それ以降をひとつづつ手作業でずらしていきますか??
それともfloat型などを用いて4.5としますか??
今後そうならないためにわざと4の次は7のように間をあけておきますか??

わかりにくいコードになってしまいますし困ったときの作業量が膨大になってしまうことは容易に想像がつくと思います。

そこで列挙体の登場です。
順位付けにenumで定義された型の定数を持つようにして対応する列挙定数を持たせ、それに応じて順位管理をしてあげればいいのです。

上の例でいくと、もし4に対応するAと5に対応するBの間にCをはさみたかったら、列挙体の定義の部分に挿入するだけでCの値は5になり、B以降も自動的にひとつづつずれたものが入ります。
呼び出す側も列挙定数を使っていれば何ら問題は起きません。

さらに呼び出すものを配列やそれに準ずるもので管理していた場合、画像ハンドルの節でやった通り配列大きさを列挙定数の最後に一つ追加するだけで簡単にぴったりサイズのものが定義できてしまいます。

実は前の順位のもの+1のように順位付けしてあげればenumなんか必要ないんですよね……
全体像見えやすいし順番入れ替えも比較的楽だから使う場面によって使い分けられたらいいかも

5. 描画順位(C++)

前節で紹介した順位付けの具体例としてやってみました。

説明書く気力がなかったのでコードぶん投げておきます。(聞いていただければ答えます。)

enum GameMode {
    GAMEMODE_GAME,
    GAMEMODE_SIZE
};

enum DrawLevel {
    DRAWLEVEL_BACK,
    DRAWLEVEL_ENEMY,
    DRAWLEVEL_PLAYER,
    DRAWLEVEL_SIZE
};

class Base {
public:
    Base(DrawLevel drawlevel) :
        drawlevel(drawlevel),
        next(NULL)
    {
    };
    virtual void draw() = 0;
    ~Base() = default;
    const DrawLevel drawlevel;

    Base *prev;
    Base *next;
};

class Manager : public Base {
public:
    Manager() :
        Base(DRAWLEVEL_BACK)
    {
    }
    virtual GameMode run() = 0;
    virtual ~Manager() = default;
    virtual void draw() override = 0;
protected:
    virtual void drawmanager() = 0;
};

class Enemy : public Base {
public:
    Enemy() :
        Base(DRAWLEVEL_ENEMY)
    {
    }
    virtual ~Enemy() = default;
    virtual void draw() override;
    virtual bool judge(); //死亡判定
};

class Player : public Base {
public:
    Player() :
        Base(DRAWLEVEL_PLAYER)
    {
    }
    virtual void draw() override;
    virtual ~Player() = default;
};

class Game : public Manager {
public:
    Game() :
        Manager(),
        player(new Player())
    {
        for(int i = 0; i < DRAWLEVEL_SIZE; i++) {
            drawlist[i] = NULL;
            drawlevelorder[i] = DrawLevel(i);
            drawlistend[i] = NULL;
        }
        addDrawList(player);
        addDrawList(this);
    }
    virtual GameMode run() override { 
        drawmanager();
        return GAMEMODE_GAME;
    }
    virtual void draw() override {cout << "Game" << endl;}
    virtual ~Game() = default;

protected:
    std::vector<Enemy*> enemy;
    Player *player;
    Base *drawlist[DRAWLEVEL_SIZE];
    Base *drawlistend[DRAWLEVEL_SIZE];
    virtual void drawmanager() override { /////////////
        for(int i = 0; i < DRAWLEVEL_SIZE; i++){
            for(Base *now = drawlist[drawlevelorder[i]]; now != NULL; now = now->next) {
                now->draw();
            }
        }
    }

    void addenemy(Enemy *nEne) {
        addDrawList(nEne);
        enemy.emplace_back(nEne);
    }
    void rmenemy() {
        for(auto itr = enemy.begin(); itr != enemy.end();){
            if((*itr)->judge()){
                rmDrawList(*itr);
                delete (*itr);
                itr = enemy.erase(itr);
            }
            else itr++;
        }
    }

    void addDrawList(Base *np) {
        np->prev = drawlistend[np->drawlevel];
        if(drawlistend[np->drawlevel] == NULL) {
            drawlist[np->drawlevel] = np;
        }
        else {
            drawlistend[np->drawlevel]->next = np;
        }
        drawlistend[np->drawlevel] = np;
    }

    void rmDrawList(Base *rp) {
        if(rp->prev != NULL) (rp->prev)->next = rp->next;
        else drawlist[rp->drawlevel] = rp->next;
        if(rp->next != NULL) (rp->next)->prev = rp->prev;
        else drawlistend[rp->drawlevel] = rp->prev;
    }

    DrawLevel drawlevelorder[DRAWLEVEL_SIZE]; //この中身をswapしてあげるとゲーム内で描画順位を変更できます。
};


少々省いたところもありますがこんな感じに実装してみました。
Baseクラスのポインタ型で双方向連結リスト構造を作成し、配列でそれらを管理しています。(コンテナ使ってできたかも……??)
前節のシーン遷移も部分的に取り入れて書いています。

実際に形にしてみるとなかなかゴツイ感じになってしまいました……(汗)

ちなみに、Gameクラスのメンバ変数であるdrawevelorder[DRAWLEVELSIZE]の要素の内二つをとりだして中身を入れ替えてあげると実行中であってもそこの描画順位をいれかえることができるようになっています。
エフェクトに力を入れたので見てほしいけど若干見づらくなってしまう時などにデフォではエフェクトの描画順位を最後にして迫力ある映像を見てもらい、見やすくしたいけど完全には消したくないユーザーには設定で変えてもらうなんてことができます。
見づらくなるエフェクトなんて、お前らエフェクトじゃねぇ!!!!!(cv.う〇だゆうじ)

わざわざこうするほどの理由はあんまり見いだせなかったとしても何となく気持ちはわかっていただけたのではないでしょうか。

6. おわりに

列挙体の使用例いかがだったでしょうか。
途中から話が若干逸れた感じもしますがまぁいいんじゃないでしょうかねー(適当)

名前の衝突が怖い時はC++にはenum classというものが用意されているのでそちらも参考にしてみるといいと思います。
(暗黙の型変換が行われないのでint型を定義部分に書くか毎度キャストしてあげる必要はありますが)
enumの定義をクラスのメンバにしちゃうこともできるらしい

C, C++力はまだまだなのでもっといいコードもあるかとは思いますが、温かい目で見ていただけると幸いです。

アドベントカレンダー次の人は@starealnightさんです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4