過去記事一覧
現在のコードについては前回の記事の最後の項を参照してください。
プログラム
今回作ったプログラムはhttp://yurinya.net/roguelike2019/3/srl.htmlで公開しています。良かったらアクセスしてみてください!
プレイヤーの移動
このゲームは基本的にキーボードを使って操作することにします。
プレイヤーの移動は矢印キーを使って行うことにします。
まず、プレイヤーは全てのマスに移動できる訳ではないのでした。
床のマスには移動できますが、壁のマスには移動できません。
つまり、マスの種類によって移動できるかどうかが決まります。
これをB_CAN_STAND
という配列で下のように定義することにします。true
は移動できるマスであることを表し、false
は移動できないマスであることを表します。
var B_CAN_STAND = [];
B_CAN_STAND[B_FLOOR] = true;
B_CAN_STAND[B_WALL] = false;
そして、キー入力の処理を下のように変更します。
なお、矢印キーのkeyCode
の値はそれぞれ下のようになっています。
-
←
・・・37
-
↑
・・・38
-
→
・・・39
-
↓
・・・40
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);
});
矢印キーが押された時の処理が追加されました。
矢印キーの種類によってプレイヤーの新しい位置が決まり、その位置のマスの種類が移動できるものである場合には移動し(プレイヤーの位置を更新し)、再描画を行います。
See the Pen roguelike-3-1 by pizyumi (@pizyumi) on CodePen.
斜め移動
ローグライクゲームはやっぱり斜め移動もできないとですよね。
斜め移動を実装しましょう。
斜め移動はShift
キーを押しながら矢印キーを押すことで行うことにします。
実装においては少なくとも以下の2点を満たす必要があるかと思います。
-
Shift
キーを押している時のみしか斜め移動できない(Shift
キーを押していないのに斜め移動できてはならない)。 -
Shift
キーを押している時には斜め移動しか出来ない(Shift
キーを押している時には上下左右の移動はできてはならない)。
このようにすることにより、誤操作が起きにくくなります。
このような斜め移動の実装方法は幾つかあると思いますが、私は下のような実装方法が好きです。
keyl
、keyu
、keyr
、keyd
という変数を用意し、矢印キーが押されているかどうかを常に監視するようにします。
そして、Shift
キーが押されている場合にはこれらの変数の値を基に移動を行うべきかを判断します。
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
キーを押した時にプレイヤーの周りに斜め矢印が表示されるようにしましょう。
この斜め矢印は、たとえば、ふし幻(不思議の幻想郷)でも採用されていますね。
これを行うには、Shift
キーが押されているか(というか斜め移動可能な状態か)を保持しておく必要があります。
この状態はenv.diagonal
で保持することにします。env.diagonal
の値がtrue
である場合には斜め移動可能で、false
である場合には斜め移動不可能です。
var env = {
diagonal: false
};
そして、この状態(env.diagonal
の値)によってゲームの描画内容(プレイヤーの周りに斜め矢印を表示するかどうか)が変わりますので、この状態はdraw
関数に渡さなければならないということになります。
そこで、全てのdraw
関数の呼び出しにおいてはenv
を第2引数として指定することにします(変更したコードは示しませんが、全てのdraw
関数の呼び出しで第2引数としてenv
を渡すように変更します)。
そうしたら、2つ目のkeydown
イベントハンドラを下のように変更します。
つまり、Shift
キーが押された場合の処理を追加します。
Shift
キーが押された場合にはenv.diagonal
の値をtrue
にし、再描画を行います。
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
にし、再描画を行います。
これはウィンドウからフォーカスが外れた場合にも斜め移動が有効なままであるように表示されるのを防ぐためです。
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
である場合の描画処理を追加します。
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
は下のようになりました。
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();
}
}
次回はダンジョンの生成を少しだけ考えてみたいと思います。