ブロックくずしを作る4「ラケットと球」

ラケットを追加する

ゲームエリアができたので、ラケットを追加しましょう。

ラケットは、「TypeScriptでゲームプログラム」のRacketクラスを流用します。

import { IGameCanvasPart } from './game/GameCanvasPart';
import { Rectangle } from './shape/Rectangle';
import { GameMng } from './GameMng';
import { GameParameter } from './GameParameter';
/**
 * ラケットを表すクラス
 */
export class Racket implements IGameCanvasPart {
    /**
     * ラケット本体の四角
     */
    private _body = new Rectangle(0, 0,
        GameParameter.Racket.Width, GameParameter.Racket.Height);

    /**
     * ラケット本体の四角
     */
    public get Rect() { return this._body; }

    /**
     * コンストラクタ
     */
    constructor(private _game: GameMng) {
    }

    /**
     * ゲーム開始時の初期化処理
     */
    public Init() {
        let gameArea = this._game.Area;
        if (gameArea === undefined) {
            throw new Error("gameArea が未定義です。");
        }

        // X座標:初期値はGameParameter.Areaの横幅の真ん中
        this._body.X = gameArea.X;
        // Y座標:初期値はGameParameter.Areaの下辺からラケットの縦幅の半分だけ上
        this._body.Y = gameArea.Bottom - this._body.Height / 2
    }

    /**
     * 状態更新
     */
    public Update() {
        let gameArea = this._game.Area;
        if (gameArea === undefined) { return }

        let left = gameArea.Left + this._body.Width / 2;
        let right = gameArea.Right - this._body.Width / 2;

        // ラケットの移動先と現在値との距離
        let distance = this._game.Canvas.Mouse.X - this._body.X;
        // 距離に応じて、X軸の変化速度を変更する。
        let vx = distance / 5;

        // ラケットを移動する。
        let x = this._body.X + vx;
        if (x < left) {
            x = left;
        }
        else if (x > right) {
            x = right;
        }
        this._body.X = x;
    }

    /**
     * CANVASへ描画する
     * @param context 
     */
    public DrawCanvas(context: CanvasRenderingContext2D) {
        context.fillStyle = GameParameter.Racket.Color;
        context.fillRect(this._body.Left, this._body.Top, this._body.Width, this._body.Height);
    }
}

ゲームエリアの情報を「TypeScriptでゲームプログラム」ではGamaParameter.Areaから取得していましたが、今回は_game.Areaから取得するようになったので、置き換えています。

また、ゲームのスコアに応じてラケットの横幅を小さくしていく処理は、今の時点では外しています。(今後、ゲームの難易度調整をどうするのかを考えたあとで、復活するかもしれませんが完全に同じになるとは限りません。)

_game.Areaがundefinedになるときの扱い

_game.Areaはundefinedになっていることもあります。それはキャンバスの情報にアクセスできるようになる前です(「ブロックくずしを作る3」を参照)。_game.Areaがundefinedの時にどうするのかは、処理処理の内容に応じて考えるべきでしょう。

基本的には、ゲームエリアの情報が必要になる処理は、キャンバス情報にアクセスできるようになってから実行するはずです。そのため、_game.Areaがundefinedのときは処理をスキップします。例えば、Update()はこれに該当します。Update()はIGameCanvasPartを継承したメソッドなので、「キャンバスに描画するゲーム部品」に対する処理です。キャンバス情報にアクセスできるようになってからコールされるはずです。

Init()はthrowすることにしました。Init()はキャンバス情報にアクセスできるようになってからコールされるという確実性が(Update()などと比べると)がありません。そのため、Init()を不適切なタイミングでコールするというバグを(デバッグ時に)発見しやすくなります。

パラメーターをGameParameterに移動

ラケットの横幅や縦幅、色などを直接コーディングするのではなく、GameParameterに書いたそれを参照するようにしました。

球を追加する

球も同様に、流用します。

import { IGameCanvasPart } from './game/GameCanvasPart';
import { Circle } from './shape/Circle';
import { GameMng } from './GameMng';
import { GameParameter } from './GameParameter';
/**
 * 球を表すクラス
 */
export class Ball implements IGameCanvasPart {
    /**
     * 球本体の円
     */
    private _body = new Circle(0, 0, GameParameter.Ball.Size);

    /**
     * X座標の速度
     */
    private _VX = 0;

    /**
     * Y座標の速度
     */
    private _VY = 0;

    /**
     * コンストラクタ
     */
    constructor(private _game: GameMng) {
    }

