タイルのスライド処理

画面ができたのでついに、タイルをスライドさせてみる。
コンソール版を作ってみてわかったと思うが、このゲームのかなめになる処理である。
現在のゲームの製作状況は、

  • ゲームの基本的な状態遷移と各Draw,Update関数を制作
  • 盤面を表す構造体Boardを定義
  • Boardの中にタイルを表す配列を設置
  • Boardの初期化関数InitBoardを作成
  • プレイ画面での現在の盤面の表示処理

までできている(気がする)
ここで、「盤面のタイル番号は、実はコンソール版のデータを画面表示しているだけである」点に注意すると、
問題作成の関数MakeProblemや、SwapTile、CanMoveIt、SearchTileNumはコンソール版のままそのまま使える気がする。
全体のゲーム関連の初期化を行う関数をここで作っておこう。そこでデータの初期化や、問題作成をしてからゲームに突入することにする。

Listing. 1: InitGame関数の作成
//プロトタイプ宣言(適切なところに書いてね)
void InitGame(Board& _board);

//初期化関数 こっちはプロトタイプ宣言してあれば書く位置はどこでもいいよ
void InitGame(Board& _board)
{
	Window::Resize(WSIZE); //ここに移動させる
	state = GAME_STATE::TITLE;
	InitBoard(_board);//ここに移動させる
}

方針を立てるためにちょっといたずら

ここで、今きれいに並んでいるタイルを2つ選んで適当に入れ替えし表示してみよう。

myboard.tile[2][2] ← 11が入ってる
myboard.tile[3][3] ← 16が入ってる(つまりBLANK_POS)

これを入れ替えてみよう
Main()の中で

