アクセルとブレーキは別のキーにしてほしいというリクエストがあったので別バージョンも作成しました。Zキー:アクセル、Xキー:ブレーキです。⇒ 動作確認はこちらから
前回、問題点を解決しましたが、まだ未解決の問題として時間が正確でない問題があります。1秒間60フレームという前提ですが、実際には処理がおくれてしまうためそのようになっていません。そこでゲームの開始時刻と現在時刻の差から経過時刻を調べる方法で解決します。
Contents
経過時間を正確に測る
最初にUpdate関数が呼びだされたらそのときの時刻を記憶させます。そして再度Update関数が呼び出されたらそれとの差を調べます。これでゲーム開始からの時刻を知ることができます。またゲーム開始からの時刻から最後にゴールを通過した時刻を引けばラップタイムもわかります。
それからゴールしたかどうかの判定法も変えました。スタート地点に立ち止まったままでもゴールしたことにならないように、前回のゴールのときに変数 mileageAfterGoalをリセットして、mileageAfterGoalの値が一定以上でないとゴールしたとは判定されません。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | let lastLapTimeText: string = ""; let isFirst: boolean = true; let startTime: number = 0; let startTimeAfterGoal: number = 0; let mileageAfterGoal: number = 0; let passedTime = 0; let lapTime = 0; function Update() {     //updateCountAfterGoal++; 使わない     //updateCount++;     if (isFirst) {         isFirst = false;         ResetGame(); // ゲームをリセットするときにも使えるので関数にした     }     let now = new Date();     let nowTime = now.getTime();     passedTime = nowTime - startTime;     lapTime = passedTime - startTimeAfterGoal;     if (remainingNumberUntilResurrection > 0)         BlowOffMyCar();     if (remainingNumberUntilResurrection == 0) {         if (IsCarInside(car1))             MoveMyCar();         else             Crash();     }     if (IsGoal()) {         if (mileageAfterGoal > 1000) {             let lapTime = passedTime - startTimeAfterGoal;             let minutes = Math.floor(lapTime / 1000 / 60);             lastLapTimeText = minutes + " 分 " + ((lapTime - 60000 * minutes) / 1000).toFixed(1) + " 秒";             startTimeAfterGoal = passedTime;             mileageAfterGoal = 0;             // ゴールしたあとで実行される関数             OnGoal(lapTime);         }     }     // iフレームあたりの移動距離(単位 メートル)     let a = speed / expansionRate * 1.05;     mileageAfterGoal += a;     mileage += a;     realSpeedH = a * 216; // 時速     PlaySoundIfTurning();     RivalCar.MoveAll();     ShowText(realSpeedH);     // レンダリング     renderer.render(scene, camera);     requestAnimationFrame(Update); } function OnGoal(lapTime: number) { } | 
ResetGame関数を示します。スタート時刻をリセットするとともに、自車とライバル車の状態をリセットします。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function ResetGame() {     passedTime = 0;     startTimeAfterGoal = 0;     lastLapTimeText = "";     mileage = 0;     // スタート時刻をリセットする     let now = new Date();     startTime = now.getTime();     // 自車の状態をリセットする     speed = 0;     car1.position.x = 148 * expansionRate;     car1.position.y = 0;     car1.position.z = 120 * expansionRate;     let angle = GetIdealRotationY(car1);     car1.rotation.y = angle;     FollowJikiCamera();     remainingNumberUntilResurrection = 0;     // ライバル車の状態をリセットする     RivalCar.ResetAll();     // BGMを開始する(ただしチェックボックスがONのときのみ)     PlayBgms(); } | 
ライバル車の状態をリセットする
ライバル車の状態をリセットする処理を示します。RivalCarクラスにライバル車の状態をリセットするReset関数とAllReset関数を追加します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class RivalCar {     Speed: number = 0;     Car: THREE.Group = null;     // 初期の値を記憶する     InitPositionX: number = 0;     InitPositionZ: number = 0;     InitSpeed: number = 0;     InitAngle: number = 0;     constructor(car: THREE.Group, initX: number, initZ: number, initSpeed: number) {         this.Car = car;         car.position.x = initX * expansionRate;         car.position.z = initZ * expansionRate;         this.Speed = initSpeed * expansionRate;         let angle = GetIdealRotationY(car);         car.rotation.y = angle;         this.InitPositionX = car.position.x;         this.InitPositionZ = car.position.z;         this.InitSpeed = this.Speed;         this.InitAngle = car.rotation.y;     }     // 初期の値にリセットする     Reset() {         this.Car.position.x = this.InitPositionX;         this.Car.position.z = this.InitPositionZ;         this.Speed = this.InitSpeed;         this.Car.rotation.y = this.InitAngle;     }     static ResetAll() {         RivalCar.Cars.forEach(car => car.Reset());     } } | 
ゲームらしくする
ゲームとして成り立たせるために残機制にしてクラッシュするたびに自機が減り、0になったらゲームオーバーとします。またラップタイムが1分を超えると残機1没収とします。
| 1 2 | let initRest = 5; let rest = initRest; | 
現在の状態を表示する
現在の状態を表示する関数を示します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | function ShowText(realSpeedH) {     // 経過時間と残機をテキストフィールドに表示     const tf1 = document.getElementById("info1");     let minutes = Math.floor(passedTime / 1000 / 60);     let str1 = "Total Time " + minutes + " 分 " + ((passedTime - 60000 * minutes) / 1000).toFixed(1) + " 秒";     minutes = Math.floor(lapTime / 1000 / 60);     let str2 = "Lap Time " + minutes + " 分 " + ((lapTime - 60000 * minutes) / 1000).toFixed(1) + " 秒";     if (rest > 0)         tf1.innerHTML = str1 + "  " + str2 + "  残 " + rest.toString();     else         tf1.innerHTML = str1 + "  " + str2 + "  残 0";     tf1.style.transform = "translate(30px, 10px)";     tf1.style.backgroundColor = "#00008B";     tf1.style.color = "white";     tf1.style.fontSize = "18px";     // 速度と走行距離をテキストフィールドに表示     const tf2 = document.getElementById("info2");     let str3 = Math.round(realSpeedH).toString() + " Km / h" + "  総走行距離 " + mileage.toFixed(1) + " m";     if (lastLapTimeText != "")         str3 += "  Last Lap Time " + lastLapTimeText;     tf2.innerHTML = str3;     tf2.style.transform = "translate(30px, 40px)";     tf2.style.backgroundColor = "#00008B";     tf2.style.color = "white";     tf2.style.fontSize = "18px";     ShowGameOverText(rest <= 0); } | 
ゲームオーバー表示
ShowGameOverText関数はゲームオーバーのとき、ゲームオーバー表示をする関数です。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function ShowGameOverText(isGameover: boolean) {     const tf3 = document.getElementById("info3");     if (isGameOver)         tf3.innerHTML = "GAME OVER";     else         tf3.innerHTML = "";     tf3.style.transform = "translate(190px, 150px)";     tf3.style.backgroundColor = "#000000";     tf3.style.color = "white";     tf3.style.fontSize = "32px";     const tf4 = document.getElementById("info4");     if (isGameOver)         tf4.innerHTML = "Press S key to Retry";     else         tf4.innerHTML = "";     tf4.style.transform = "translate(230px, 200px)";     tf4.style.backgroundColor = "#000000";     tf4.style.color = "white";     tf4.style.fontSize = "18px"; } | 
クラッシュ時の処理
クラッシュしたら自機を1減らします。
| 1 2 3 4 | function Crash() {     rest--; 	// 他は省略 } | 
ゲームオーバーのときの処理
クラッシュすると自車が吹っ飛びますが、そのあと残機が0になっていた場合は復活させずにゲームオーバーの処理をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function BlowOffMyCar() {     car1.position.x += xDead;     car1.position.y += 0.3;     car1.position.z += zDead;     car1.rotation.x += 0.5;     car1.rotation.y += 0.5;     car1.rotation.z += 0.5;     remainingNumberUntilResurrection--;     if (remainingNumberUntilResurrection == 0) {         if (rest > 0)             ResurrectionCar();         else             GameOver();     } } | 
ゲームオーバーの処理はフラグをセットするとともに自車を見えない位置に移動させます。
| 1 2 3 4 5 6 7 8 9 10 11 12 | let isGameOver = false; function GameOver() {     isGameOver = true;     // 自車をみえない位置に移動させる     car1.position.x = 0;     car1.position.z = 0;     // BGMが鳴っていたら止める     StopBgm(); } | 
1分以内に1周できないなら1機没収
それから新しく作ったルールでは、ゴールしたときに1分以上かかっていた場合は1機没収することになっています。その結果、残機0になった場合はゲームオーバーです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | // ゴールしたときに実行される関数 function OnGoal(lapTime: number) {     let minutes = Math.floor(lapTime / 1000 / 60);     if (minutes > 0) {         rest--;         if (rest <= 0) {             car1.position.x = 0;             car1.position.z = 0;             isGameOver = true;         }     } } | 
ゲームが終了したにもかかわらず時間が経過していくのはおかしいのでisGameOverがtrueのときはUpdate関数が呼び出されてもなにもしません。ただしこれまではUpdate関数のなかでUpdate関数を呼び出していました。これが途絶えてしまうとゲームを再開するときはもう一度Update関数を呼び出さなければなりません。
| 1 2 3 4 5 6 7 8 9 | function Update() { 	// ゲームオーバーなら処理はしない     if (isGameOver) {         return;     } 	// 以下はこれまでと同じ } | 
リトライ時の処理
ゲームをもう一度やるときの処理を示します。残機をゲーム開始時に戻してisGameOverフラグをクリアします。またisGameOverフラグがtrueだった場合、Update関数を呼び出します。
| 1 2 3 4 5 6 7 8 9 10 11 | function ResetGame() {     rest = initRest;     let isGameOvered = isGameOver;     isGameOver = false; 	// 以下、略     // 最後に     if (isGameOvered)         Update(); } | 
ゲームオーバーのときにSキーを押すとリトライできます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function OnKeyDown(e: KeyboardEvent) {     if (e.keyCode == 37)         isLeftKeyDown = true;     if (e.keyCode == 38)         isUpKeyDown = true;     if (e.keyCode == 39)         isRightKeyDown = true;     if (e.keyCode == 40)         isDownKeyDown = true;     if (e.keyCode == 83) {         if (!isGameInited) {             isGameInited = true;             InitGame();         }         if (rest <= 0) {             ResetGame();         }     } } | 
