クリスマスにちなんだブラウザゲームを作ってみた(その2)

# はじめに
昨年に引き続き、クリスマスにちなんだゲームを作ってみました。
PCとスマートフォンで遊べます。
昨年のものは[こちら](https://qiita.com/noji505/items/6af1f143e2246e4cbbd1)

すぐに遊んでみたい方は[こちら](#ゲーム)


# ゲーム説明
ゲームのイメージはこのようになります
<img width="300" alt="01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740996/726aa3fd-5184-09db-e883-404d221d4340.png">

* 画面中央下のサンタを操作し、プレゼントを上の人の所に運びます
* 頭上にそれぞれ欲しいプレゼントが表示されており、同じ色のプレゼントを渡す(※)とスコアがアップします
* 3つ全て運ぶと再度プレゼントが出現するのでそれを繰り返します
<font color="Red">※プレゼントを持った順番通りに渡さないとダメです</font>

# 力を入れた点など
テストプレイとして身近な人に遊んでもらった所、何度か遊ばないとルールがよくわからないと言われたためゲーム内で説明画面を追加しました
<img width="300" alt="02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740996/b474fd57-7995-2f45-cb09-b98da0e50b5a.png">

あと基本的に絵が描けないので、背景については矩形で表現しました。
↓こんな感じ

```javascript:マップ表示部分
function drawMap()
{
	const arrColors = [
		[BLACK,   RED,   RED,   RED, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,   RED,   RED,   RED, BLACK],
		[BLACK,   RED,   RED,   RED, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,   RED,   RED,   RED, BLACK],
		[BLACK,  SKIN,  SKIN,  SKIN, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,  SKIN,  SKIN,  SKIN, BLACK],
		[BLACK,  SKIN, BLACK,  SKIN, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,  SKIN, BLACK,  SKIN, BLACK],
		[ GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY],
		[ GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY],
		[BLACK, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK, BLACK],
		[BLACK, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK, BLACK],
		[BLACK,   RED,   RED,   RED, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK,   RED,   RED,   RED, BLACK],
		[BLACK,   RED,   RED,   RED, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK,   RED,   RED,   RED, BLACK],
		[BLACK,  SKIN,  SKIN,  SKIN, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK,  SKIN,  SKIN,  SKIN, BLACK],
		[BLACK,  SKIN, BLACK,  SKIN, BLACK,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY, BLACK,  SKIN, BLACK,  SKIN, BLACK],
		[ GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY],
		[ GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY,  GRAY],
		[BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK],
		[BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK],
		[BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK],
		[BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK],
		[BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,  GRAY,  GRAY,  GRAY,  GRAY, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK],
		[BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK]
	];

	for( let i=0; i<MAP_Y; i++ ){
		for( let j=0; j<MAP_X; j++ ){
			drawFill( i*MAP_TIP, j*MAP_TIP, MAP_TIP, MAP_TIP, arrColors[j][i], null );
		}
	}

}
```
サンタは頑張って描きましたｗ

# 当たり判定処理について
サンタと壁との当たり判定処理はライブラリのような作りにして楽に行えるようにしています。

<details><summary>当たり判定処理のソースを見る</summary><div>

```javascript:当たり判定ライブラリソース
//=======================================================================================
//  壁当たり判定処理
//=======================================================================================

//-----------------------------------------------------------
// wallHitCheck : 壁当たり判定チェック
//-----------------------------------------------------------
function wallHitCheck( w, h, arrWall )
{
	let i;
	let retArray;
	let wallNum;

	wallNum = arrWall.length;
	let dir = new Array(wallNum);
	let retDirNum = 0;		// 当たった向き(0:当たっていない  1:上  2:下  3:左  4:右)

	if( arrWall == null )	return retDirNum;

	for( i=0; i<wallNum; i++ ){
		if( objHitCheck(playerX, playerY, w, h, arrWall[i][0], arrWall[i][1], arrWall[i][2], arrWall[i][3]) ){

			dir[i] = getDirection( playerX, playerY, w, h, arrWall[i][0], arrWall[i][1], arrWall[i][2], arrWall[i][3] );

			if( dir[i] == 0 ){

				// 壁との判定を調べて、どの方向に移動すれば一番早く抜けれるかを調べる
				retArray = getHitEscape( playerX, playerY, w, h, arrWall[i][0], arrWall[i][1], arrWall[i][2], arrWall[i][3] );

				switch( retArray[0] ){
				case 0:	// 下と当たったので上に移動
					playerY -= retArray[1];
					retDirNum = 2;
					break;
				case 1:	// 上に当たったので下に移動
					playerY += retArray[1];
					retDirNum = 1;
					break;
				case 2:	// 右に当たったので左に移動
					playerX -= retArray[1];
					retDirNum = 4;
					break;
				case 3:	// 左に当たったので右に移動
					playerX += retArray[1];
					retDirNum = 3;
					break;
				}
			}
		}
	}

	return retDirNum;
}

//-----------------------------------------------------------
// objHitCheck : 当たり判定チェック
//        引数 : obj1のx,y,w,h  obj2のx,y,w,h 
//      戻り値 : true:ヒット  false:未ヒット
//-----------------------------------------------------------
function objHitCheck( px, py, pw, ph, ex, ey, ew, eh )
{
	if( (px <= (ex + ew) ) && (ex <= (px + pw)) && (py <= (ey + eh)) && (ey <= (py + ph)) ){
		return true;
	}
	return false;
}

//-----------------------------------------------------------
// getDirection : 当たり判定方向取得
//         引数 : obj1のx,y,w,h  obj2のx,y,w,h
//       戻り値 : 当たった方向
//-----------------------------------------------------------
function getDirection( x, y, w, h, x2, y2, w2, h2 )
{
	let result = 0;

	if( y2 > y + h ){
		result |= DIR_UP;		// 上
	}
	if( y2 + h2 < y ){
		result |= DIR_DOWN;		// 下
	}

	if( x2 > x + w ){
		result |= DIR_LEFT;		// 左
	}
	if( x2 + w2 < x ){
		result |= DIR_RIGHT;	// 右
	}

	return result;
}

//-----------------------------------------------------------
// getHitEscape : オブジェクトの判定位置を取得
//-----------------------------------------------------------
function getHitEscape( x, y, w, h, x2, y2, w2, h2 )
{
	let cnt = 0;
	let cnt2 = 0;
	let buff;
	let retArray = new Array(2);

	// 上下判定
	buff = y;
	while( true ){
		if( objHitCheck(x, buff, w, h, x2, y2, w2, h2) ){
			buff--;	// 上に移動
			cnt++;	// 移動量を保存
		} else {
			break;
		}
	}
	buff = y;
	while( true ){
		if( objHitCheck(x, buff, w, h, x2, y2, w2, h2) ){
			buff++;	// 下に移動
			cnt2++;	// 移動量を保存
		} else {
			break;
		}
	}
	// 移動量が少なかった方を採用
	if( cnt <= cnt2 ){	// 上
		retArray[0] = 0;	// 上
		retArray[1] = cnt;	// 上の移動量を保存
	} else {			// 下
		retArray[0] = 1;	// 下
		retArray[1] = cnt2;	// 下の移動量を保存
	}

	cnt = 0;
	cnt2 = 0;

	// 左右判定
	buff = x;
	while( true ){
		if( objHitCheck(buff, y, w, h, x2, y2, w2, h2) ){
			buff--;	// 左に移動
			cnt++;	// 移動量を保存
		} else {
			break;
		}
	}
	buff = x;
	while( true ){
		if( objHitCheck(buff, y, w, h, x2, y2, w2, h2) ){
			buff++;	// 右に移動
			cnt2++;	// 移動量を保存
		} else {
			break;
		}
	}
	// 移動量が少なかった方を採用
	if( cnt <= cnt2 ){	// 左
		if( cnt < retArray[1] ){	// 左の方が移動量が少ない
			retArray[0] = 2;	// 左
			retArray[1] = cnt;	// 左の移動量を保存
		}
	} else {
		if( cnt2 < retArray[1] ){	// 右の方が移動量が少ない
			retArray[0] = 3;	// 右
			retArray[1] = cnt2;	// 右の移動量を保存
		}
	}

	return retArray;
}
```

</div></details>

# 当たり判定ライブラリ解説
以下のような流れで判定を行っています。
1.キャラと壁が当たっているかチェック
→wallHitCheckメソッド
2.当たっている場合、どの方向に当たっているかチェック
→getDirectionメソッド
3.壁から離さないと埋まった状態になるので、壁から抜ける距離を算出
→getHitEscapeメソッド


今回は当たり判定を行うのはサンタ(プレイヤー)のみなので、ライブラリ内で座標調整を行っています。
他にも同じ処理を行う必要がある場合は、移動量(壁に接触した際に埋まった部分を戻す)と当たった向きの二つを返すようにすればより汎用的な処理になるかと思います。

# ライブラリ使用イメージ
使用する側はライブラリの一番最初のメソッド(wallHitCheck)を呼び出して、壁と当たっていたら当たっている向きが返ってくるので
その値に応じて処理をするだけというシンプルな作りになっています。

```javascript:使用側ソース
let dir = wallHitCheck( 16, 16, arrWallHitRect );  // 壁当たり判定チェック

// 壁に当たったので停止
switch( dir ){
case 1:	// 上
case 2:	// 下
	iDelY = 0;
	break;
case 3:	// 左
case 4:	// 右
	iDelX = 0;
	break;
}
```

# 考え方のポイント
上記の例は当たり判定の処理ですが、ポイントとしては「<b>ゲーム毎に変わる要素と変わらない要素</b>」を分けて考えることだと思っています。
[変わる要素]
* 当たり判定元の座標、サイズ、壁の位置

[変わらない要素]
* 当たり判定処理

つまり「<b>変わる要素</b>」の部分を外から指定してあげられるようにすれば、ゲームが変わっても汎用的に利用出来るようになります。

# その他工夫した所とか
繰り返し遊べるように、プレゼントや人の配置は毎回ランダムにしました。
あとちょっとしたオマケ要素も入れています。
何度か遊ばないと分からないと思うので書いておきます。
<font color="Red">※自力で見つけたい人は見ないでください</font>

<details><summary>ここをクリックすると見れます</summary><div>

<img width="300" alt="03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740996/8de93281-6be5-4d8a-693f-b8371023b203.png">

遊んでいると、たまに画像のようなスペシャルキャラが登場します
(中央左の赤いキャラ)
このキャラに上手くプレゼントを渡せるとスコアがたくさんもらえます
頑張ってチャレンジしてみて下さい

[ヒント]
* このキャラは50%の確率で出現します
* 出現場所は必ず「通常のキャラ(棒人間)が居ない所」から出現します
* 画面上に残っているプレゼントを要求します(つまり残っているプレゼントが1つだと...)

</div></details>

# ゲーム
↓から遊べます。<font color="Red">※音が出るので注意</font>
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="NWzvENX" data-user="noji505" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/noji505/pen/NWzvENX">
  Qiita24_santagame02</a> by nojima (<a href="https://codepen.io/noji505">@noji505</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

大きい画面で遊びたい方は[こちらからどうぞ](https://codepen.io/noji505/full/NWzvENX)

それでは良いクリスマスを♪