    /**
     * ゲーム開始時の初期化処理
     */
    public Init() {
        let gameArea = this._game.Area;
        if (gameArea === undefined) {
            throw new Error("gameArea が未定義です。");
        }

        // フレームカウンターをランダム値として使う。
        let random = this._game.Random(1, 18);     // 1~18までのランダム値

        // X座標:初期値はフレームカウンタに応じて、GameParameter.Areaの横幅の0.1~0.9の位置にランダムに変化する。
        let random2 = 0.1 * (1 + (random % 9)); // randomから0.1~0.9まで0.1刻みの値を作る。
        this._body.X = gameArea.Left + gameArea.Width * random2;
        // Y座標:初期値はGameParameter.Areaの上辺から球の半径だけ下
        this._body.Y = gameArea.Top + this._body.Radius;
        // X軸速度:初期値はランダム値が奇数なら-5、偶数なら5に変化する。
        this._VX = (random & 1) ? 5 : -5;
        // Y軸速度:初期値は5
        this._VY = 5;
    }

    /**
     * 状態更新
     */
    public Update() {
        let gameArea = this._game.Area;
        if (gameArea === undefined) { return }
        if (this._game.Status == "game") {
            let N = 10;
            for (let i = 0; i < N; i++) {
                // 衝突を考慮せずに、座標と速度を更新する。
                this._body.X += this._VX / N;
                this._body.Y += this._VY / N;

                // ラケットとの衝突処理
                this._CollisionRachket();

                // 左右の壁との衝突処理
                if (this._VX < 0 && this._body.Left < gameArea.Left) {
                    // X座標速度を反転する。
                    this._VX = -this._VX;
                }
                else if (this._VX > 0 && this._body.Right > gameArea.Right) {
                    // X座標速度を反転する。
                    this._VX = -this._VX;
                }

                // 天井との衝突処理
                if (this._VY < 0 && this._body.Top < gameArea.Top) {
                    // Y座標速度を半分にして反転する。
                    this._VY = -this._VY;
                }

                // 床との衝突処理
                if (this._body.Bottom > gameArea.Bottom) {
                    this._game.Status = "gameover";
                }
            }
        }
    }

    /**
     * ラケットとの衝突処理
     */
    private _CollisionRachket() {
        let racket_rect = this._game.Racket.Rect;
        if (this._VY > 0 && racket_rect.Top < this._body.Bottom) {
            if (racket_rect.Left <= this._body.X && this._body.X <= racket_rect.Right) {
                this._VY = -this._VY;
            }
        }
    }

    /**
     * CANVASへ描画する
     * @param context 
     */
    public DrawCanvas(context: CanvasRenderingContext2D) {
        context.fillStyle = GameParameter.Ball.Color;
        context.beginPath();
        context.arc(this._body.X, this._body.Y, this._body.Radius, 0, 2 * Math.PI);
        context.closePath();
        context.fill();
    }
}

_game.Areaの取り扱いや、球のサイズと色をGameParameter記載するように変更したことなどはラケットと同様です。

Circleは円を扱うクラスです。クラス定義の詳細は以下を見てください。

その他に、以下の変更を加えています。

球の加速処理を削除

TypeScriptでゲームプログラム」では、重力に見立てた下向きの加速をしており、球が放物線を描くように移動していました。しかし、この動きはブロックくずしのイメージにはあわないように感じます。

そこで、加速処理を削除し、球が直線状に移動するようにしました。

跳ね返り処理

球が天井(ゲームエリアの上辺)に当たったときやラケットに当たったときの跳ね返り処理を単純にY軸方向速度を逆にするようにしました。今後ブロックとの衝突処理も追加しなくてはならないので、いったん単純化しておいます。(将来的には跳ね返りについては、もう少しバラエティを持たせたいと考えています。)

点数

「球が天井や左右の壁に当たったときに点数を増やす」という処理を行っていましたが、このような点数のつけ方はブロックくずしというゲームには合いません。いったん点数に関するすべての処理を削除しました。

GameParameterの変更

ラケットや珠で使用するパラメーターを追加しています。追加した部分はハイライトにしています。

/**
 * ゲームパラメーター
 */
export const GameParameter = {
    /**
     * ゲームキャンバスの幅の最小値
     */
    CanvasMinWidth: 40,
    /**
     * ゲームキャンバスの高さの最小値
     */
    CanvasMinHeight: 60,

    /**
     * ラケットに関連するパラメータ
     */
    Racket: {
        /**
         * ラケットの横幅
         */
        Width: 40,
        /**
         * ラケットの縦幅
         */
        Height: 10,
        /**
         * ラケットの色
         */
        Color: "yellowgreen",
    } as const,

    /**
     * 球関連するパラメーター
     */
    Ball: {
        /**
         * 球のサイズ(半径)
         */
        Size: 5,
        /**
         * 球の色
         */
        Color: "yellow",
    } as const,
} as const;

ラケットと球をGameMngに追加する

ラケットと球のクラスができましたので、GameMngのプロパティに追加し、関連するコードを追加します。

