地味に便利な2次元配列管理クラス
こんにちは。プログラマーの尾関です。
今回は2次元配列を管理する便利なクラスを紹介します。
2次元配列は昔ながらのゲームを作る時、よく使われます。
例えば、パズルゲームの盤面やダンジョンRPGのマップなどです。
このように、オブジェクトの情報がグリッド(格子状)に配置されるゲームの場合、情報を2次元配列にすると管理しやすいです。ただ、通常はこれを直接参照するのは危険なので、領域外アクセスのチェック処理を挟みます。
// マップ情報から値の取得
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
#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
#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");
}
}
もともとこのクラスは私が趣味でゲームを作るときに使っていたものですが、社内でパズルゲームを作るときに「これを使うと便利だよ」とこのコードを渡したら、意外に評判が良かったので、それなりに使いやすいのでは……、と思っています。
使い方の例は以下のとおりです。
// 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() を使います。
■出力結果
(w,h)=(4,4)
0,0,0,7
3,0,0,0
0,0,5,0
0,9,0,0
視覚的にわかりやすいので、この出力機能もなかなか便利だと思います(自画自賛)
あと、すべての値に対して何らかの処理を行う場合には、Array2D::ForEach() を使うと、関数ポインタを渡してまとめて処理をすることもできます。
C++11が使える環境であれば、ラムダ式を渡せます。
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次元配列を便利に扱えるクラスの紹介でした。
なお、今回紹介したコードは自由に使っていただいて構いませんが、運用した結果については保証いたしませんので、各自の責任で利用をお願いします。