JavaScriptプログラミング

【ゲームサンプル】JavaScriptで作るリズムじゃんけん

JavaScript

こんにちは。ゲームプログラマーのメガネです。

「JavaScriptで脱初心者のために、少し複雑なゲームを作ってみたい」

という人を対象にしています。じゃんけんにタイミング要素を追加した、リズムじゃんけんというゲームを作りました。ゲームの作り方を解説します。

JavaScript初心者の人は、まずはこちらを見ていってください。

リズムじゃんけんを公開

ゲームを遊ぶ

上からじゃんけんボールが降ってきます。

画面下のラインとボール中央のラインが一致するタイミングで、「✊」「✌️」「🖐️」のどれかを選んでください。

ボールに「かって」と書かれていたらボールに勝つ手を選び、「まけて」と書かれていたら負ける手を、「あいこ」と書かれていたらあいこになる手を選びます。

ライン同士が一致するタイミングで押せたら高得点です。

段々難しくなっていきますので、高得点を目指してがんばってください!

スマホ等でフルスクリーンで遊びたい場合はこちら。

https://meganeprog.com/js-samples/rythm-janken/rythm-janken.html

ソースコード

ソースコード一式はGitHubで公開しています。ファイル名をクリックすればブラウザでも見れるので、気軽に覗いてください。

https://github.com/meganeprog/javascript_rythm_janken

ファイルの説明:

  • rythm-janken.html・・・ゲームの側のHTMLです。ほとんど空っぽです。
  • rythm-janken.js・・・このゲームのJavaScriptコードです。
  • framework.js・・・rythm-janken.jsで利用するフレームワークのソースコードです。

ソースコードの説明

自作の簡易フレームワーク

自作の簡易フレームワークを使用しています。以下の記事で紹介していますので、見ていってください。

じゃんけんボールをスポーン

こちらがボールをスポーンする関数です。

