画面ができたのでついに、タイルをスライドさせてみる。
コンソール版を作ってみてわかったと思うが、このゲームのかなめになる処理である。
現在のゲームの製作状況は、
までできている(気がする)
ここで、「盤面のタイル番号は、実はコンソール版のデータを画面表示しているだけである」点に注意すると、
問題作成の関数MakeProblemや、SwapTile、CanMoveIt、SearchTileNumはコンソール版のままそのまま使える気がする。
全体のゲーム関連の初期化を行う関数をここで作っておこう。そこでデータの初期化や、問題作成をしてからゲームに突入することにする。
//プロトタイプ宣言(適切なところに書いてね)
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)
入れ替えできたよね。つまり、あとは裏で、コンソール版のプログラムを動かしながら、画面表示を頑張ればよさそう。
確認出来たら、swapは必要ないのでコメントするか消しておこう。
次に、画面上のパネルをクリックしたらその番号を取得する処理を考える。
この処理をパズルが完成するまで繰り返すことでゲームを進行する。
siv3Dには、Rectの領域がクリックされたかどうかの判別関数が用意されている。
Rect rect{x,y,width};
があるとき
rect.leftClicked()
を呼ぶと、そのフレームでクリックされたかどうかをtrue, falseで返す。
しかしながら、君たちはゲームエンジニア科の学生である。
このような用意された道具を使って、ゲームを作るのも大事だが、自分で道具を作れなければエンジニアにはなれない。
矩形領域がクリックされたかどうかという問題は、ある四角形の中に点が含まれるか含まれないかという判定、すなわち点と矩形の当たり判定である。
具体的には、クリックした座標が、図の点aの状態なのか点bの状態なのか判別し、aならtrue、bならfalseを返す関数を作ればよい。
条件として今回は、タイルをx軸、y軸に平行に並べてボードを構成している。矩形のエッジ(辺)は必ず軸に平行なので結構簡単な判定ができる。
これは当たり判定の超基本になる。
ここでPoint型は、Vec2の整数版である。コンソール版を作った時のPosition型に相当するので、そのまま置き換えてよい
条件を整理する
//矩形に関する変数
Point p{x, y};
Rect rec{ P, Width, Height};
//内外判定する点
Point cp{a, b}
が宣言されているとき
矩形の中にcp(a,b)が存在する条件は、
こんな関数を作ればよい。
関数は、Point型でクリック点を、Rect型でタイルを表す。すなわちこの2つを引数にとる。
戻り値はtrue, false
名前は、IsPointInRectとかにしよう。(別に好きな名前にしていいよ)
bool IsPointInRect(Point point, Rect rect) { // 点が矩形の中にあるかチェック if (条件① かつ 条件②) { return true; } return false; }
void PlayUpdate()内で、クリックされた矩形のインデックスを表示してみよう。
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; } } } }
押したボードの枠の場所(インデックスがi,jのもの)にはまっているタイルの番号を取得する。
つまり、ボードのtile[j][i]を参照すればよい。
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"; } } } }
コンソール版の時は、以下のように、番号ベースで処理していた。
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でチェックする!
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"; } } } } }
画面は、配列のデータをグラフィカルに表示しているだけであった。
クリック位置の、タイル番号が取得できるようになったので、実際に配列に入ってるデータを交換すれば、画面上にも反映されるはずである。
タイルの交換関数SwapTileはタイル番号を指定していたのを、直接インデックス指定するように変更すれば、ほぼそのまま使えるはず。
(2度手間だけど、そのまま使っても行けると思う。そん時はCanMoveItも番号で検索したほうが楽)
//引数が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を読んでやることにするだけ。
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の位置のタイルを動かす! } } } } }