void Main()
{
省略
	//Window::Resize(WSIZE);->InitGameへ
 
	Board myboard;
	InitGame(myboard);
        //無理やり11と16を入れ替える
	std::swap(myboard.tile[2][2], myboard.tile[3][3]);
 
	while (System::Update())
	{
		switch (state)
Fig. 1: 数字の入れ替え

入れ替えできたよね。つまり、あとは裏で、コンソール版のプログラムを動かしながら、画面表示を頑張ればよさそう。
確認出来たら、swapは必要ないのでコメントするか消しておこう。

次に、画面上のパネルをクリックしたらその番号を取得する処理を考える。

  1. パネルをクリック
  2. クリックしたパネルの番号を取得
  3. パネルが交換可能かチェック
  4. 交換可能なら交換、そうでなければスルー

この処理をパズルが完成するまで繰り返すことでゲームを進行する。

siv3Dには、Rectの領域がクリックされたかどうかの判別関数が用意されている。

Rect rect{x,y,width};
があるとき
rect.leftClicked() 

を呼ぶと、そのフレームでクリックされたかどうかをtrue, falseで返す。

しかしながら、君たちはゲームエンジニア科の学生である。
このような用意された道具を使って、ゲームを作るのも大事だが、自分で道具を作れなければエンジニアにはなれない。

点と矩形の当たり判定

矩形領域がクリックされたかどうかという問題は、ある四角形の中に点が含まれるか含まれないかという判定、すなわち点と矩形の当たり判定である。

Fig. 2: 点と矩形の当たり判定

具体的には、クリックした座標が、図の点aの状態なのか点bの状態なのか判別し、aならtrue、bならfalseを返す関数を作ればよい。
条件として今回は、タイルをx軸、y軸に平行に並べてボードを構成している。矩形のエッジ(辺)は必ず軸に平行なので結構簡単な判定ができる。
これは当たり判定の超基本になる。
ここでPoint型は、Vec2の整数版である。コンソール版を作った時のPosition型に相当するので、そのまま置き換えてよい

条件を整理する

//矩形に関する変数
Point p{x, y};
Rect rec{ P, Width, Height};
//内外判定する点
Point cp{a, b}

が宣言されているとき

  • 矩形recの左側の辺のx座標 p.x
  • 矩形recの右側の辺のx座標 p.x + Width
  • 矩形recの上側の辺のy座標 p.y
  • 矩形recの下側の辺のy座標 p.y + Height
  • 内外判定する点 (a, b)

矩形の中にcp(a,b)が存在する条件は、

  • cpのx座標が、recの右側のエッジと、左側のエッジの間に存在する・・・①
  • cpのy座標が、recの上側のエッジと、下側のエッジの間に存在する・・・②
  • ①と②を同時に満たすなら、矩形recの中にcpがある。
Fig. 3: 判定条件の図

こんな関数を作ればよい。
関数は、Point型でクリック点を、Rect型でタイルを表す。すなわちこの2つを引数にとる。
戻り値はtrue, false 名前は、IsPointInRectとかにしよう。(別に好きな名前にしていいよ)

"点と矩形の当たり判定"
bool IsPointInRect(Point point, Rect rect)
{
	// 点が矩形の中にあるかチェック
	if (条件① かつ 条件②)
	{
		return true;
	}
	return false;
}

動作確認をしてみる

void PlayUpdate()内で、クリックされた矩形のインデックスを表示してみよう。

Listing. 2: クリックされた矩形のチェック
void PlayUpdate(Board& _board)
{
	if (MouseL.down()) {
		Point cp =カーソル位置の取得;//クリックしたカーソル位置
		for (int j = 0; j < BOARD_HEIGHT; j++)
		{
			for (int i = 0; i < BOARD_WIDTH; i++)
			{
				if (IsPointInRect(cp, _board.tileRec[j][i]))
					Print << cp << U":" << j << U", " << i;
			}
		}
	}
}
Fig. 4: 1~16を順番に押した例

押したボードの枠の場所(インデックスがi,jのもの)にはまっているタイルの番号を取得する。
つまり、ボードのtile[j][i]を参照すればよい。

Listing. 3: クリックされた矩形のチェック
void PlayUpdate(Board& _board)
{
	if (MouseL.down()) {
		Point cp =カーソル位置の取得;//クリックしたカーソル位置
		for (int j = 0; j < BOARD_HEIGHT; j++)
		{
			for (int i = 0; i < BOARD_WIDTH; i++)
			{
				if (IsPointInRect(cp, _board.tileRec[j][i]))
					Print << _board.tile[j][i] << U"is Clicked";
			}
		}
	}
}
Fig. 5: 1~16を順番に押した例

コンソール版の時は、以下のように、番号ベースで処理していた。

"指定した番号は動かせるタイルの番号ですか?"
bool CanMoveIt(int _num, Board& _board)
{
	Position blank = BLANK_POSのタイル位置を探す
	Position moveTile = _numのタイル位置を探す
        if(blank.x == -1 || moveTile.x == -1) return false;//エラーならfalse
	int dist = blank と moveTile のマンハッタン距離の2乗を計算;
	if (距離が1なら)
		return true;//空白の隣なので動かせる
	else
		return false;//空白の隣じゃなさそうだから動かせない
}

今回は、グラフィック化の副作用で、自分がクリックした場所のインデックスも番号も(i, j)は、初めから取得できる。
したがって、初めからPoint型で、チェックしたい番号のインデックス座標を引数として渡しすと処理がすっきりする。
関数を以下のように変更する。

"指定したインデックスのタイルは動かせるタイルですか?"
bool CanMoveIt(Point _p, Board& _board)
{
	Point blank = タイル番号がBLANK_POSのタイル位置を探す
	if(p.x == -1||blank.x) return false;//エラーならfalse
	int dist = blank と p のマンハッタン距離の2乗を計算;
	if (距離が1なら)
		return true;//空白の隣なので動かせる
	else
		return false;//空白の隣じゃなさそうだから動かせない
}
//SearchTileNumは、Position -> Pointにすることでほぼそのまま使える
//この順番で書くならプロトタイプ宣言を、ソースコードの冒頭のほうでしておく
Point SearchTileNum(int num, Board& _board)
{
    	for (int j = 0; j < BOARD_HEIGHT; j++)
	{
		for (int i = 0; i < BOARD_WIDTH; i++)
		{
			if (_board.tile[j][i] == num)
			{
				Point p = { i, j };
				return(p);
			}
		}
	}
	//ここまでたどり着いたら、変な番号入れてエラー対応 -> -1,-1はエラー判定用
	Point p = { -1,-1 };
	return(p);
}

動作確認

さっきと同じように、クリックしたときにPlayUpdateで検出し、それをIsPointInRect経由の、さらにCanMoveItでチェックする!

Listing. 4: タイルの交換可能性のチェック
void PlayUpdate(Board& _board)
{
	if (MouseL.down()) {
		Point cp = Cursor::Pos();//クリックしたカーソル位置
 
		for (int j = 0; j < BOARD_HEIGHT; j++)
		{
			for (int i = 0; i < BOARD_WIDTH; i++)
			{
				if (IsPointInRect(cp, _board.tileRec[j][i]))
				{
					Point p = { i, j };
					if (CanMoveIt(p, _board))
						Print << _board.tile[j][i] << U";  It Can Move";
					else
						Print << _board.tile[j][i] << U";  Umm, It Can't Move";
				}
			}
		}
	}
}

動作チェック結果

タイルを1~16まで押してみて、動かせるかどうか確認する。
12番と、15番のみ動かせる判定が出ればちゃんと動作できてる気がする

Fig. 6: タイルが動かせるか全押しチェック

画面は、配列のデータをグラフィカルに表示しているだけであった。
クリック位置の、タイル番号が取得できるようになったので、実際に配列に入ってるデータを交換すれば、画面上にも反映されるはずである。
タイルの交換関数SwapTileはタイル番号を指定していたのを、直接インデックス指定するように変更すれば、ほぼそのまま使えるはず。
(2度手間だけど、そのまま使っても行けると思う。そん時はCanMoveItも番号で検索したほうが楽)

Listing. 5: タイルの交換処理
//引数がint _num -> Point _pに変更されていることに注意
void SwapTile(Point _p, Board& _board)
{
	Point blank = SearchTileNum(BLANK_POS, _board);
 
	if (エラー座標じゃないかチェック)//-1がx,yどっちかに入ってたらエラーだよね
		return;//そのままおかえりいただく
	if (_pのタイルが交換可能なら)
	{
		blankとpの位置のタイルを交換(自分でやっても、swap関数使ってもどちらでもいいよ)
	}
	else {
		return;//動かせないときはおかえりいただく
	}
}

動作確認

PlayUpdateで、さっきまでCanMoveItで表示してたメッセージを、動かせる場合はSwapTileを読んでやることにするだけ。

Listing. 6: タイル交換のチェック
void PlayUpdate(Board& _board)
{
	if (MouseL.down()) {
		Point cp = Cursor::Pos();//クリックしたカーソル位置
 
		for (int j = 0; j < BOARD_HEIGHT; j++)
		{
			for (int i = 0; i < BOARD_WIDTH; i++)
			{
				if (cpと_board.tileRec[j][i]の当たり判定)
				{
					Point p = { i, j };
					if (pは動かせるタイルの位置か)
						動かせるならpの位置のタイルを動かす!
 
				}
			}
		}
	}
}

動作確認結果

Fig. 7: 動作確認

やっとスライドできた。。。
できる人はスライドの動きを滑らかにしてみたり、画面に枠をつけたり、いろいろ追加要素考えてみよう。

その6 問題の作成と、ゲームクリア

  • game-engineer/classes/2023/something-else/summertime-special-cource/slidepuzle-siv3d-5.txt
  • 最終更新: 3年前
  • by root