#spawnBall(context) {
    this.#time += context.deltaTime;
    this.#nextBallTimer -= context.deltaTime;
    if (this.#nextBallTimer < 0) {
        // じゃんけんボールの出現タイミング
        this.#nextBallTimer = 0;
        // レベルテーブルの time が 0 じゃないときは、一定時間おきにレベルアップする
        if (LEVEL_OF_DIFFICULTY_TABLE[this.#level].time != 0) {
            if (this.#time > LEVEL_OF_DIFFICULTY_TABLE[this.#level].time) {
                if (this.#balls.getChildCount() == 0) {
                    this.#time = LEVEL_OF_DIFFICULTY_TABLE[this.#level].time;
                    ++this.#level;
                }
                else {
                    return;
                }
            }
        }

        // 現在のレベル
        const level = LEVEL_OF_DIFFICULTY_TABLE[this.#level];
        // かちまけのルールを決める
        // 設定された割合に応じてランダムに決定
        const rand = Math.random() * (level.win + level.lose + level.draw);
        const rule = rand < level.win ? Rule.win : (rand < level.win + level.lose ? Rule.lose : Rule.draw);
        // じゃんけんボールの手をランダムに決める
        const hands = [Hand.rock, Hand.scissors, Hand.paper];
        const hand = hands[Math.floor(Math.random() * 3)];
        // 移動速度を設定
        const speed = this.#judgementSizes.good * level.speed;
        // じゃんけんボールをスポーン
        this.#balls.addChild(new Ball(rule, hand, speed, this.#judgementSizes.good, this.#judgementSizes.excellent));
        // スポーン間隔のタイマーをリセット
        this.#nextBallTimer = level.interval;
    }
}

JavaScriptの上の方でレベルテーブルを作成しています。

const LEVEL_OF_DIFFICULTY_TABLE = [
    {time:  7, interval: 3, speed:  3, win:1, draw:0, lose:0},
    {time:  14, interval: 3, speed:  4, win:0, draw:0, lose:1},
    {time:  21, interval: 3, speed:  5, win:0, draw:1, lose:0},
    {time:  30, interval: 2, speed:  5, win:1, draw:1, lose:1},
    {time:  40, interval: 2, speed:  7, win:1, draw:1, lose:1},
    {time:  50, interval: 1.5, speed: 8, win:1, draw:0, lose:0},
    {time:  60, interval: 1.5, speed: 8, win:0, draw:0, lose:1},
    {time:  70, interval: 1.5, speed: 8, win:0, draw:1, lose:0},
    {time:  80, interval: 1.7, speed: 10, win:1, draw:1, lose:1},
    {time:  90, interval: 1.2, speed: 3, win:1, draw:1, lose:1},
    {time: 100, interval: 1.4, speed: 8, win:1, draw:1, lose:1},
    {time:   0, interval: 1.2, speed: 10, win:1, draw:1, lose:1},
];

このテーブルの指示どおりに、じゃんけんボールを出現させるようにしています。

        // 現在のレベル
        const level = LEVEL_OF_DIFFICULTY_TABLE[this.#level];
        // かちまけのルールを決める
        // 設定された割合に応じてランダムに決定
        const rand = Math.random() * (level.win + level.lose + level.draw);
        const rule = rand < level.win ? Rule.win : (rand < level.win + level.lose ? Rule.lose : Rule.draw);
        // じゃんけんボールの手をランダムに決める
        const hands = [Hand.rock, Hand.scissors, Hand.paper];
        const hand = hands[Math.floor(Math.random() * 3)];
        // 移動速度を設定
        const speed = this.#judgementSizes.good * level.speed;
        // じゃんけんボールをスポーン
        this.#balls.addChild(new Ball(rule, hand, speed, this.#judgementSizes.good, this.#judgementSizes.excellent));
        // スポーン間隔のタイマーをリセット
        this.#nextBallTimer = level.interval;

一定時間おきにレベルアップしていきます。

        // レベルテーブルの time が 0 じゃないときは、一定時間おきにレベルアップする
        if (LEVEL_OF_DIFFICULTY_TABLE[this.#level].time != 0) {
            if (this.#time > LEVEL_OF_DIFFICULTY_TABLE[this.#level].time) {
                if (this.#balls.getChildCount() == 0) {
                    this.#time = LEVEL_OF_DIFFICULTY_TABLE[this.#level].time;
                    ++this.#level;
                }
                else {
                    return;
                }
            }
        }

じゃんけんボールの処理

こちらがじゃんけんボールのクラスです。

class Ball extends fw.GameObject {
    constructor(rule, hand, speed, radius, lineWidth) {
        super();
        this.#rule = rule;
        this.#hand = hand;
        this.#speed = speed;
        this.#radius = radius;
        this.#lineWidth = lineWidth;
        // ルールによって色を変える
        if (rule == Rule.win) {
            this.#color = "indianred";
        }
        else if (rule == Rule.lose) {
            this.#color = "royalblue";
        }
        else {
            this.#color = "mediumseagreen";
        }
        this.#y = -radius;
    }

    onUpdate(context) {
        this.#y += context.deltaTime * this.#speed;
    }

    onRender(context) {
        const x = Math.floor(context.canvas.width / 2);
        const y = Math.floor(this.#y);
        this.#x = x;
        context.beginPath();
        context.arc(x, y, this.#radius, 0 * Math.PI / 180, 360 * Math.PI / 180, false);
        context.fillStyle = this.#color;
        context.fill();
        context.fillStyle = "rgb(255, 255, 255)";
        context.fillRect(x-this.#radius, y - this.#lineWidth / 2, this.#radius*2, this.#lineWidth);
        context.font = `${Math.floor(this.#radius/3)}pt sans-serif`;
        context.textAlign = "center";
        context.textBaseline = "middle";
        context.fillText(this.#rule, x,  y - this.#radius / 2);
        context.font = `${Math.floor(this.#radius/2)}pt sans-serif`;
        context.textAlign = "center";
        context.textBaseline = "middle";
        context.fillText(this.#hand, x,  y + this.#radius / 2);
    }

    get x() {
        return this.#x;
    }

    get y() {
        return this.#y;
    }

    get radius() {
        return this.#radius;
    }

    get hand() {
        return this.#hand;
    }

    get rule() {
        return this.#rule;
    }

    #x = 100;
    #y = 100;
    #radius;
    #lineWidth;
    #rule;
    #hand;
    #color;
    #speed;
}

更新処理はシンプルです。単純に速度を縦方向に足していて、上から下に落としているだけです。

onUpdate(context) {
    this.#y += context.deltaTime * this.#speed;
}

ボタンが押されたときの判定

ボタンが押されたときの判定です。

「👊」「✌️」「🖐️」それぞれのボタンに、judgementというコールバックを仕込んでいます。ボタンが押されたらコールバックが呼ばれます。

this.addChild(new Button(this.controller.canvasWidth/4, this.controller.canvasHeight/10*9, this.#judgementSizes.good, Hand.rock, (hand) => { this.#judgement(hand); }));
this.addChild(new Button(this.controller.canvasWidth/4*2, this.controller.canvasHeight/10*9, this.#judgementSizes.good, Hand.scissors, (hand) => { this.#judgement(hand); }));
this.addChild(new Button(this.controller.canvasWidth/4*3, this.controller.canvasHeight/10*9, this.#judgementSizes.good, Hand.paper, (hand) => { this.#judgement(hand); }));

「かって」「まけて」「あいこ」の選択に成功していたら、ボールと判定ラインの距離に応じて「エクセレント」「グレート」「グッド」の評価になります。

選択に失敗していたらミスです。ミスの場合は体力を1減らしていて、体力が0になるとゲームオーバーです。

また、ボールが判定ラインよりも上にあるときには、ボタンが押されても無視しています。じゃないと厳しすぎますからね。

#judgement(hand) {
    if (this.#balls.getChildCount() > 0) {
        // 一番下のボールだけを判定
        let ball = this.#balls.getChild(0);
        // ボールと判定ラインの距離の差分
        const dy = this.#judgementLine - ball.y;

        if (dy > this.#judgementSizes.good) {
            // 判定距離よりも遠いので無視
        }
        else {
            // 成功したかどうか
            const success = this.#checkHand(ball.rule, hand, ball.hand);

            // 成功していてかつ、距離の差分がエクセレントの範囲内
            if (success && dy >= -this.#judgementSizes.excellent && dy <= this.#judgementSizes.excellent) {
                // エクセレント
                this.addChild(new Particle(ball.x, ball.y, ball.radius, "gold"));
                this.addChild(new ExcellentEffect());
                ball.removeSelf();
                this.#score += this.#points.excellent;
            }
            // 成功していてかつ、距離の差分がグレートの範囲内
            else if (success && dy >= -this.#judgementSizes.greate && dy <= this.#judgementSizes.greate) {
                // グレート
                this.addChild(new Particle(ball.x, ball.y, ball.radius, "silver"));
                this.addChild(new GreateEffect());
                ball.removeSelf();
                this.#score += this.#points.greate;
            }
            // 成功していてかつ、距離の差分がグッドの範囲内
            else if (success && dy >= -this.#judgementSizes.good && dy <= this.#judgementSizes.good ) {
                // グッド
                this.addChild(new Particle(ball.x, ball.y, ball.radius, "cadetblue"));
                this.addChild(new GoodEffect());
                ball.removeSelf();
                this.#score += this.#points.good;
            }
            else {
                // ミス
                ball.removeSelf();
                this.addChild(new MissEffect());
                if (--this.#life == 0) {
                    // ゲームオーバー
                    this.controller.pushScene(new GameOverScene(this.controller));
                }
            }
        }
    }
}

ボタンを押せずに行きすぎてしまったときの判定

落下しすぎたボールがあれば、体力を1減らしてミスにします。体力が0になったらゲームオーバーです。

    // 行き過ぎたものを削除する
    this.#balls.childForEach((ball) => {
        if (ball.y > this.controller.canvasHeight/5*4 + this.#judgementSizes.good) {
            this.addChild(new MissEffect());
            ball.removeSelf();
            if (--this.#life == 0) {
                // ゲームオーバー
                this.controller.pushScene(new GameOverScene(this.controller));
            }
        }
    });

演出

成功・失敗した際に、演出を入れています。

成功したときにParticleクラスを生成して、ボタンから円を放射しています。

ExcellentEffectは「エクセレント」という文字を、GreateEffectは「グレート」、GoodEffectは「グッド」と表示しています。

ミスのときはMissEffectです。

パッと作るために、似たようなクラスがいっぱいできてしまったのは反省点です。

まとめ

JavaScriptで作る少し複雑なゲーム「リズムじゃんけん」の作り方を紹介しました。

レベルテーブルを変更することで、難易度を変えることができます。色々いじって遊んでもらえるとうれしいです。

コメント