JavaScriptで2Dドット絵ジャンプアクションゲームを作ろう(完成)〜マリオに愛を込めて

ゲーム

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

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

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

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

完成したゲーム

下記のページにありますので遊んでみてください。

先に読んでおく記事

ソースコード

GitHub - yenshan/goggle_jumper_chronicles
Contribute to yenshan/goggle_jumper_chronicles development by creating an account on GitHub.

ファイル構成は下記のとおりです。太字は前回から追加したファイルです。

.
├── Block.js
├── Chara.js
├── GObject.js
├── GamePoint.js
├── PBlock.js
├── Pipe.js
├── Snail.js
├── SpriteSheet.js
├── UserInput.js
├── World.js
├── assets
│   ├── background.png
│   ├── chara_spritesheet.png
│   ├── gamefont.png
│   ├── pow_spritesheet.png
│   ├── smallnumfont.png
│   ├── snail_spritesheet.png
│   ├── stages.json
│   ├── style.css
│   ├── tilesheet.png
│   └── title.png
├── index.html
└── index.js

この記事で説明すること

前回の記事ではすでに敵キャラ(Snail)も登場したので、ゲームの基本ギミックはすべて入っていました。あとは下記のゲームの構成要素を導入します。

・スコアの表示(8×8ドットのフォントの表示)
・敵を蹴落とした点数フォントの表示(4×6ドットのフォント)
・プレイヤーが死ぬ(ゲームオーバー処理)
・ステージの導入
・画面状態の遷移

小粒ですがゲームとしては完成です。

ドッド・フォントの表示

このゲームの描画の解像度はかなり小さいです。8×8ドットのマスをベースにして構築して、36×32マスの世界で描画しています。さらにその一部の34×28マス分だけをクリップして拡大表示しています。

この小さい解像度を実際表示する際にブラウザの表示領域いっぱいに拡大しています。

ゲーム上に表示する文字は、JavaScriptでも使用できるプロポーショナルフォントを使ってもOKなのですが、8×8ドットをベースにすることでドット絵の世界観を合わせています。

フォントはpngデータを用意し、これをスプライトシートとして、各文字は8×8あるいは4×6のスプラウトとして描画します。

8×8ドッドの文字フォント

画像は黒背景になってますが実際の背景は透明色です。

gamefont.png

4×6ドットの文字フォント

画像は黒背景になってますが実際の背景は透明色です。

smallnumfont.png

文字を表示させるコードは下記のとおりです。

const fontsheet = new SpriteSheet(8,8,"./assets/gamefont.png");
const fontchars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ:-=";

function charIndex(c) {
    let idx = fontchars.indexOf(c);
    return idx >= 0? idx : fontchars.lengh;
}

function putchar(c,x,y) {
    drawSprite(fontsheet, charIndex(c), x, y);
}

export function print(str ,x ,y) {
   for (let i = 0; i < str.length; i++) {
       putchar(str[i], x + 8*i, y);
   }
}
・・・・
const numfonts = new SpriteSheet(4,6,"./assets/smallnumfont.png");

export function print_small_num(str, x, y) {
   for (let i = 0; i < str.length; i++) {
       numfonts.drawSprite(context_bg, charIndex(str[i]), x + 4*i, y);
   }
}

例えばゲームスコアの表示は下記のように print() 関数を使います。

        print(`SCORE:${padNumber(world.score, 6)}`, 2*8, 3*8);

一方でSnailを蹴落とした時にも得点が表示されます。(「800」や「1000」)。

こちらは4×6ドットの方のフォントを使います。この表示は一定時間経つと消えるので、GamePointクラスを作って管理しています。表示はprint_small_num() を使います。

            print_small_num(`${this.points}`, this.x, this.y);

プレイヤーと敵の接触(ゲームオーバー処理)

プレイヤーが、Snailがひっくり返っている時に触れた場合は、Snailを蹴落とします。が、それ以外はプレイヤーの方が死にます。

(1) 全Snailが攻撃できる状態ならプレイヤーに攻撃しかける

