地味に便利な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次元配列を便利に扱えるクラスの紹介でした。

なお、今回紹介したコードは自由に使っていただいて構いませんが、運用した結果については保証いたしませんので、各自の責任で利用をお願いします。

Follow me!