TypeScriptでゲームプログラム6「バージョン1完成:全ソースコード」

最終的なソースコード

これまで作ってきたゲームのソースコードは以下の通りです。

/**
 * FPS(Frame per second)
 */
const FPS = 20;

/**
 * フレームカウンタ(ゲーム内の時刻カウンタ)
 * GameLoop()が最初にコールされたときを0とし、
 * 1/FPS秒を1フレームとしてカウントする。
 */
let FrameCounter: number | undefined;

/**
* GameLoop()が最初にコールされたときのtimestampを保持する。
*/
let GameLoopStartTime = 0;

/**
 * ゲームのループ処理
 * window.requestAnimationFrame()のコールバック
 * @param timestamp window.requestAnimationFrame()から渡されるタイムスタンプ
 */
function GameLoop(timestamp: number) {
    // 初めてコールされたときに、GameLoopStartTimeとFrameCounterを初期化
    if (FrameCounter === undefined) {
        FrameCounter = 0;
        GameLoopStartTime = timestamp;
    }

    // ゲームループ開始時と現在のtimestampの差から、フレーム数を計算
    let frame = Math.floor((timestamp - GameLoopStartTime) * FPS / 1000);
    if (frame > FrameCounter) {
        // フレームカウンタを更新
        FrameCounter = frame;

        // ゲームの状況を更新する。
        UpdateGameSituation();

        // フレーム数が更新されたので、描画を行う。
        DrawGameCanvas();
    }

    // ゲームの状況更新、再描画のために、requestAnimationFrame()に再度実行する。
    window.requestAnimationFrame(GameLoop);
}

/**
 * 球が移動できる範囲
 */
let Area = {
    X: 10,
    Y: 10,
    Width: 300,
    Height: 300,
}

/**
 * 画面に表示する球
 */
let Ball = {
    X: 10,   // X座標
    Y: 10,   // Y座標
    VX: 5, // X座標の速度
    VY: 0,  // Y座標の速度
    SIZE: 10, // 球のサイズ
};

/**
 * ラケット(弾を撃ち返すキャラクター)
 */
let Racket = {
    X: 0,           // X座標
    Y: 0,           // Y座標
    Width: 40,      // ラケットの横幅
    Height: 10,     // ラケットの縦幅
};

/**
 * ゲームのスコア(点数)
 */
let Score = 0;

/**
 * ゲームの状況を更新する
 */
function UpdateGameSituation() {
    UpdateRacket();
    UpdateBall();
    if (GameStatus == "game") {
        ChangeMessage(
            "スコア:" + Score
        );
    }
}

/**
 * ラケットの状態更新
 */
function UpdateRacket() {
    let left = Area.X + Racket.Width / 2;
    let right = Area.X + Area.Width - Racket.Width / 2;

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

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

    // ラケットの幅をScoreに応じて短くする。
    let width = 40 - 2 * Math.floor(Score / 20);
    if (width < 10) {
        width = 10;
    }
    Racket.Width = width;
}

/**
 * 球の状態更新
 */
function UpdateBall() {
    if (GameStatus == "game") {
        // 衝突を考慮しない新座標と速度を計算する。
        let x = Ball.X + Ball.VX;
        let y = Ball.Y + Ball.VY;
        let vx = Ball.VX;
        let vy = Ball.VY + 0.25;

        // ラケットとの衝突処理
        ({ y, vx, vy } = CollisionBallAndRachket(x, y, vx, vy));

        // 左右の壁との衝突処理
        let left = Area.X + Ball.SIZE;
        let right = Area.X + Area.Width - Ball.SIZE;
        if (x < left) {
            x = left + left - x;
            vx = - vx;
            // 得点を加算
            Score++;
        }
        else if (x > right) {
            x = right + right - x;
            vx = - vx;
            // 得点を加算
            Score++;
        }

        // 天井との衝突処理
        let top = Area.Y + Ball.SIZE;
        if (y < top) {
            y = top + top - y;
            vy = - 0.5 * vy;
            // 得点を加算
            Score += 3;
        }

        // 床との衝突処理
        let bottom = Area.Y + Area.Height - Ball.SIZE;
        if (y > bottom) {
            GameStatus = "gameover";
            ChangeButton1Text("NEXTゲーム");
            ChangeMessage(
                "ゲームオーバー\n" +
                "あなたのスコアは" + Score + "でした。\n" +
                "ボタンを押すと次のゲームを開始します。"
            );
        }

        Ball.X = x;
        Ball.Y = y;
        Ball.VX = vx;
        Ball.VY = vy;
    }
}

/**
 * 球がラケットに衝突したときの処理
 * @param x 球のY座標
 * @param y 球のX座標
 * @param vx 球のX軸速度
 * @param vy 球のY軸速度
 * @returns 
 */
