今回でOpenTKでつくる3Dカーレースゲームを完成させます。完成したゲームの動画。
前回のプログラムではライバル車を登場させましたが、ライバル車との当たり判定は考えていませんでした。そのため衝突しても内部をすり抜けてしまうという現象がおきました。今回はライバル車との当たり判定もおこないます。
下は前回の動画
今回はライバル車との当たり判定と衝突時の処理も実装しました。
ライバル車との当たり判定
まずRivalCarクラスに以下のメソッドを追加します。
これは自車と衝突したRivalCarを返すメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class RivalCar : Car { public static RivalCar CheckCrash(Car myCar) { foreach (RivalCar car in RivalCars) { // Y座標が 0 なら評価しない if (car.Y > 0) continue; if (Math.Pow(car.X - myCar.X, 2) + Math.Pow(car.Z - myCar.Z, 2) < 10) { return car; } } return null; } } |
ライバル車同士の当たり判定
これは各ライバル車が自車を含む別の車と衝突したときにそのCarを返すメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class RivalCar : Car { public Car CheckCrash() { // Y座標が 0 なら評価しない List<Car> cars = RivalCars.Where(x => x != this && x.Y == 0).ToList<Car>(); if(GameManager.MyCar.Y == 0) cars.Add(GameManager.MyCar); foreach (Car car in cars) { if (Math.Pow(car.X - X, 2) + Math.Pow(car.Z - Z, 2) < 10) { return car; } } return null; } } |
ライバル車がクラッシュしたときの処理
自車が衝突した場合は派手に吹っ飛んでもらいますが、ライバル車の場合はその場でスピンさせます。このときY座標を少し浮かせます。これによってライバル車の状態が通常走行をしているのか、クラッシュしてスピンしているのかがかわります。
ライバル車の更新処理を示します。
CheckCrashメソッドがnullを返し、Y座標も0である場合は通常走行をしている状態です。この場合はこれまでどおりの処理をおこないます。クラッシュしてスピンするということはライバル車のスピードは0になるので通常移動できたときの自車の速度を記憶しておきます。
CheckCrashメソッドがnullを返さなかった場合はクラッシュした場合です。この場合、車体を少し浮かせます。Y座標が0でない場合はスピンしている状態です。
この方法ではライバル車同士が接触した場合、スピンするのは片方だけです。ちょっと理不尽かもしれませんが、処理を簡単にするためにこのような仕様にします。自車と接触した場合は両方とも処理をします。自車はふっとび、ライバル車はその場でスピンします。そしてライバル車は自車が復帰する前にその場から立ち去ります。
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 |
public class RivalCar : Car { // 通常移動できた自車の速さを記憶する float OldSpeeed = 0; // 車がスピンする動作を繰り返した回数 int SpinCount = 0; // クラッシュした車がスピンする動作を繰り返さなければならない回数 int SpinCountMax = 0; Random Random = new Random(); public override void Update() { // クラッシュはしていないし、スピンしている最中でもない if (CheckCrash() == null && Y == 0) { // 通常移動できたときの自車の速度を記憶する OldSpeeed = Speed; TurnAround(); double rad = Math.PI / 180 * Rotate; VecX = Speed * (float)Math.Cos(rad); VecZ = -Speed * (float)Math.Sin(rad); base.Update(); } else { // 一定時間スピンしたらもとの状態に戻す // 「一定時間」は乱数で決める // 自車が連続クラッシュすることを避けるため、 // 自車がクラッシュから回復するより前に元の状態に戻してその場から立ち去る // 上記の理由により、SpinCountMaxには 120より小さい値を設定する if (SpinCountMax == 0) SpinCountMax = 100 - Random.Next(4) * 5; // その場でスピンさせる Speed = 0; Y = 0.2f; SpinCount++; Rotate += 30; // 一定時間スピンしたらもとの状態に戻す if (SpinCount > SpinCountMax) { // Y座標が0なのでスピンは終わった Y = 0; // またスピンするかもしれないのでSpinCountMaxとSpinCountの値はリセットしておく SpinCountMax = 0; SpinCount = 0; // レースを続けるために適切な方向を向かせる TurnAround(); // 速度をセット Speed = OldSpeeed; } } } } |
次に自車の処理を考えます。といってもUpdateCarStatusNomalメソッドの最初のif文の条件式が変わっただけです。
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 |
public class GameManager { void UpdateCarStatusNomal() { // 通常走行の処理をする条件として、RivalCar.CheckCrash(MyCar) == nullが追加された if (Course.IsPointInside(MyCar.X, MyCar.Z, false) && RivalCar.CheckCrash(MyCar) == null) { CarPointX = MyCar.X; CarPointY = MyCar.Y; CarPointZ = MyCar.Z; CarSpeeed = MyCar.Speed; } else { CarStatus = CarStatus.Crash; MyCar.Speed = 0f; CrashCarPointX = MyCar.X; CrashCarPointZ = MyCar.Z; CrashEyePointX = Eye.X; CrashEyePointZ = Eye.Z; VecXToBlownAway = -MyCar.VecX / CarSpeeed; VecZToBlownAway = -MyCar.VecZ / CarSpeeed; } } } |
現実に近い速度を表示させる
ところでコースは筑波サーキットをモデルにしています。
「コース全長が約2kmと短いため、本格的なレース開催には適していませんが、首都圏から近いサーキットであるため、アマチュアレースや数多くの走行会を開催しています」とのこと。
これまで速度表示をしてきましたが適当な値でした。そこで実物に近づけるため、以下のような変更を加えてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Car: Charctor { // 走行距離 public double Mileage = 0; public override void Update() { double rad = Math.PI / 180 * Rotate; VecX = Speed * (float)Math.Cos(rad); VecZ = -Speed * (float)Math.Sin(rad); base.Update(); double d = Math.Sqrt(Math.Pow(VecX, 2) + Math.Pow(VecZ, 2)); Mileage += d; } } |
これで一周回ったときのMileageの値は2400前後でした。筑波サーキットはコース全長が約2kmなので以下のように変更します。
以下のコードで1フレームあたりの筑波サーキットにおける移動量と時速を算出できます。
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 |
public class Car: Charctor { // 1フレーム当たりの走行距離 public double MileageParFlame = 0; // 本当の時速 public double RealSpeed = 0; public override void Update() { double rad = Math.PI / 180 * Rotate; VecX = Speed * (float)Math.Cos(rad); VecZ = -Speed * (float)Math.Sin(rad); base.Update(); // 1フレームの移動距離 double d = Math.Sqrt(Math.Pow(VecX, 2) + Math.Pow(VecZ, 2)); // 1フレーム当たりの走行距離(単位:メートル 走行距離に2000 / 2400を掛けて調整) MileageParFlame = d * 2000 / 2400; // 時速は1フレームの移動距離 * 60 * 3600 なので単位を km/h にするなら以下の式になる RealSpeed = MileageParFlame * 60 * 3600 / 1000; } } |
あとはGameManagerクラスのUpdateメソッドを変更して
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class GameManager { double Mileage = 0; // 総走行距離 double AccidentFreeMileage = 0; // 無事故走行距離 void Update() { UpdateCount++; UpdateMyCar(); RivalCar.UpdateAll(); Mileage += MyCar.MileageParFlame; AccidentFreeMileage += MyCar.MileageParFlame; Form1.label1.Text = String.Format("速度: {0} km/h", Math.Round(MyCar.RealSpeed)); Form1.label2.Text = String.Format("無事走行距離:{0} m 総走行距離: {1} m", Math.Round(AccidentFreeMileage), Math.Round(Mileage)); if (CarStatus == CarStatus.Crash) AccidentFreeMileage = 0; // クラッシュしたら無事故走行距離を0に } } |
タイムトライアル的なゲームにする
制限時間を設けてどれだけ走れるか、残機制にして総走行距離を競うゲームにしてみると面白いかもしれません。以下は短い時間で一周する時間を競うゲームにした例です。
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 |
public partial class Form1 : Form { Timer Timer2 = new Timer(); public int TotalTime = 0; public int LapTime = 0; public Form1() { InitializeComponent(); glControl.Load += GlControl_Load; glControl.Paint += GlControl_Paint; glControl.Resize += GlControl_Resize; Timer.Tick += Timer_Tick; Timer.Interval = 1000 / 60; Timer.Start(); Timer2.Tick += Timer2_Tick; Timer2.Interval = 1000; Timer2.Start(); GameManager = new GameManager(this, glControl); label1.Text = ""; label2.Text = ""; label3.Text = ""; } private void Timer2_Tick(object sender, EventArgs e) { TotalTime++; LapTime++; TimeSpan timeSpan = new TimeSpan(0, 0, Time); label3.Text = String.Format("{0:00}:{1:00}", timeSpan.Minutes, timeSpan.Seconds); } } |
GameManagerクラスに以下の修正を加えます。
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 |
public class GameManager { int UpdateCount = 0; int ShowRecord = 0; void Update() { UpdateCount++; UpdateMyCar(); RivalCar.UpdateAll(); Mileage += MyCar.MileageParFlame; AccidentFreeMileage += MyCar.MileageParFlame; Form1.label1.Text = String.Format("速度: {0} km/h", Math.Round(MyCar.RealSpeed)); Form1.label2.Text = String.Format("無事走行距離:{0} m 総走行距離: {1} m", Math.Round(AccidentFreeMileage), Math.Round(Mileage)); if (CarStatus == CarStatus.Crash) AccidentFreeMileage = 0; // ゴールしたら記録を3秒間上部に表示する if (IsGoal()) { ShowRecord = 180; TimeSpan timeSpan = new TimeSpan(0, 0, Form1.LapTime); Form1.label4.Text = String.Format("記録 {0:00}:{1:00}", timeSpan.Minutes, timeSpan.Seconds); Form1.LapTime = 0; } else { ShowRecord--; if(ShowRecord < 0) Form1.label4.Text = ""; } } } |
ゴールしたかどうかはスタート地点の脇にあるブロックが再び自車からみて最短距離にきたかどうかで判断しています。スタート時もスタート地点の脇にあるブロックが自車からみて最短距離にあります。そこである程度
走行してからでないとゴールとは見なさない、逆走をしてゴールラインにきてもゴールとは見なさない仕様にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class GameManager { bool IsGoal() { PointF[] goalPointFs = this.Course.GetNeerestExpandedBorderPoints(new PointF(93 * (float)this.ExpansionRate, 179 * (float)this.ExpansionRate)); PointF[] carPointFs = this.Course.GetNeerestExpandedBorderPoints(new PointF(MyCar.X, MyCar.Z)); float a = goalPointFs[0].X; float b = carPointFs[0].X; // rotateが0~360の範囲内にする int rotate = MyCar.Rotate; rotate = rotate % 360; if (rotate < 0) rotate += 360; // a - b == 0であれば自車はゴールラインにいる // rotate >= 18 && rotate <= 78 ただし逆走は不可 // Mileage > 100 最低100は走ることの条件付き if (Mileage > 100 && a - b == 0 && rotate >= 18 && rotate <= 78) return true; else return false; } } |
実際に操作した動画です。筑波サーキットにおけるコースレコードは43秒304とのことですが、どうしても50秒以上かかってしまいます・・・。