ローグライクゲームを作ってみるその3 プレイヤーの移動

### 過去記事一覧

* [その1 タイトル画面](https://qiita.com/pizyumi/items/3526fddd4f18a462e1ae)
* [その2 ダンジョン・プレイヤーの生成と描画](https://qiita.com/pizyumi/items/2562a159f497a608615b)

現在のコードについては前回の記事の最後の項を参照してください。

### プログラム

今回作ったプログラムは[http://yurinya.net/roguelike2019/3/srl.html](http://yurinya.net/roguelike2019/3/srl.html)で公開しています。良かったらアクセスしてみてください！

### プレイヤーの移動

このゲームは基本的にキーボードを使って操作することにします。

プレイヤーの移動は矢印キーを使って行うことにします。

まず、プレイヤーは全てのマスに移動できる訳ではないのでした。

床のマスには移動できますが、壁のマスには移動できません。

つまり、マスの種類によって移動できるかどうかが決まります。

これを`B_CAN_STAND`という配列で下のように定義することにします。`true`は移動できるマスであることを表し、`false`は移動できないマスであることを表します。

```js
var B_CAN_STAND = [];
B_CAN_STAND[B_FLOOR] = true;
B_CAN_STAND[B_WALL] = false;
```

そして、キー入力の処理を下のように変更します。

なお、矢印キーの`keyCode`の値はそれぞれ下のようになっています。

* `←`・・・`37`
* `↑`・・・`38`
* `→`・・・`39`
* `↓`・・・`40`

```js
	c.on('keydown', function (e) {
（省略）
		if (e.keyCode >= 37 && e.keyCode <= 40) {
			var x = player.x;
			var y = player.y;
			if (e.keyCode === 37) {
				if (x === 0) {
					return;
				}
				x--;
			}
			else if (e.keyCode === 38) {
				if (y === 0) {
					return;
				}
				y--;
			}
			else if (e.keyCode === 39) {
				if (x === LX - 1) {
					return;
				}
				x++;
			}
			else if (e.keyCode === 40) {
				if (y === LY - 1) {
					return;
				}
				y++;
			}

			if (x !== player.x || y !== player.y) {
				var block = field.blocks[x][y];
				if (B_CAN_STAND[block.base]) {
					player.x = x;
					player.y = y;
				}
				else {
					return;
				}
			}
			else {
				return;
			}
		}
		else {
			return;
		}

		draw(con);
	});
```

矢印キーが押された時の処理が追加されました。

矢印キーの種類によってプレイヤーの新しい位置が決まり、その位置のマスの種類が移動できるものである場合には移動し（プレイヤーの位置を更新し）、再描画を行います。

[](https://codepen.io/pizyumi/pen/ExYOVwj)

<p class="codepen" data-height="600" data-theme-id="0" data-default-tab="result" data-user="pizyumi" data-slug-hash="ExYOVwj" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="roguelike-3-1">
  <span>See the Pen <a href="https://codepen.io/pizyumi/pen/ExYOVwj/">
  roguelike-3-1</a> by pizyumi (<a href="https://codepen.io/pizyumi">@pizyumi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

### 斜め移動

ローグライクゲームはやっぱり斜め移動もできないとですよね。

斜め移動を実装しましょう。

斜め移動は`Shift`キーを押しながら矢印キーを押すことで行うことにします。

実装においては少なくとも以下の2点を満たす必要があるかと思います。

* `Shift`キーを押している時のみしか斜め移動できない（`Shift`キーを押していないのに斜め移動できてはならない）。
* `Shift`キーを押している時には斜め移動しか出来ない（`Shift`キーを押している時には上下左右の移動はできてはならない）。

このようにすることにより、誤操作が起きにくくなります。

このような斜め移動の実装方法は幾つかあると思いますが、私は下のような実装方法が好きです。

`keyl`、`keyu`、`keyr`、`keyd`という変数を用意し、矢印キーが押されているかどうかを常に監視するようにします。

そして、`Shift`キーが押されている場合にはこれらの変数の値を基に移動を行うべきかを判断します。

```js
	var keyl = false;
	var keyu = false;
	var keyr = false;
	var keyd = false;

	var c = $('body');
	c.on('keydown', function (e) {
		if (e.keyCode === 37) {
			keyl = true;
		}
		else if (e.keyCode === 38) {
			keyu = true;
		}
		else if (e.keyCode === 39) {
			keyr = true;
		}
		else if (e.keyCode === 40) {
			keyd = true;
		}
		else {
			keyl = false;
			keyu = false;
			keyr = false;
			keyd = false;
		}
	});
	c.on('keyup', function (e) {
		if (e.keyCode === 37) {
			keyl = false;
		}
		else if (e.keyCode === 38) {
			keyu = false;
		}
		else if (e.keyCode === 39) {
			keyr = false;
		}
		else if (e.keyCode === 40) {
			keyd = false;
		}
	});
	c.on('keydown', function (e) {
（省略）
		if (e.keyCode >= 37 && e.keyCode <= 40) {
			var x = player.x;
			var y = player.y;
			if (e.shiftKey) {
				if (keyl && keyu) {
					if (x === 0 || y === 0) {
						return;
					}
					x--;
					y--;
				}
				else if (keyr && keyu) {
					if (x === LX - 1 || y === 0) {
						return;
					}
					x++;
					y--;
				}
				else if (keyl && keyd) {
					if (x === 0 || y === LY - 1) {
						return;
					}
					x--;
					y++;
				}
				else if (keyr && keyd) {
					if (x === LX - 1 || y === LY - 1) {
						return;
					}
					x++;
					y++;
				}
				else {
					return;
				}
			}
			else {
				if (e.keyCode === 37) {
					if (x === 0) {
						return;
					}
					x--;
				}
				else if (e.keyCode === 38) {
					if (y === 0) {
						return;
					}
					y--;
				}
				else if (e.keyCode === 39) {
					if (x === LX - 1) {
						return;
					}
					x++;
				}
				else if (e.keyCode === 40) {
					if (y === LY - 1) {
						return;
					}
					y++;
				}
			}

			if (x !== player.x || y !== player.y) {
（省略）
			}
			else {
				return;
			}
		}
		else {
			return;
		}

		draw(con);
	});
```

### 斜め矢印の表示

`Shift`キーを押した時に斜め移動が可能なことをユーザーに分かりやすく見せるために、`Shift`キーを押した時にプレイヤーの周りに斜め矢印が表示されるようにしましょう。

この斜め矢印は、たとえば、ふし幻（不思議の幻想郷）でも採用されていますね。

![01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10969/8014acad-2342-860d-a6ca-49e852c56845.jpeg)

これを行うには、`Shift`キーが押されているか（というか斜め移動可能な状態か）を保持しておく必要があります。

この状態は`env.diagonal`で保持することにします。`env.diagonal`の値が`true`である場合には斜め移動可能で、`false`である場合には斜め移動不可能です。

```js
	var env = {
		diagonal: false
	};
```

そして、この状態（`env.diagonal`の値）によってゲームの描画内容（プレイヤーの周りに斜め矢印を表示するかどうか）が変わりますので、この状態は`draw`関数に渡さなければならないということになります。

そこで、全ての`draw`関数の呼び出しにおいては`env`を第2引数として指定することにします（変更したコードは示しませんが、全ての`draw`関数の呼び出しで第2引数として`env`を渡すように変更します）。

そうしたら、2つ目の`keydown`イベントハンドラを下のように変更します。

つまり、`Shift`キーが押された場合の処理を追加します。

`Shift`キーが押された場合には`env.diagonal`の値を`true`にし、再描画を行います。

```js
	c.on('keydown', function (e) {
		if (!startf) {
（省略）
		}

		if (e.keyCode === 16) {
			if (!env.diagonal) {
				env.diagonal = true;

				draw(con, env);
			}

			return;
		}

		if (e.keyCode >= 37 && e.keyCode <= 40) {
（省略）
		}
		else {
			return;
		}

		draw(con, env);
	});
```

更に、新たに`keyup`イベントハンドラと`window`の`blur`イベントハンドラを追加します。

`keyup`イベントハンドラには`Shift`キーが離された場合の処理を記述します。

`Shift`キーが離された場合には`env.diagonal`の値を`false`にし、再描画を行います。

`blur`イベントハンドラにはウィンドウからフォーカスが外れた場合の処理を記述します。

ウィンドウからフォーカスが外れた場合にも`env.diagonal`の値を`false`にし、再描画を行います。

これはウィンドウからフォーカスが外れた場合にも斜め移動が有効なままであるように表示されるのを防ぐためです。

```js
	c.on('keyup', function (e) {
		if (e.keyCode === 16) {
			if (env.diagonal) {
				env.diagonal = false;

				draw(con, env);
			}
		}
	});
	$(window).on('blur', function (e) {
		if (env.diagonal) {
			env.diagonal = false;

			draw(con, env);
		}
	});
```

最後に`draw`関数を変更します。

`env.diagonal`の値が`true`である場合の描画処理を追加します。

```js
function draw (con, env) {
（省略）

	if (env.diagonal) {
		con.save();

		con.strokeStyle = 'white';
		con.translate(player.x * PX + (PX / 2), player.y * PY + (PY / 2));
		con.rotate(Math.PI / 4);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();
		con.rotate(Math.PI / 4 * 2);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();
		con.rotate(Math.PI / 4 * 2);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();
		con.rotate(Math.PI / 4 * 2);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();

		con.restore();
	}
}
```

これで`Shift`キーを押すとプレイヤーの周りに4つの斜め矢印が表示され、斜め移動できるようになりました。

### 今回はここまで

今回はここまでです。

`game.js`は下のようになりました。

```js
﻿var TITLE = 'シンプルローグライク';

var TEXT_START = 'はじめる';

var SCREEN_X = 1600;
var SCREEN_Y = 800;

var LX = 25;
var LY = 25;
var PX = 32;
var PY = 32;

var B_FLOOR = 0;
var B_WALL = 1;

var B_CAN_STAND = [];
B_CAN_STAND[B_FLOOR] = true;
B_CAN_STAND[B_WALL] = false;

var startf = false;

var field = null;
var player = null;

$(function(){
	var canvas = document.getElementById('game');
	var con = canvas.getContext('2d');

	var keyl = false;
	var keyu = false;
	var keyr = false;
	var keyd = false;

	var env = {
		diagonal: false
	};

	var c = $('body');
	c.on('keydown', function (e) {
		if (e.keyCode === 37) {
			keyl = true;
		}
		else if (e.keyCode === 38) {
			keyu = true;
		}
		else if (e.keyCode === 39) {
			keyr = true;
		}
		else if (e.keyCode === 40) {
			keyd = true;
		}
		else {
			keyl = false;
			keyu = false;
			keyr = false;
			keyd = false;
		}
	});
	c.on('keyup', function (e) {
		if (e.keyCode === 37) {
			keyl = false;
		}
		else if (e.keyCode === 38) {
			keyu = false;
		}
		else if (e.keyCode === 39) {
			keyr = false;
		}
		else if (e.keyCode === 40) {
			keyd = false;
		}
	});
	c.on('keydown', function (e) {
		if (!startf) {
			if (e.keyCode === 90) {
				startf = true;

				init();

				draw(con, env);
			}

			return;
		}

		if (e.keyCode === 16) {
			if (!env.diagonal) {
				env.diagonal = true;

				draw(con, env);
			}

			return;
		}

		if (e.keyCode >= 37 && e.keyCode <= 40) {
			var x = player.x;
			var y = player.y;
			if (e.shiftKey) {
				if (keyl && keyu) {
					if (x === 0 || y === 0) {
						return;
					}
					x--;
					y--;
				}
				else if (keyr && keyu) {
					if (x === LX - 1 || y === 0) {
						return;
					}
					x++;
					y--;
				}
				else if (keyl && keyd) {
					if (x === 0 || y === LY - 1) {
						return;
					}
					x--;
					y++;
				}
				else if (keyr && keyd) {
					if (x === LX - 1 || y === LY - 1) {
						return;
					}
					x++;
					y++;
				}
				else {
					return;
				}
			}
			else {
				if (e.keyCode === 37) {
					if (x === 0) {
						return;
					}
					x--;
				}
				else if (e.keyCode === 38) {
					if (y === 0) {
						return;
					}
					y--;
				}
				else if (e.keyCode === 39) {
					if (x === LX - 1) {
						return;
					}
					x++;
				}
				else if (e.keyCode === 40) {
					if (y === LY - 1) {
						return;
					}
					y++;
				}
			}

			if (x !== player.x || y !== player.y) {
				var block = field.blocks[x][y];
				if (B_CAN_STAND[block.base]) {
					player.x = x;
					player.y = y;
				}
				else {
					return;
				}
			}
			else {
				return;
			}
		}
		else {
			return;
		}

		draw(con, env);
	});
	c.on('keyup', function (e) {
		if (e.keyCode === 16) {
			if (env.diagonal) {
				env.diagonal = false;

				draw(con, env);
			}
		}
	});
	$(window).on('blur', function (e) {
		if (env.diagonal) {
			env.diagonal = false;

			draw(con, env);
		}
	});

	draw(con, env);
});

function init () {
	field = create_field();
	player = {
		x: 12,
		y: 17
	};
}

function create_field () {
	var blocks = [];
	for (var i = 0; i < LX; i++) {
		blocks[i] = [];
		for (var j = 0; j < LY; j++) {
			if ((i === 0 || j === 0) || (i === LX -1 || j === LY - 1)) {
				blocks[i][j] = {
					base: B_WALL
				};
			}
			else {
				blocks[i][j] = {
					base: B_FLOOR
				};
			}
		}
	}

	return {
		blocks: blocks
	};
}

function draw (con, env) {
	con.fillStyle = 'black';
	con.fillRect(0, 0, SCREEN_X, SCREEN_Y);

	if (!startf) {
		con.textBaseline = 'alphabetic';
		con.textAlign = 'center';
		con.fillStyle = 'white';

		con.font = "48px consolas";
		con.fillText(TITLE, SCREEN_X / 2, SCREEN_Y / 4);

		con.font = "32px consolas";
		con.fillText('> ' + TEXT_START, SCREEN_X / 2, SCREEN_Y / 4 * 3);

		return;
	}

	for (var i = 0; i < LX; i++) {
		for (var j = 0; j < LY; j++) {
			var block = field.blocks[i][j];
			if (block.base === B_FLOOR) {
				con.fillStyle = 'white';
				con.beginPath();
				con.arc((i + 0.5) * PX, (j + 0.5) * PY, 1, 0, Math.PI * 2);
				con.closePath();
				con.fill();
			}
			else if (block.base === B_WALL) {
				con.strokeStyle = 'white';
				con.strokeRect(i * PX, j * PY, PX, PY);
				con.beginPath();
				con.moveTo(i * PX, j * PY);
				con.lineTo((i + 1) * PX, (j + 1) * PY);
				con.moveTo((i + 1) * PX, j * PY);
				con.lineTo(i * PX, (j + 1) * PY);
				con.closePath();
				con.stroke();
			}
		}
	}

	con.textBaseline = 'middle';
	con.textAlign = 'center';
	con.fillStyle = 'red';
	con.font = '24px consolas';
	con.fillText('🚶\uFE0E', player.x * PX + (PX / 2), player.y * PY + (PY / 2));

	if (env.diagonal) {
		con.save();

		con.strokeStyle = 'white';
		con.translate(player.x * PX + (PX / 2), player.y * PY + (PY / 2));
		con.rotate(Math.PI / 4);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();
		con.rotate(Math.PI / 4 * 2);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();
		con.rotate(Math.PI / 4 * 2);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();
		con.rotate(Math.PI / 4 * 2);
		con.beginPath();
		con.moveTo((PX / 2) + 4, -4);
		con.lineTo((PX / 2) + 4 + 8, -4);
		con.lineTo((PX / 2) + 4 + 8, -4 - 4);
		con.lineTo((PX / 2) + 4 + 8 + 8, 0);
		con.lineTo((PX / 2) + 4 + 8, 4 + 4);
		con.lineTo((PX / 2) + 4 + 8, 4);
		con.lineTo((PX / 2) + 4, 4);
		con.closePath();
		con.stroke();

		con.restore();
	}
}
```

次回はダンジョンの生成を少しだけ考えてみたいと思います。