function CollisionBallAndRachket(x: number, y: number, vx: number, vy: number) {
    let racket_left = Racket.X - Racket.Width / 2;
    let racket_right = Racket.X + Racket.Width / 2;
    let racket_top = Racket.Y - Racket.Height / 2;

    // 球がラケットに当たるとき、以下の条件を満たす。
    // - 球が下向きに移動している
    // - 球のX座標がラケットの左右の間にある。
    // - 球の最も下にある点座標(y+Ball.SIZE)とラケットの上面の距離が0以下になる
    let ball_bottom = y + Ball.SIZE;
    if (vy > 0 && racket_top <= ball_bottom) {
        if (racket_left <= x && x <= racket_right) {
            y += (racket_top - ball_bottom);
            // ラケットの幅を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 = (x - racket_left) / Racket.Width;
            if (dx < 0.1) {
                vy = -0.8 * vy;
                vx = Random(-10, -5);
            }
            else if (0.9 < dx) {
                vy = -0.8 * vy;
                vx = Random(5, 10);
            }
            else if (0.3 <= dx && dx <= 0.7) {
                vy = -1.2 * vy;
                if (vx > 5) { vx--; }
                else if (vx < 5) { vx++; }
            }
            else {
                vy = -0.9 * vy;
            }
        }
    }
    return { y, vx, vy };
}

/**
 * FrameCounterの値を使って、minとmaxの間の整数乱数値を得る。
 * @param min 返値の最小値
 * @param max 返値の最大値
 * @returns 
 */
function Random(min: number, max: number) {
    let frame = FrameCounter ?? 0;
    if (max < min) {
        console.log('error: max:{max} min:{min}')
    }
    let diff = max - min + 1;
    return (frame % diff) + min;
}

/**
 * ゲームCANVASを描画する。
 */
function DrawGameCanvas() {
    let cvsGame = document.getElementById("game") as HTMLCanvasElement;
    if (cvsGame) {
        let width = cvsGame.width;
        let height = cvsGame.height;
        let context = cvsGame.getContext("2d");
        if (context && width > 0 && height > 0) {
            // 実際の描画処理
            // 背景を黒に
            context.fillStyle = "black";
            context.fillRect(0, 0, width, height);

            // Areaの範囲を白で描画
            context.strokeStyle = "white";
            context.strokeRect(Area.X, Area.Y, Area.Width, Area.Height);

            // ラケットを描画
            context.fillStyle = "yellowgreen";
            let racket_x = Racket.X - Racket.Width / 2;
            let racket_y = Racket.Y - Racket.Height / 2;
            context.fillRect(racket_x, racket_y, Racket.Width, Racket.Height);

            // 球を黄色で描画
            context.fillStyle = "yellow";
            context.beginPath();
            context.arc(Ball.X, Ball.Y, Ball.SIZE, 0, 2 * Math.PI);
            context.fill();
        }
    }
}


/**
 * ラケットの移動先のX座標
 */
let RacketDestinationX = 0;

window.addEventListener("DOMContentLoaded", () => {
    let cvsGame = document.getElementById("game") as HTMLCanvasElement;
    if (cvsGame) {
        // ゲームキャンバス内をマウスが移動したときのイベント処理を追加する。
        cvsGame.addEventListener("mousemove", (ev) => {
            RacketDestinationX = ev.offsetX;
        })
    }
});

/**
 * id名にマッチするHTML要素のinnerTextを変更する。
 * @param id id名
 * @param text innerTextにセットする文字列
 */
function ChangeText(id: string, text: string) {
    let element = document.getElementById(id);
    if (element) {
        element.innerText = text;
    }
}

/**
 * button1のテキストを変更する。
 * @param text 変更する文字列
 */
function ChangeButton1Text(text: string) {
    ChangeText("button1", text);
}

/**
 * messageのテキストを変更する。
 * @param text 変更する文字列
 */
function ChangeMessage(text: string) {
    ChangeText("message", text);
}

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

document.addEventListener("DOMContentLoaded", () => {
    let button1 = document.getElementById("button1");
    if (button1) {
        button1.addEventListener("click", () => {
            switch (GameStatus) {
                case "game":
                    GameStatus = "stop";
                    ChangeButton1Text("スタート");
                    ChangeMessage("ゲームポーズ中\nボタンを押すと再開します。");
                    break;
                case "stop":
                    GameStatus = "game";
                    ChangeButton1Text("ストップ");
                    ChangeMessage("");
                    break;
                case "start":
                case "gameover":
                    GameStatus = "game";
                    InitGameSituation();
                    ChangeButton1Text("ストップ");
                    ChangeMessage("");
                    break;
            }
        })
    }
});

/**
 * ゲーム状態を初期化する。
 */
function InitGameSituation() {
    // フレームカウンターをランダム値として使う。
    let random = Random(1, 18);

    // ボールの座標、速度を初期化する。
    // X座標:初期値はフレームカウンタに応じてランダムに変化
    Ball.X = Area.X + Area.Width * 0.1 * (1 + (random % 9));
    // Y座標:初期値はAreaの上辺から球のSIZEだけ下
    Ball.Y = Area.Y + Ball.SIZE;
    // X軸速度:初期値はフレームカウンタが奇数なら-5、偶数なら5に変化する。
    Ball.VX = (random & 1) ? 5 : -5;
    // Y軸速度:初期値は0
    Ball.VY = 0;

    // ラケットの座標と幅を初期化する。
    // X座標:初期値はAreaの横幅の真ん中
    Racket.X = Area.X + Area.Width / 2;
    // Y座標:初期値はAreaの下辺からラケットの縦幅の半分だけ上
    Racket.Y = Area.Y + Area.Height - Racket.Height / 2
    // 幅は40
    Racket.Width = 40;

    // ゲームスコアのクリア
    Score = 0;
}

InitGameSituation();
window.requestAnimationFrame(GameLoop);

コメント