import { GameLoop } from './game/GameLoop';
import { GameCanvas } from './game/GameCanvas';
import { GameCanvasPart } from './game/GameCanvasPart';
import { Rectangle } from './shape/Rectangle';
import { GameParameter } from './GameParameter';
import { Racket } from './Racket';
import { Ball } from './Ball';

/**
 * ゲームの状況を表すtype
 * start - ゲーム開始
 * stop - ゲーム停止中
 * game - ゲーム実行中
 * gameover - ゲームオーバー
 */
export type GameStatus_t = 'start' | 'stop' | 'game' | 'gameover';

export class GameMng extends GameLoop {
    /**
     * ゲームの状況
     */
    public Status: GameStatus_t = "game";

    /**
     * id="gameCanvas"のCANVAS
     */
    public Canvas = new GameCanvas("gameCanvas");

    /**
     * ゲームエリア
     */
    public Area: Rectangle | undefined;

    /**
     * 球
     */
    public Ball = new Ball(this);

    /**
     * ラケット
     */
    public Racket = new Racket(this);

    /**
     * コンストラクタ
     */
    constructor() {
        super();
        this._AfterReadyCanvas();

        // 部品を登録
        this.AddPart(this.Canvas);

        // CANVAS部品を登録
        let tmp = new GameCanvasPart();
        tmp.onDrawCanvas = context => {
            if (this.Area) {
                let TL = `${this.Area.Left},${this.Area.Top}`;
                let BR = `${this.Area.Right},${this.Area.Bottom}`
                let text = `ゲームエリアは、(${TL})-(${BR})`;
                context.strokeStyle = "yellow";
                context.strokeText(text, 10, 10);
                context.strokeStyle = "white";
                context.strokeRect(this.Area.Left, this.Area.Top, this.Area.Width, this.Area.Height);
            }
            else {
                let text = `ゲームエリアを設定できません。`;
                context.strokeStyle = "red";
                context.strokeText(text, 0, 12);
            }
        }
        this.Canvas.AddPart(tmp);
        this.Canvas.AddPart(this.Ball);
        this.Canvas.AddPart(this.Racket);
    }

    /**
     * ゲームの状態を初期化する
     */
    public InitSituation() {
        // 球を初期化する
        this.Ball.Init();

        // ラケットを初期化する。
        this.Racket.Init();
    }

    /**
     * ゲームキャンバスの準備ができてから行なう処理
     */
    private async _AfterReadyCanvas() {
        await this.Canvas.WaitUntil_GetHtmlElement();
        let width = this.Canvas.Width;
        let height = this.Canvas.Height;
        if (width > GameParameter.CanvasMinWidth && height > GameParameter.CanvasMinHeight) {
            this.Area = new Rectangle(20, 20, width - 20, height - 40);

            // ゲーム状態の初期化
            this.InitSituation();
        }
    }

    /**
     * Frameの値を使って、minとmaxの間の整数乱数値を得る。
     * @param min 
     * @param max 
     * @returns min以上、max以下の欄数値
     */
    public Random(min: number, max: number) {
        let frame = this.Frame ?? 0;
        if (max < min) {
            console.log('error: max:{max} min:{min}')
        }
        let diff = max - min + 1;
        return (frame % diff) + min;
    }
}

変更が入った部分をハイライトにしています。コードの上から順位に説明します。

statusプロパティの追加

class Ballでゲーム状態(GameMng.status)を参照するコードがあるので、GameMngにも追加しました。あわせて、type GameStatus_tの定義も追加しています。(9-22行目です。)

statusの初期値は本来は'start'なのですが、まだゲームの開始処理('start'->'game'に移行する処理)ができていないので、一時的に'game'にしています。

球とラケットの追加

34-42行目にプロパティーを追加しています。また、73-74行目でGame.Canvasに登録しています。

初期化処理の追加

77-86行目にゲーム状態の初期化処理のためのメソッドInitSituation()を追加しました。この中で球とラケットの初期化を行います。

TypeScriptでゲームプログラム」では、このメソッドをcontructorの最後にコールしていました。しかし、このゲームでは球やラケットの初期化はゲームエリアが決定した後で行う必要があります。そこでGame.Areaがセットされた後でコールするようにしています。(99行目です。)

Random()メソッドの追加

Ballクラスの中で乱数処理のために必要なので、追加しました。(103-116行目です。)

中身は「TypeScriptでゲームプログラム」とまったく同じです。

ここまでを実行する

ここまでを実際に実行すると下のようになります。今回は、ゲームキャンバスのサイズが変わるとゲームエリアが変わることを示すために、ゲームキャンバスを480x320にしています。

またスタート処理がないので、いきなりゲームが始まっています。ここまでを見ている間にゲームオーバーになっているかもしれません。(その場合はページを再読み込みしてください。)

SCORE:10000
メッセージ

コメント