こんにちは。プログラマーの尾関です。
今回は2次元配列を管理する便利なクラスを紹介します。
2次元配列は昔ながらのゲームを作る時、よく使われます。
例えば、パズルゲームの盤面やダンジョンRPGのマップなどです。
このように、オブジェクトの情報がグリッド(格子状)に配置されるゲームの場合、情報を2次元配列にすると管理しやすいです。ただ、通常はこれを直接参照するのは危険なので、領域外アクセスのチェック処理を挟みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// マップ情報から値の取得 int Map::Get(int x, int y) { if(x < 0 || mWidth <= x) { // 領域外 return -1; } if(y < 0 || mHeight <= y) { // 領域外 return -1; } // 値を取得 return mData[x][y]; } |
とはいえ、2次元配列を定義するたびに、このようなチェック処理を毎回書くのは面倒なので、私はArray2Dという自作の便利クラスを使っています。
■Array2D.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
// Array2D.h #ifndef ___ARRAY2D_H__ #define ___ARRAY2D_H__ #include <stdio.h> #include <string.h> /** * 2次元配列管理クラス */ class Array2D { public: // ForEach関数用の関数ポインタ (X座標, Y座標, 値) typedef void (*FP_FOREACH)(int x, int y, int v); // ForEach2関数用の関数ポインタ (インデックス座標, 値) typedef void (*FP_FOREACH2)(int idx, int v); // 領域外を参照したときの値 (SetOutOfRangeで変更可能) enum { OUT_OF_RANGE = -1 }; // コンストラクタ Array2D(int w=0, int h=0) { mWidth = 0; mHeight = 0; mpData = NULL; mOutOfRange = OUT_OF_RANGE; if(w > 0 && h > 0) { Create(w, h); } } // コピーするコンストラクタ Array2D(Array2D* pSrc) { mWidth = 0; mHeight = 0; mpData = NULL; mOutOfRange = OUT_OF_RANGE; Copy(pSrc); } // デストラクタ ~Array2D() { Destroy(); } // 生成 void Create(int w, int h) { Destroy(); mWidth = w; mHeight = h; mOutOfRange = OUT_OF_RANGE; mpData = new int[Length()]; // 0で初期化 memset(mpData, 0, sizeof(int) * Length()); } // 破棄 void Destroy() { if(mpData) { delete[] mpData; mpData = NULL; } mWidth = 0; mHeight = 0; } // ■プロパティ int Width() { return mWidth; } int Height() { return mHeight; } int Length() { return mWidth * mHeight; } // ■値の取得・操作 // コピー void Copy(Array2D* pSrc); // 指定の2次元配列にコピーする void CopyTo(Array2D* pDst); // 値の取得 int Get(int x, int y); int GetFromIdx(int idx); // 値の設定 bool Set(int x, int y, int v); bool SetFromIdx(int idx, int v); // 有効な座標かどうかをチェックする bool Check(int x, int y); bool CheckIdx(int Idx); // 領域外参照時の値を変更 void SetOutOfRange(int v); // ■座標変換系 // 一次元のインデックス(x + y*Width)に変換する int ToIdx(int x, int y); // 一次元のインデックスをX座標に変換する int IdxToX(int idx); // 一次元のインデックスをY座標に変換する int IdxToY(int idx); // ■検索系 // 指定の値が最初に存在する座標(インデックス)を返す (見つからなかったら「-1」) int Search(int v); // 指定の値の数をカウントする int Count(int v); // 指定の値ですべて埋める void Fill(int v); // 値を入れ替える void Swap(int x1, int y1, int x2, int y2); // 関数ポインタで全要素を走査します(XY座標) void ForEach(FP_FOREACH fp); // 関数ポインタで全要素を走査します(インデックス版) void ForEach2(FP_FOREACH2 fp); // ■デバッグ // デバッグ出力する void Dump(); private: int* mpData; // 配列データ int mWidth; // 幅 int mHeight; // 高さ int mOutOfRange; // 領域外を参照したときに返す値 }; #endif // #ifndef ___ARRAY2D_H__ |
■Array2D.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
// Array2D.cpp #include "Array2D.h" // コピー void Array2D::Copy(Array2D* pSrc) { const int w = pSrc->Width(); const int h = pSrc->Height(); if(mpData == NULL) { // バッファがないので作る Create(w, h); } else if(Width() != w || Height() != h) { // 作り直し Create(w, h); } // コピー memcpy(mpData, pSrc->mpData, (w*h)*sizeof(int)); } // 指定の2次元配列にコピーする void Array2D::CopyTo(Array2D* pDst) { pDst->Copy(this); } // 値の取得 int Array2D::Get(int x, int y) { if(Check(x, y) == false) { // 領域外 return mOutOfRange; } int idx = ToIdx(x, y); return mpData[idx]; } // 値の取得(Index指定) int Array2D::GetFromIdx(int idx) { if(CheckIdx(idx) == false) { // 領域外 return mOutOfRange; } return mpData[idx]; } // 値の設定 bool Array2D::Set(int x, int y, int v) { if(Check(x, y) == false) { // 領域外なので設定できない return false; } int idx = ToIdx(x, y); mpData[idx] = v; return true; } // 値の設定(Index指定) bool Array2D::SetFromIdx(int idx, int v) { if(CheckIdx(idx) == false) { // 領域外なので設定できない return false; } mpData[idx] = v; return true; } // 有効な座標かどうかをチェックする bool Array2D::Check(int x, int y) { if(x < 0 || mWidth <= x) { // 領域外 return false; } if(y < 0 || mHeight <= y) { // 領域外 return false; } // 領域内 return true; } // 有効な座標かどうかをチェックする(Index指定) bool Array2D::CheckIdx(int Idx) { if(Idx < 0 || Length() <= Idx) { // 領域外 return false; } // 領域内 return true; } // 領域外参照時の値を変更 void Array2D::SetOutOfRange(int v) { mOutOfRange = v; } // 一次元のインデックス(x + y*Width)に変換する int Array2D::ToIdx(int x, int y) { return x + y*mWidth; } // 一次元のインデックスをX座標に変換する int Array2D::IdxToX(int idx) { return idx % mWidth; } // 一次元のインデックスをY座標に変換する int Array2D::IdxToY(int idx) { return idx / mWidth; } // 指定の値が最初に存在する座標(インデックス)を返す int Array2D::Search(int v) { for(int idx = 0; idx < Length(); idx++) { if(mpData[idx] == v) { // 見つかった return idx; } } // 見つからなかった return -1; } // 指定の値の数をカウントする int Array2D::Count(int v) { int count = 0; for(int idx = 0; idx < Length(); idx++) { if(mpData[idx] == v) { // 見つかった count++; } } return count; } // 指定の値ですべて埋める void Array2D::Fill(int v) { int length = Length(); for(int idx = 0; idx < length; idx++) { mpData[idx] = v; } } // 値を入れ替える void Array2D::Swap(int x1, int y1, int x2, int y2) { if(Check(x1, y1) == false) { return; } if(Check(x2, y2) == false) { return; } int a = Get(x1, y1); int b = Get(x2, y2); Set(x1, y1, b); Set(x2, y2, a); } // 関数ポインタで全要素を走査します void Array2D::ForEach(FP_FOREACH fp) { for(int j = 0; j < mHeight; j++) { for(int i = 0; i < mWidth; i++) { int idx = ToIdx(i, j); fp(i, j, mpData[idx]); } } } // 関数ポインタで全要素を走査します void Array2D::ForEach2(FP_FOREACH2 fp) { int length = mWidth * mHeight; for(int idx = 0; idx < length; idx++) { fp(idx, mpData[idx]); } } // デバッグ出力する void Array2D::Dump() { printf("<Array2D> (w,h)=(%d,%d)\n", mWidth, mHeight); for(int j = 0; j < mHeight; j++) { for(int i = 0; i < mWidth; i++) { if(i == 0) { printf("%d", Get(i, j)); } else { printf(",%d", Get(i, j)); } } printf("\n"); } } |
もともとこのクラスは私が趣味でゲームを作るときに使っていたものですが、社内でパズルゲームを作るときに「これを使うと便利だよ」とこのコードを渡したら、意外に評判が良かったので、それなりに使いやすいのでは……、と思っています。
使い方の例は以下のとおりです。
1 2 3 4 5 6 |
// 4x4の2次元配列を作成 Array2D array(4, 4); array.Set(0, 1, 3); // (0, 1)に 3 を設定 array.Set(2, 2, 5); // (2, 2)に 5 を設定 array.Set(3, 0, 7); // (3, 0)に 7 を設定 array.Set(1, 3, 9); // (1, 3)に 9 を設定 |
Array2D::Set() 関数では、XY座標を指定し、3番目の引数に設定する値を指定します。
値の取得は Array2D::Get() 関数を使用します。
設定されている値をデバッグ出力して確認する場合は、Array2D.Dump() を使います。
■出力結果
1 2 3 4 5 |
(w,h)=(4,4) 0,0,0,7 3,0,0,0 0,0,5,0 0,9,0,0 |
視覚的にわかりやすいので、この出力機能もなかなか便利だと思います(自画自賛)
あと、すべての値に対して何らかの処理を行う場合には、Array2D::ForEach() を使うと、関数ポインタを渡してまとめて処理をすることもできます。
C++11が使える環境であれば、ラムダ式を渡せます。
1 2 3 4 5 6 |
array.ForEach([](int x, int y, int v) { if(v > 0) { // 0より大きい値が設定されている座標を出力 printf("(%d,%d) = %d\n", x, y, v); } }); |
forループを書く必要がないので、簡潔な記述になりますね。
また、このクラスでは、XY座標系とは別にインデックス座標系を用意しています。
インデックス座標系とは、私の造語ですが、通し番号で各要素にアクセスするためのものです。
このインデックス座標系は一見、不要に思えるのですが(XY座標系の方が直感的なため)、実際に配置するオブジェクト情報を1次元配列で管理する際に、相互に連携するためのキーとして使うことがあります。
例えば、マップの配置情報はこのArray2Dで管理し、実際に配置するマップチップモデルは1次元配列で管理する、などです。
他にもゲームで使うと便利な関数も用意しています。
- Array2D::Count(int v) : vに一致する値の存在数をカウントする → 配置したコインをすべて回収したかどうかの判定など
- Array2D::Fill(int v) : vに指定した値をすべての要素に設定する → 0以外で初期化したい場合に使う
- Array2D::Swap(int x1, int y1, int x2, int y2) : 値を交換する → 「パネルでポン」のようなブロックを交換するゲームで使う
ということで、2次元配列を便利に扱えるクラスの紹介でした。
なお、今回紹介したコードは自由に使っていただいて構いませんが、運用した結果については保証いたしませんので、各自の責任で利用をお願いします。