過去記事一覧
現在のコードについては前回の記事の最後の項を参照してください。
プログラム
今回作ったプログラムはhttp://yurinya.net/roguelike2019/6/srl.htmlで公開しています。良かったらアクセスしてみてください!
ゲームメッセージ
今回は初めにゲームメッセージを出力する機能を実装します。
ゲームメッセージはゲーム画面の右下の部分に描画することにします。
まず、ゲームメッセージを保持するためのmessages
変数を用意します。ゲームメッセージはメッセージを表すオブジェクトの配列として保持することにします。
var messages = null;
次に、保持するメッセージの最大数をNUM_MESSAGE
変数で定義します。保持するメッセージの最大数は8個とします。
var NUM_MESSAGE = 8;
次に、メッセージを追加するためのadd_message
関数を実装します。
第1引数にはメッセージを表すオブジェクトを渡すものとします。
メッセージを表すオブジェクトは下のようなプロパティを有するものとします。
-
text
・・・メッセージテキストです。 -
type
・・・メッセージの種類です。 -
repeat
・・・メッセージの繰り返し回数を表します。同じメッセージが連続して出力された場合には繰り返し回数を増加させることによって同じメッセージが連続して出力されたことを表現することにします。
この関数では追加されたメッセージが直前に追加されたメッセージと同じものである場合には直前に追加されたメッセージの繰り返し回数を1
増加させ、異なるものである場合にはmessages
変数にメッセージを追加します。
ただし、保持されているメッセージが8個より多い場合には古いメッセージを削除するようにします。
function add_message (message) {
var l = messages[messages.length - 1];
if (message.text === l.text && message.type === l.type) {
if (!l.repeat) {
l.repeat = 2;
}
else {
l.repeat++;
}
}
else {
messages.push(message);
while (messages.length > NUM_MESSAGE) {
messages.shift();
}
}
}
最後に、draw
関数にメッセージを描画する処理を追加します。
メッセージの種類に応じてメッセージの文字色を変更します。
また、メッセージに繰り返し回数が設定されている場合には繰り返し回数も表記するようにします。
con.save();
con.textBaseline = 'top';
con.textAlign = 'left';
con.font = '16px consolas';
con.translate(SX * PX, SCREEN_Y - ((16 + 6) * NUM_MESSAGE + 8 * 2));
for (var i = 0; i < messages.length; i++) {
if (messages[i].type === 'normal') {
con.fillStyle = 'white';
}
else if (messages[i].type === 'special') {
con.fillStyle = 'yellow';
}
else {
throw new Error('not supported.');
}
var text = messages[i].text;
if (messages[i].repeat) {
text += '(' + 'x' + messages[i].repeat + ')';
}
con.fillText(text, 8, (16 + 6) * i + 8);
}
con.restore();
これでゲームメッセージを出力する機能が実装できました。
メッセージの追加
実際に幾つかメッセージを出力してみましょう。
まず、メッセージテキストを定義します。
3つのメッセージを出力するようにしてみます。
1つ目のメッセージは新しいゲームを開始した時に出力します。
2つ目のメッセージは下り階段を降りた場合に出力します。
3つ目のメッセージは壁に向かって歩こうとした場合に出力します。
var MSG_INIT = 'あなたは目覚めました。';
var MSG_DOWNSTAIR = '下り階段を降りました。';
var MSG_WALL = '壁に阻まれました。';
1つ目のメッセージはinit
関数で追加します。
init
関数を下のように変更します。
種類がspecial
でテキストがMSG_INIT
のメッセージを追加します。
function init () {
fields = [];
fields[0] = create_field(0, [], seed);
player = {
depth: 0,
x: 12,
y: 17
};
messages = [{
text: MSG_INIT,
type: 'special'
}];
}
2つ目と3つ目のメッセージはkeydown
イベントハンドラで追加します。
2つ目のkeydown
イベントハンドラを下のように変更します。
移動しようとしたマスの種類が壁であった場合に種類がnormal
でテキストがMSG_WALL
のメッセージを追加します。
また、下り階段から1つ下の階に移動した場合に種類がnormal
でテキストがMSG_DOWNSTAIR
のメッセージを追加します。
c.on('keydown', function (e) {
(省略)
if (e.keyCode >= 37 && e.keyCode <= 40) {
(省略)
if (x !== player.x || y !== player.y) {
var block = fields[player.depth].blocks[x][y];
if (B_CAN_STAND[block.base]) {
player.x = x;
player.y = y;
draw(con, env);
}
else {
if (block.base === B_WALL) {
add_message({
text: MSG_WALL,
type: 'normal'
});
draw(con, env);
}
return;
}
}
else {
return;
}
}
else if (e.keyCode === 32) {
var block = fields[player.depth].blocks[player.x][player.y];
if (block.base === B_DOWNSTAIR) {
player.depth++;
if (!fields[player.depth]) {
fields[player.depth] = create_field(player.depth, [{
x: player.x,
y: player.y
}], seed);
}
add_message({
text: MSG_DOWNSTAIR,
type: 'normal'
});
}
else {
return;
}
}
else {
return;
}
draw(con, env);
});
メッセージが表示されたゲーム画面は下のようになります。
プレイヤーのステータスの描画
次にプレイヤーのステータスを描画します。
と言っても、現在プレイヤーの状態として存在する値は、player
変数に格納されるdepth
、x
、y
のみです。
この中でx
とy
は敢えてゲーム画面に表示する必要はありませんが、depth
は表示すべきでしょう。
また、今後ゲームを開発していくにつれてプレイヤーの状態として様々なものが追加されていくことになるはずです。
その中の幾つかはゲーム画面に表示させるべきものとなるでしょう。
そこで、今後はゲーム画面の右上部分にプレイヤーのステータスを表示していくことにします。
今回はプレイヤーが何階のフロアにいるかを表示する処理を追加してみることにします。
ゲーム画面に描画するテキストを保持する変数を追加します。
var TEXT_DEPTH = '階';
draw
関数に下のコードを追加します。
con.save();
con.textBaseline = 'top';
con.textAlign = 'left';
con.font = '24px consolas';
con.fillStyle = 'white';
con.translate(SX * PX, 0);
con.fillText(player.depth + TEXT_DEPTH, 8, (24 + 6) * 0 + 8);
con.restore();
これでプレイヤーのいる階がゲーム画面に表示されるようになりました。
ゲーム画面は下のようになります。
今回はここまで
今回はここまでです。
game.js
は下のようになりました。
var TITLE = 'シンプルローグライク';
var TEXT_START = 'はじめる';
var TEXT_DEPTH = '階';
var MSG_INIT = 'あなたは目覚めました。';
var MSG_DOWNSTAIR = '下り階段を降りました。';
var MSG_WALL = '壁に阻まれました。';
var SCREEN_X = 1600;
var SCREEN_Y = 800;
var SX = 25;
var SY = 25;
var PX = 32;
var PY = 32;
var B_FLOOR = 0;
var B_WALL = 1;
var B_DOWNSTAIR = 2;
var B_CAN_STAND = [];
B_CAN_STAND[B_FLOOR] = true;
B_CAN_STAND[B_WALL] = false;
B_CAN_STAND[B_DOWNSTAIR] = true;
var NUM_MESSAGE = 8;
var img = new Image();
img.src = 'Dungeon_B_Freem7.png';
var seed = Date.now().toString(10);
var startf = false;
var fields = null;
var player = null;
var messages = 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 nx = fields[player.depth].nx;
var ny = fields[player.depth].ny;
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 === nx - 1 || y === 0) {
return;
}
x++;
y--;
}
else if (keyl && keyd) {
if (x === 0 || y === ny - 1) {
return;
}
x--;
y++;
}
else if (keyr && keyd) {
if (x === nx - 1 || y === ny - 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 === nx - 1) {
return;
}
x++;
}
else if (e.keyCode === 40) {
if (y === ny - 1) {
return;
}
y++;
}
}
if (x !== player.x || y !== player.y) {
var block = fields[player.depth].blocks[x][y];
if (B_CAN_STAND[block.base]) {
player.x = x;
player.y = y;
}
else {
if (block.base === B_WALL) {
add_message({
text: MSG_WALL,
type: 'normal'
});
draw(con, env);
}
return;
}
}
else {
return;
}
}
else if (e.keyCode === 32) {
var block = fields[player.depth].blocks[player.x][player.y];
if (block.base === B_DOWNSTAIR) {
player.depth++;
if (!fields[player.depth]) {
fields[player.depth] = create_field(player.depth, [{
x: player.x,
y: player.y
}], seed);
}
add_message({
text: MSG_DOWNSTAIR,
type: 'normal'
});
}
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 () {
fields = [];
fields[0] = create_field(0, [], seed);
player = {
depth: 0,
x: 12,
y: 17
};
messages = [{
text: MSG_INIT,
type: 'special'
}];
}
function add_message (message) {
var l = messages[messages.length - 1];
if (message.text === l.text && message.type === l.type) {
if (!l.repeat) {
l.repeat = 2;
}
else {
l.repeat++;
}
}
else {
messages.push(message);
while (messages.length > NUM_MESSAGE) {
messages.shift();
}
}
}
function create_field (depth, upstairs, base_seed) {
var random = new Random(base_seed + ',' + depth.toString(10));
var nx = 25;
var ny = 25;
if (depth > 0) {
nx = 50;
ny = 50;
}
var blocks = [];
for (var i = 0; i < nx; i++) {
blocks[i] = [];
for (var j = 0; j < ny; j++) {
if ((i === 0 || j === 0) || (i === nx - 1 || j === ny - 1)) {
blocks[i][j] = {
base: B_WALL
};
}
else {
blocks[i][j] = {
base: B_FLOOR
};
}
}
}
if (depth === 0) {
blocks[12][5] = {
base: B_DOWNSTAIR
};
return {
nx: nx,
ny: ny,
blocks: blocks
};
}
var rs = [{
x1: 1,
x2: nx - 2,
y1: 1,
y2: ny - 2
}];
var ers = [];
var dps = [1, 1, 1, 1, 1, 1, 0.5, 0.5, 0.5, 0.5];
while (rs.length > 0 && dps.length > 0) {
var r = rs.shift();
var nrs = split_room(blocks, r, dps.shift(), random);
for (var i = 0; i < nrs.length; i++) {
rs.push(nrs[i]);
}
if (nrs.length === 0) {
ers.push(r);
}
}
while (rs.length > 0) {
ers.push(rs.shift());
}
var nds = 1;
while (nds > 0) {
var x = random.num(nx - 2) + 1;
var y = random.num(ny - 2) + 1;
var f = true;
for (var i = 0; i < upstairs.length; i++) {
if (x === upstairs[i].x && y === upstairs[i].y) {
f = false;
break;
}
}
if (f) {
blocks[x][y].base = B_DOWNSTAIR;
nds--;
}
}
for (var i = 0; i < upstairs.length; i++) {
if (blocks[upstairs[i].x][upstairs[i].y].base = B_WALL) {
blocks[upstairs[i].x][upstairs[i].y].base = B_FLOOR;
}
}
return {
nx: nx,
ny: ny,
blocks: blocks
};
}
function split_room (blocks, r, dp, random) {
var ap = random.fraction();
if (ap <= dp) {
var dir = random.num(2);
if (r.x2 - r.x1 > (r.y2 - r.y1) * 2) {
dir = 0;
}
else if ((r.x2 - r.x1) * 2 < r.y2 - r.y1) {
dir = 1;
}
if (dir === 0) {
if (r.x2 - r.x1 <= 6) {
return [];
}
var x = random.num(r.x2 - r.x1 - 6) + 3 + r.x1;
if (blocks[x][r.y1 - 1].base !== B_WALL) {
return [];
}
if (blocks[x][r.y2 + 1].base !== B_WALL) {
return [];
}
var y = random.num(r.y2 - r.y1) + r.y1;
for (var i = r.y1; i <= r.y2; i++) {
if (i !== y) {
blocks[x][i].base = B_WALL;
}
}
var r1 = {
x1: r.x1,
x2: x - 1,
y1: r.y1,
y2: r.y2
};
var r2 = {
x1: x + 1,
x2: r.x2,
y1: r.y1,
y2: r.y2
};
var ord = random.num(2);
if (ord === 0) {
return [r1, r2];
}
else {
return [r2, r1];
}
}
else if (dir === 1) {
if (r.y2 - r.y1 <= 6) {
return [];
}
var y = random.num(r.y2 - r.y1 - 6) + 3 + r.y1;
if (blocks[r.x1 - 1][y].base !== B_WALL) {
return [];
}
if (blocks[r.x2 + 1][y].base !== B_WALL) {
return [];
}
var x = random.num(r.x2 - r.x1) + r.x1;
for (var i = r.x1; i <= r.x2; i++) {
if (i !== x) {
blocks[i][y].base = B_WALL;
}
}
var r1 = {
x1: r.x1,
x2: r.x2,
y1: r.y1,
y2: y - 1
};
var r2 = {
x1: r.x1,
x2: r.x2,
y1: y + 1,
y2: r.y2
};
var ord = random.num(2);
if (ord === 0) {
return [r1, r2];
}
else {
return [r2, r1];
}
}
}
return [];
}
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;
}
var nx = fields[player.depth].nx;
var ny = fields[player.depth].ny;
var ox = 0;
if (player.x <= Math.floor(SX / 2)) {
ox = 0;
}
else if (player.x >= nx - Math.floor(SX / 2)) {
ox = nx - SX;
}
else {
ox = player.x - Math.floor(SX / 2);
}
var oy = 0;
if (player.y <= Math.floor(SY / 2)) {
oy = 0;
}
else if (player.y >= ny - Math.floor(SY / 2)) {
oy = ny - SY;
}
else {
oy = player.y - Math.floor(SY / 2);
}
for (var i = 0; i < SX; i++) {
for (var j = 0; j < SY; j++) {
var block = fields[player.depth].blocks[ox + i][oy + 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();
}
else if (block.base === B_DOWNSTAIR) {
con.drawImage(img, 4 * 32, 5 * 32, 32, 32, i * PX, j * PY, PX, PY);
}
}
}
var px = player.x - ox;
var py = player.y - oy;
con.textBaseline = 'middle';
con.textAlign = 'center';
con.fillStyle = 'red';
con.font = '24px consolas';
con.fillText('🚶\uFE0E', px * PX + (PX / 2), py * PY + (PY / 2));
if (env.diagonal) {
con.save();
con.strokeStyle = 'white';
con.translate(px * PX + (PX / 2), py * 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();
}
con.save();
con.textBaseline = 'top';
con.textAlign = 'left';
con.font = '24px consolas';
con.fillStyle = 'white';
con.translate(SX * PX, 0);
con.fillText(player.depth + TEXT_DEPTH, 8, (24 + 6) * 0 + 8);
con.restore();
con.save();
con.textBaseline = 'top';
con.textAlign = 'left';
con.font = '16px consolas';
con.translate(SX * PX, SCREEN_Y - ((16 + 6) * NUM_MESSAGE + 8 * 2));
for (var i = 0; i < messages.length; i++) {
if (messages[i].type === 'normal') {
con.fillStyle = 'white';
}
else if (messages[i].type === 'special') {
con.fillStyle = 'yellow';
}
else {
throw new Error('not supported.');
}
var text = messages[i].text;
if (messages[i].repeat) {
text += '(' + 'x' + messages[i].repeat + ')';
}
con.fillText(text, 8, (16 + 6) * i + 8);
}
con.restore();
}
function hash (seed) {
var sha256 = new jsSHA('SHA-256', 'TEXT');
sha256.update(seed);
return sha256.getHash('HEX');
}
class Random {
constructor (seed) {
this.seed = seed;
this.hash = hash(seed);
this.pointer = 0;
}
byte () {
if (this.pointer === 64) {
this.hash = hash(this.hash);
this.pointer = 0;
}
var value = this.hash.substring(this.pointer, this.pointer + 2);
this.pointer += 2;
return parseInt(value, 16);
}
num (max) {
if (max <= 0) {
throw new Error('max of random.num must be positive.');
}
else if (max <= 256) {
return this.byte() % max;
}
else {
throw new Error('not supported.');
}
}
fraction () {
return this.byte() / 256;
}
}
function test_random_class_byte () {
var a = [68, 9, 150, 66, 71, 184, 42, 152,
84, 31, 148, 195, 79, 121, 253, 235,
87, 142, 108, 87, 64, 95, 18, 186,
184, 92, 200, 43, 179, 155, 117, 136,
209, 241, 173, 107, 190, 11, 178, 50];
var r = new Random('yurina');
for (var i = 0; i < a.length; i++) {
if (r.byte() !== a[i]) {
throw new Error('test_random_class_byte');
}
}
}
function test_random_class_num () {
var a = [0, 1, 0, 2, 1, 4, 0, 0, 3, 1, 5, 3, 1, 9, 13, 11];
var r = new Random('yurina');
for (var i = 0; i < a.length; i++) {
if (r.num(i + 1) !== a[i]) {
throw new Error('test_random_class_num');
}
}
}
次回は敵キャラクターと満腹度について考えたいと思います。