押しものパズルの作り方 – 倉庫番風のパズルを作ってみよう(後編)

ゲーム

ゲームエンジンやライブラリを使わずに、素のHTMLとJavaScriptだけでゲームを作ります。

以下のような人のためにあります。

ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは面倒くさい。

ある程度プログラミングの知識がある人を対象にしています。

先に読んでおく記事

倉庫番の基本ロジックの作成は前編で説明しています。先に読んでおくことをお勧めします。

この記事で作るもの

前編の四角と丸の表示からドット絵に変えたものです。これだけでも随分、雰囲気が良くなりました。
オリジナルにない要素として「ドア」があります。荷物をすべて所定の場所に置いたらドアが開きます。

ちょっと、本家にそっくりすぎたので、動くバージョンは公開しません。
記事を読んで完成版は自分で作ってみてください。

ソースコード

前編からの主な差分のみを掲載します。

const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
context.imageSmoothingEnabled = false;

const SCREEN_W = 20
const SCREEN_H = 15
const BLOCK_SIZE = 32

const Const = {
    NONE: 0,
    WALL: 1,
    BOX: 2,
    POINT: 3,
    PLAYER: 4,
    DOOR: 5,
    GOAL: 6,
}

const PlayerDir = {
    LEFT: 0,
    RIGHT: 1,
    UP: 2,
    DOWN: 3,
}

class Obj {
    constructor(x,y) {
        this.x = x;
        this.y = y;
    }
}

const stages = [
// 省略
];

let stage_i = 0;
let map;

let player = new Obj(0,0);
let player_dir = PlayerDir.LEFT;

let box_list = [];


// 入力ハンドラー
document.addEventListener('keydown', keyDownHandler, false);

function keyDownHandler(event) {
    if (event.key === 'i') { // Up
        movePlayer(0, -1);
        player_dir = PlayerDir.UP;
    } 

    if (event.key === 'm') { // Down
        movePlayer(0, 1);
        player_dir = PlayerDir.DOWN;
    }

    if (event.key === 'j') { // Left
        movePlayer(-1, 0);
        player_dir = PlayerDir.LEFT;
    }

    if (event.key == 'l') { // Right
        movePlayer(1, 0);
        player_dir = PlayerDir.RIGHT;
    }

    if (event.key == 'r') { // Right
        init();
    }
}

// ・・・・省略

function drawSprite(sprite_no, x, y) {
    context.drawImage(spriteSheet, sprite_no*8, 0, 8, 8, x*BLOCK_SIZE, y*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}

function drawWall(x, y) {
    drawSprite(1, x, y);
}

function drawDoor(x, y) {
    drawSprite(3, x, y);
}

function drawPoint(x, y) {
    drawSprite(2, x, y);
}

function drawPlayer() {
    drawSprite(4 + player_dir, player.x, player.y);
}

function drawBox() {
    for (const item of box_list) {
        drawSprite(0, item.x, item.y);
    }
}

function drawScreen() {
    for (j = 0; j < SCREEN_H; j++) {
        for (i = 0; i < SCREEN_W; i++) {
            obj = map[ i + j*SCREEN_W ];
            switch(obj) {
            case Const.WALL:
                drawWall(i, j); break;
            case Const.POINT:
                drawPoint(i, j); break;
            case Const.DOOR:
                drawDoor(i, j); break;
            default:
                break;
            }
        }
    }
}

function isGameClear() {
    for (const item of box_list) {
        if (map[item.x + item.y * SCREEN_W] != Const.POINT) {
            return false;
        }
    }
    return true;
}

function init() {
    map = Array.from(stages[stage_i]);
    player_dir = PlayerDir.LEFT;
    box_list = [];
    for (y = 0; y < SCREEN_H; y++) {
        for (x = 0; x < SCREEN_W; x++) {
            obj = map[x + y*SCREEN_W];
            switch (obj) {
            case Const.PLAYER:
                player.x = x;
                player.y = y;
                break;
            case Const.BOX:
                box_list.push(new Obj(x,y));
                break;
            }
        }
    }
}

function openTheDoor() {
    for (i = 0; i < SCREEN_W*SCREEN_H; i++) {
        if (map[i] == Const.DOOR)
            map[i] = Const.NONE;
    }
}

function update() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    drawScreen();
    drawBox();
    drawPlayer();
    if (isGameClear()) {
        openTheDoor();
    }
    if (hitObj(player.x, player.y, Const.GOAL)) {
        stage_i = (stage_i+1) % 2;
        init();
    }
}

