TypeScriptでゲームプログラム8「球のクラス化と処理改善」

球をクラス化する

ラケットをクラス化したときにRectクラスを使ったように、球をクラス化するにはCircleクラスを使います。

/**
 * 球を表すクラス
 */
class Ball {
    /**
     * 球本体の円
     */
    private _body: Circle;

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

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

    /**
     * コンストラクタ
     */
    constructor() {
        // 球のサイズ10で宣言
        this._body = new Circle(0, 0, 10);
    }
}

球には速度の情報もあるので、_VX、_VYで管理します。

Racketのときと同じようにInit()、 DrawCanvas()メソッドを追加し、初期化処理と描画処理を行います。

class Ball {
    /**
     * ゲーム開始時の初期化処理
     */
    public Init() {
        // フレームカウンターをランダム値として使う。
        let random = Random(1, 18);     // 1~18までのランダム値

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

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

処理内容は従来(TypeScriptでゲームプログラム6のソースコードの279-283行目と379-387行目)と同じです。

球の状態更新を改善する

球の状態更新の処理は、今回少し変更しました。というのも、デバッグ中に球が跳ね返る時の動き方に少し違和感を感じだからです。変更点は複数あります。

変更点1:マジックナンバーを減らす

球のY軸速度を増やすときに"0.25"という数値を使っていましたが、これをいったん変数にセットし、Y軸速度を増やすときにはその変数を使うようにしました。Y軸速度に0.25を足していたのは、球が「重力によって放物線を描くように移動する」のを再現するためなのだから、変数名はgravityにしました。

"0.25"の様な数値を(説明もなく)使うのはマジックナンバーといって、一般的に良くないコーディングです。意味のある名前の変数や定数に置き換えた方が良いのです。

変更点2:速度と座標の変更タイミング

Y軸の座標と速度を更新するタイミングを変更しました。

変更前:Y軸座標に速度を加算し、速度に0.25(gravity)を加算する
変更後:速度に0.25(gravity)を加算し、Y軸座標に速度を加算する

結果としてY座標の値は、これまでよりも早く下に移動するようになっています。

変更点3:状態更新の回数を増やす

forループを使って、球の座標や速度の更新処理を10回実行するようにしました。そして、forループの1回の中では座標や速度の更新は従来の1/10にしています。

つまり、以下のような違いがあります。

// 変更前
this._VY += gravity;
this._body.X += this._VX;
this._body.Y += this._VY;

// 変更後
let N = 10;
for ( let i = 0 ; i < N ; i++){
   this._VY += gravity / N;
   this._body.X += this._VX / N;
   this._body.Y += this._VY / N;
}

X座標の移動距離は変更前と変更後に差異はありません。しかし、Y座標は速度が変化するため、若干違ってきます。ラケットや壁との衝突判定も同様にforループの中で行い、細かく判定します。

変更前のコードでは球の移動量が大きく、ラケットの衝突処理と左右の壁に衝突処理が同時に起きてしまいやすいです。そのため、本来なら「壁に当たる→ラケットに当たる」という移動を球がしているはずなのに、「ラケットに当たる→壁に当たる」と処理されてしまうことがありました。このため結果的に跳ね返りの挙動がおかしくなっていました。

変更したコードでは、球は少しづつ移動するため、ラケットと左右の壁の衝突処理が同時には発生しにくくなり、跳ね返り処理が改善されました。それでも上述の異常挙動を完全に0にはできていませんが、少しづつ移動するため(人間が見る範囲では)挙動がおかしく見えることは減っています。

挙動がおかしくなる可能性は、Nが大きくなるほど低くなりますが、ループ回数が増えることで処理が重たくなってしまいます。今回はいくつかNを変えて試行した結果、10にしています。

変更点4:反転処理の単純化

変更前のコードでは、球が壁やラケットにめり込んだ分を補正するために、以下のような処理を行っていましたが、これを削除しました。

// 変更前の「左壁に当たったときの補正処理」
x = left + left - x;

変更点3により、球を少しづつ移動するので、球がめり込んで移動する距離は短くなりました。補正処理をしなくても、無視できる(違和感がない)レベルになったので、補正処理を行わないようにした。

変更点5:衝突時に速度を考慮する

変更前は衝突のチェックは座標だけで行っていましたが、速度も考慮するようにしました。つまり、

変更前の左壁との衝突条件:球の座標が左の壁より左に位置している。
変更後の左壁との衝突条件:球の速度は左向き、かつ、球の座標が左の壁より左に位置している。

のように、条件を変更しました。

変更点4により、球が「めり込んだ時の補正」をしないように変えたことで、座標だけでチェックしていると衝突を誤判定することがあったので、それを防ぐために追加しました。

以上の変更を加えたコードを以下に示します。

/**
 * 球の移動計算で使う、重力加速度
 */
const gravity = 0.25;

class Ball {
    /**
     * 状態更新
     */
    public Update() {
        if (Game.Status == "game") {
            let N = 10;
            for (let i = 0; i < N; i++) {

                // 速度を更新する。(下向きに加速する)
                this._VY += gravity / N;

                // 衝突を考慮せずに、座標と速度を更新する。
                this._body.X += this._VX / N;
                this._body.Y += this._VY / N;

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

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

                // 天井との衝突処理
                if (this._VY < 0 && this._body.Top < Area.Top) {
                    // Y座標速度を半分にして反転する。
                    this._VY *= -0.5;
                    // 得点を加算
                    Score += 3;
                }

                // 床との衝突処理
                if (this._body.Bottom > Area.Bottom) {
                    Game.Status = "gameover";
                    ChangeButton1Text("NEXTゲーム");
                    ChangeMessage(
                        "ゲームオーバー\n" +
                        "あなたのスコアは" + Score + "でした。\n" +
                        "ボタンを押すと次のゲームを開始します。"
                    );
                }
            }
        }
    }

    /**
     * ラケットとの衝突処理
     */
    private _CollisionRachket() {
        let racket_rect = Racket1.Rect;

        // 球がラケットに当たるとき、以下の条件を満たす。
        // - 球が下向きに移動している
        // - 球の最も下(_body.Bottom)がラケットの上面(racket_rect.Top)より下になる
        // - 球のX座標がラケットの左右の間にある。
        if (this._VY > 0 && racket_rect.Top < this._body.Bottom) {
            if (racket_rect.Left <= this._body.X && this._body.X <= racket_rect.Right) {
                // ラケットの幅を5つに分割し、球が当たった位置に応じて跳ね返り方を変える。
                // 1.ラケットの左側10%の範囲に当たったとき
                //   Y軸の速度を0.8倍にして反転する。
                //   X軸の速度を左向きに5~10にする。
                // 2.ラケットの右側10%の範囲に当たったとき
                //   Y軸の速度を0.8倍にして反転する。
                //   X軸の速度を右向きに5~10にする。
                // 3.ラケットの中央40%の範囲に当たったとき
                //   Y軸の速度を1.2倍にして反転する。
                //   X軸の速度が5を超えるときは、1つ減らす。
                // 4.上記以外
                //   Y軸の速度を0.9倍にして反転する。
                let dx = (this._body.X - racket_rect.Left) / racket_rect.Width;
                if (dx < 0.1) {                     // 左側10%
                    this._VY *= -0.8;
                    this._VX = Random(-10, -5);     // 左向きに5~10
                }
                else if (0.9 < dx) {                // 右側10%
                    this._VY *= -0.8;
                    this._VX = Random(5, 10);       // 右向きに5~10
                }
                else if (0.3 <= dx && dx <= 0.7) {  // 中央40%
                    this._VY *= -1.2;
                    if (this._VX > 5) { this._VX--; }
                    else if (this._VX < -5) { this._VX++; }
                }
                else {
                    this._VY *= -0.9;
                }
            }
        }
    }
}

コメント