0
0

More than 3 years have passed since last update.

ローグライクゲームを作ってみるその6 ゲームメッセージとプレイヤーのステータス

Last updated at Posted at 2019-12-19

過去記事一覧

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

プログラム

今回作ったプログラムは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);
    });

メッセージが表示されたゲーム画面は下のようになります。

01.jpg

プレイヤーのステータスの描画

次にプレイヤーのステータスを描画します。

と言っても、現在プレイヤーの状態として存在する値は、player変数に格納されるdepthxyのみです。

この中でxyは敢えてゲーム画面に表示する必要はありませんが、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();

これでプレイヤーのいる階がゲーム画面に表示されるようになりました。

ゲーム画面は下のようになります。

02.jpg

今回はここまで

今回はここまでです。

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');
        }
    }
}

次回は敵キャラクターと満腹度について考えたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0