// スプライトシートのロード
const spriteSheet = new Image();
spriteSheet.src = "./spritesheet.png";

init();

setInterval(update, 10);


ドット絵を表示する方法

まずは、ドット絵のpngファイルを作成します。ピクセル編集できるグラフィックスエディターが色々でていますが、筆者はFirealpacaを使っています。

用意したのは以下の画像データです。実際の画像サイズはかなり小さいので、ブログ用に拡大表示しています。背景のグレーの部分は、実際は透明色指定です。

ひとつの8×8ピクセルの画像をスプライトと呼びます。プログラムの中では、このスプライトの番号を指定して描画するように関数を作ります。

この画像を spritesheet.png と言う名前で、index.js と同じフォルダーに保存します。

そして、JavaScriptからは png ファイルをロードします。

// スプライトシートのロード
const spriteSheet = new Image();
spriteSheet.src = "./spritesheet.png";

これにより、spriteSheetという名前のImageオブジェクトのインスタンスに画像データが保存されます。

イメージデータの描画は context.drawImage() 関数を使います。仕様は下記の通りです。

context.drawImage(
                image,      // 画像オブジェクト
                sx,         // 画像内で切り取るX座標
                sy,         // 画像内で切り取るY座標
                sWidth,     // 切り取る幅
                sHeight,    // 切り取る高さ
                tx,         // Canvas上のX座標
                ty,         // Canvas上のY座標
                tWidth,     // 描画する幅
                tHeight     // 描画する高さ
            );

実際のゲームの中での処理は下記の関数です。

function drawSprite(sprite_no, x, y) {
    context.drawImage(spriteSheet, sprite_no*8, 0, 8, 8, x*BLOCK_SIZE, y*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}

やっていることは、spritesheet.png の画像データの中の特定の領域を切り取って、canvasに拡大描画しています。図示すると下記になります。

ゲーム作りの楽しいところ、面倒くさいところ

ゲームを作るにあたって、楽しい部分と面倒に感じる部分があります。どの作業が楽しくて、どの作業がつまらないかは人それれぞれだと思います。

筆者はゲームプログラミングは無性に楽しいのですが、それ以外の作業がとても面倒に感じます。グラフィックスを作る部分もそうです。正直、ひとつのキャラクタが8 x 8ピクセルの絵を描くのが忍耐力の限界です。それ以上は作る気が起きません。

とはいえ、しょぼいピクセル画であっても、あるのとないとでは天地の差があります。前回の四角と丸の世界からすると、だいぶゲームの雰囲気が出ています。

8 x 8ピクセルの世界でも悪くないな、と今回改めて思いました。

作った後の楽しみ方

さて、これまで倉庫番の作り方を見てきました。とてもシンプルなゲームなので作ること自体は難しくないと思います。

ただし、このゲームが面白いのは「面データ」です。ゲームシステムはパズルのルールですが、パズルそのものは「面データ」です。

自分で作って気にいったら、本家本元を買って遊ぶのが王道かと思います。

「倉庫番」が驚異的なのは、1980年代の発売から40年以上経っているのに、いまだに現役で売っているということです。このゲームは極めてシンプルですが、ゲーム史に残る名作と言えます。

著者は以前、このゲームの影響を受けたパズルゲームを2つ紹介しました。この2つも名作です。倉庫番を気に入った人は、こちらもお勧めです。


コメント

タイトルとURLをコピーしました