プレイヤーがSnailを攻撃する場合の判定は前回の記事を参照してください。Snailがプレイヤーを攻撃する方は、全Snailをまわしてチェックします。

・・・
        for (let e of this.enemy_list) {
            e.affectForce(0, GRAVITY);
            e.update();
            if (e.offensive()) {
                this.player.attack(e)
            }

Snail.jsのoffensive()は攻撃できる状態かどうかを判断して返します。

    offensive() {
        return this.state == State.STOP || this.state == State.MOVE_LEFT || this.state == State.MOVE_RIGHT;
    }

(2) attack() されたプレイヤーは当たったかどうかを判定する

当たりならDEAD状態に移行する。

    attack(src) {
        if (this.state == State.DEAD || this.state == State.FALL_END)
            return;
        if (this.hit(src)) {
            this.vx = 0;
            this.vy = 0;
            this.change_state(State.DEAD);
        }
    }

DEAD状態になった後の状態遷移は下記の図のとおりです。

(3) プレイヤーが死んでいる状態なら、ゲーム状態を遷移する

一方で、Worldクラスではプレイヤーが死んだかどうかをupdateの周期で毎回チェックしています。Playerクラスのis_deading()は DEAD か FALL_END かを見ています。

Worldクラスはゲーム中の状態の管理をしていて、プレイヤーが死んでいる状態なら画面全体を止めたいのでPLAYER_FALLに遷移します。

    update_game_run() {
    ・・・
        if (this.player.is_deading()) {
            this.state = State.PLAYER_FALL;
            return;
        }
・・・

ゲーム状態の管理

このゲームは大きく3つの状態があります。

TITLEタイトル表示画面。
’s’キーを押すとゲームが始まる。
GAMEゲーム動作状態。
GAME_OVERプレイヤーが死んだ状態。
しばらくするとタイトル画面に戻る。

ゲーム動作の中にはさらに細かい状態に分かれます。

START_STAGEステージ開始のメッセージを表示する。
GAME_RUNゲーム動作状態。
STAGE_CLEAR敵キャラを全部倒した状態。次のステージの準備をしてSTART_STAGEに遷移する。
PLAYER_FALLプレイヤーが敵キャラに触れて死んで落ちる状態。
GAME_OVERプレイヤーが画面の外に落ちた状態。

状態遷移図にすると下記のようになります。

大きな状態は index.js で管理し、GAME中のさらに細かい状態は World.js で管理しています。実装コードはそれぞれのupdate() 内でswitch分で状態を見て処理を分けている部分になります。

・・・
function update() {
    switch(state) {
    case State.TITLE:
      ・・・
        break;
    case State.GAME:
      ・・・
        break;
    case State.GAME_OVER:
      ・・・
        break;
    }
    requestAnimationFrame(update);
}
・・・
    update() {
        switch(this.state) {
        case State.START_STAGE:
           ・・・
            break;
        case State.GAME_RUN:
           ・・・
            break;
        case State.STAGE_CLEAR:
           ・・・
            break;
        case State.PLAYER_FALL:
           ・・・
            break;
        case State.GAME_OVER:
            // do nothing
            break;
        }
    }

まとめ

4回かけて、「マリオブラザーズ」風の2Dジャンプアクションゲームの作り方を説明してきました。マリオの基本的なゲームのギミックはすべて入っており、ゲームの状態遷移やスコア表示もあり、小粒ながら完成したゲームになっています。

あとは、敵キャラの種類を増やしたり、床を凍らせたりのギミックを追加したりするなど、ステージが進んでも飽きがこない工夫を入れていく感じです。うまくアニメーションさせれば、動く床などの実現も割と簡単です。

ソースはすべてオープンソースとして公開していますので、気に入った人は色々いじって好きなように改造してみてください。なお、ソースコードはMITライセンスですが、assets/以下の画像ファイルはMITライセンスではありません。個人で楽しむ範囲にとどめてください。

Enjoy it!

コメント

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