前回 ASP.NET Coreで3Dっぽいカーレースをつくる(1)の続きです。今回はPlayerクラスの定義をおこないます。
Contents
Playerクラスの定義
1 2 3 4 5 6 |
namespace CarRace3d { public class Player { } } |
インデントが深くなるので以降、この記事のなかでは
1 2 3 |
public class Player { } |
と書きます。
フィールド変数とプロパティ
Playerクラスのフィールド変数を示します。
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 |
public class Player { const int MaxRest = 5; // 初期の残機数 const int InitX = 148; // スタート地点のX座標 const int InitZ = 120; // スタート地点のZ座標 // クラッシュすると自車がふっとび、ライバル車はスピンする処理をするがその上限値 const int BLOWING_COUNT_MAX = 24 * 3; // ユーザーによってキーが押されているかどうか? public bool IsUpKeyDown = false; public bool IsDownKeyDown = false; public bool IsLeftKeyDown = false; public bool IsRightKeyDown = false; // ゲームが開始された時刻と最後にゴールした時刻 DateTime StartTime = DateTime.MinValue; DateTime LastTime = DateTime.MinValue; // NPCの名前をつけるために必要 static int nextNpcNumber = 1; int npcNumber = 1; // クラッシュすることなく移動できた直前の座標 // クラッシュしたときに自車をふっとばせる方向を決めるうえで必要 double OldX = 0; double OldZ = 0; // 走行距離とゴール後に走行した距離 double Mileage = 0; double MileageAfterGoal = 0; // 時速 double SpeedParHour = 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
public class Player { // 接続するときのConnectionID // これが""ならNPC(non player character) public string ConnectionID { private set; get; } // プレイヤーの名前。ゲーム中にライバル車が視界に入るときはこれが表示される public string Name { set; get; } // 車のX座標 public double X { private set; get; } // 車のY座標 // 普通は0 非表示にしたい場合は負数を設定する public double Y { private set; get; } // 車のZ座標 public double Z { private set; get; } // 車のX軸を中心とした回転 public double RotationX { private set; get; } // 車のY軸を中心とした回転 public double RotationY { private set; get; } // 車のZ軸を中心とした回転 public double RotationZ { private set; get; } // 車の移動速度 public double Speed { private set; get; } // 車がクラッシュしてからふっとぶ処理がおこなわれた回数 public int BlowingCount { private set; get; } // ゲーム開始直後とクラッシュから復帰したら、立て続きにクラッシュしないように5秒間無敵状態にする int _invincible = 24 * 5; public bool IsInvincible { private set { if (value == true) _invincible = 24 * 5; } get { if (_invincible > 0) return true; else return false; } } // ゲーム中にcanvasの上部に表示させる走行時間、走行時間、残機数などの文字列(1) public string InfoText1 { private set; get; } // ゲーム中にcanvasの上部に表示させる走行時間、走行時間、残機数などの文字列(2) public string InfoText2 { private set; get; } // 残機数 public int Rest { private set; get; } // クラッシュしたときに車を吹っ飛ぶX方向の初速 public double IVX_OnCrash { private set; get; } // クラッシュしたときに車を吹っ飛ぶZ方向の初速 public double IVZ_OnCrash { private set; get; } } |
次にイベント関連を示します。
1 2 3 4 5 6 7 8 9 10 11 |
public class Player { // クラッシュしたときとクラッシュから復帰の通知するためのイベント public delegate void GameHandler(Player player); public event GameHandler? Crash; public event GameHandler? Recover; // ゲームオーバーを通知するためのイベント public delegate void GameOveredHandler(Player player, int score); public event GameOveredHandler? GameOvered; } |
初期化
コンストラクタを示します。これはプレイヤー用のものとNPC用の2種類があります。
プレイヤー用のコンストラクタではConnectionIDプロパティに接続されたときのconnectionIDを設定します(NPC:non player character の場合は空文字)。そのあと残機を最大値にしてStartTimeとLastTimeにそのときの時刻をセットします。そして車の座標にスタート地点の座標を設定します。
NPCの場合はスタート地点ではなくコース上の分散された位置からのスタートになります。そこでそのときの座標と移動速度を各プロパティに設定します。
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 |
public class Player { public Player(string connectionID) { ConnectionID = connectionID; IsInvincible = true; Name = ""; InfoText1 = ""; InfoText2 = ""; Rest = MaxRest; StartTime = DateTime.Now; LastTime = DateTime.Now; LastLapText = ""; Speed = 0; X = InitX; Y = 0; Z = InitZ; RotationY = Game.GetIdealRotationY(X, Z); RotationX = 0; RotationZ = 0; BlowingCount = 0; } public Player(double x, double z, double speed) { ConnectionID = ""; X = x; Z = z; Speed = speed; InfoText1 = ""; InfoText2 = ""; // NPCに名前をつける npcNumber = nextNpcNumber; Name = "NPC 00" + npcNumber.ToString(); nextNpcNumber++; } } |
車の移動処理
車(ただしNPCは除く)を移動させる処理を示します。
ここではユーザーが押しているキーによって方向転換、加速、減速処理をおこなうのですが、方向転換の際にコースを逆走できないようにGame.GetIdealRotationYメソッドで最適化された移動方向を取得し、これと左右90度以上はずれている場合は方向転換できないようにしています。
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
public class Player { void Move() { double rad = Game.GetIdealRotationY(X, Z); // 角度を 0から2πの範囲になるように調整している if (rad < 0) rad += Math.PI * 2; if (rad > Math.PI * 2) rad -= Math.PI * 2; if (RotationY < 0) RotationY += Math.PI * 2; if (RotationY > Math.PI * 2) RotationY -= Math.PI * 2; if (IsLeftKeyDown) { double newRY = RotationY + 0.04; double dr = Math.Abs(newRY - rad); // 逆走防止 方向転換しようとしている方向は適切な方向からズレすぎてしないか? if (dr < Math.PI / 2) RotationY += 0.04; else { // 0radに近い値と2πを超えた値をそのまま比較使用としている場合もあるので // その場合は値を調整したあともう一度比較する if (newRY < rad) newRY += Math.PI * 2; else newRY -= Math.PI * 2; dr = Math.Abs(newRY - rad); if (dr < Math.PI / 2) RotationY += 0.04; } } if (IsRightKeyDown) { double newRY = RotationY - 0.04; double dr = Math.Abs(newRY - rad); if (dr < Math.PI / 2) RotationY -= 0.04; else { if (newRY < rad) newRY += Math.PI * 2; else newRY -= Math.PI * 2; dr = Math.Abs(newRY - rad); if (dr < Math.PI / 2) RotationY -= 0.04; } } // 加速処理 どこまでも加速できないように上限は2.0とする(時速換算で 300km/h 弱相当) if (IsUpKeyDown && Speed < 2.0) Speed += 0.03; // 減速処理 0未満にはならない if (IsDownKeyDown) { Speed -= 0.05; if (Speed < 0) Speed = 0; } // クラッシュ時の処理で必要になるので古いX座標とZ座標を変更するまえに保存しておく OldX = X; OldZ = Z; // X座標とZ座標を変更 X += Speed * Math.Sin(RotationY); Z += Speed * Math.Cos(RotationY); // Y座標(高さ)とX軸とZ軸の回転は0とする Y = 0; RotationX = 0; RotationZ = 0; // 移動させたあとコースアウトしているのであればクラッシュ! if (X != 0 && BlowingCount <= 0 && !Game.IsCourceInside(X, Z)) Crush(); } } |
クラッシュ時の移動処理
クラッシュ時の処理を示します。
NPC同士が衝突することもあります。このときNPCがクラッシュから復帰するタイミングが同じだとまたクラッシュしていつまでもレースに復帰することができなくなります。そこでNPCの場合はクラッシュから復帰するまでの時間を乱数で分散させています。
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 |
public class Player { static Random _random = new Random(); public void Crush() { Rest--; BlowingCount = 1; if (ConnectionID == "") BlowingCount -= _random.Next(48) - 24; else Speed = 0; // クラッシュしたときに吹っ飛ぶ方向を計算し、IV_OnCrashプロパティに格納する double dx = OldX - X; double dz = OldZ - Z; double rad = Math.Atan2(dz, dx); IVZ_OnCrash = Math.Sin(rad) * 0.8; IVX_OnCrash = Math.Cos(rad) * 0.8; // NPCでないならクラッシュのイベントを発生させる if(ConnectionID != "") Crash?.Invoke(this); } } |
プレイヤーがクラッシュして吹っ飛んでいるときの処理を示します。プレイヤーがクラッシュして吹っ飛んでいるように描画されるのはプレイヤー自身だけです。それ以外の場合はその場でスピンします。そこでここではBlowingCountをカウントアップさせているだけです。BLOWING_COUNT_MAXに達したらクラッシュから復帰します。
クラッシュから復帰したら車をコースの真ん中に移動させ、コースを走るうえで最適化された方向に向かせます。そして残機が0になっていない場合はRecoverイベント、残機が0になっている場合はGameOveredイベントを発生させます。
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 |
public class Player { bool PlayerBlowIfCrashed() { if (BlowingCount > 0) { BlowingCount++; if (BlowingCount >= BLOWING_COUNT_MAX) { IsInvincible = true; BlowingCount = 0; // 最適な場所に戻す Point pt = Game.GetIdealPosition(X, Z); X = pt.X; Z = pt.Z; RotationY = Game.GetIdealRotationY(X, Z); if (Rest > 0) Recover?.Invoke(this); else GameOvered?.Invoke(this, (int)Mileage); } return true; } return false; } } |
canvasの上部に表示させる文字列の生成
ゴールしたかどうかを調べるための処理を示します。かなりいい加減な方法ですが、出発点の座標の近くにいればゴールとみなします。
1 2 3 4 5 6 7 8 9 10 |
public class Player { bool IsGoal() { if (InitX - 10 < X && X < InitX + 10 && InitZ - 10 < Z && Z < InitZ + 10) return true; else return false; } } |
ゲーム中にcanvasの上部に表示させる走行時間、走行時間、残機数などの文字列を表示させますが、これを生成するための処理を示します。
時速と移動距離の計算ですが、モデルにした筑波サーキットの1周の長さとゲームで実際に表示される値がだいたい同じになるように定数をかけて調整しているだけです。0.656と216はマジックナンバーです。
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 |
public class Player { string LastLapText = ""; void SetPlayerInfoText() { //走行距離に関するデータ MileageText double a = Speed * 0.656; // 1更新あたりの移動距離(メートル) MileageAfterGoal += a; Mileage += a; SpeedParHour = a * 216; // 時速 TimeSpan ts1 = DateTime.Now - StartTime; TimeSpan ts2 = DateTime.Now - LastTime; string infoText1 = "Total Time " + ts1.Minutes + " 分 " + ts1.Seconds + "." + ts1.Milliseconds / 100 + " 秒"; string lastLapText = ts2.Minutes + " 分 " + ts2.Seconds + "." + ts2.Milliseconds / 100 + " 秒"; infoText1 += " Lap Time " + lastLapText; infoText1 += " 残 " + Rest; string infoText2 = ((int)SpeedParHour).ToString() + " Km / h" + " 総走行距離 " + (int)Mileage + " m"; infoText2 += " Last Lap Time " + LastLapText; InfoText1 = infoText1; InfoText2 = infoText2; // ゴールにいて前回のゴールから一定距離走っているならゴールインしたとみなす if (IsGoal() && MileageAfterGoal > 1000) { MileageAfterGoal = 0; LastTime = DateTime.Now; LastLapText = lastLapText; } } } |
更新処理
プレイヤーの更新処理を示します。
_invincibleが0以上のときは無敵状態なので当たり判定は後述する当たり判定の対象からはずします。ConnectionIDが空文字のときはNPCなのでこの場合は後述するNpcUpdateメソッドで処理をおこないます。プレイヤーの場合は前述のメソッドで移動処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Player { public void Update() { _invincible--; if (ConnectionID == "") { NpcUpdate(); return; } SetPlayerInfoText(); // クラッシュ中でなければ移動処理をする if (PlayerBlowIfCrashed()) return; Move(); } } |
NPCの更新処理をしめします。
BlowingCountが0より大きいときはBLOWING_COUNT_MAXに達するまでカウントアップします。その結果、BLOWING_COUNT_MAXに達したらレースに復帰させます。
走行中はコースアウトしているのであればコースの中央に移動させ、片方に寄りすぎていない調べて移動方向を調整します。NPCは加速も減速もせず、コンストラクタにわたされた値をつかって一定速度で走行します。
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 68 |
public class Player { int updateCount = 0; void NpcUpdate() { if (BlowingCount > 0) { BlowingCount++; if (BlowingCount >= BLOWING_COUNT_MAX) { BlowingCount = 0; IsInvincible = true; Point pt = Game.GetIdealPosition(X, Z); X = pt.X; Z = pt.Z; } return; } _invincible--; updateCount++; if (updateCount % 8 == 0) { updateCount = 0; // コースアウトしているのであればコースの中央に移動させる if (!Game.IsCourceInside(X, Z)) { Point point = Game.GetIdealPosition(X, Z); X = point.X; Z = point.Z; } // 片方に寄りすぎていない調べて移動方向を調整する Game.GetNeerestBorderPoints(((int)Math.Round(X)).ToString() + "," + ((int)Math.Round(Z)), out Point? ret1, out Point? ret2); if (ret1 != null && ret2 != null) { RotationY = -Math.Atan2(ret2.Z - ret1.Z, ret2.X - ret1.X) + Math.PI; // どちら側に寄っているのか? double dx1 = X - ret1.X; double dz1 = Z - ret1.Z; double distance1 = Math.Sqrt(Math.Pow(dx1, 2) + Math.Pow(dz1, 2)); double dx2 = X - ret2.X; double dz2 = Z - ret2.Z; double distance2 = Math.Sqrt(Math.Pow(dx2, 2) + Math.Pow(dz2, 2)); // 左寄りなので右にハンドルを切る if (distance1 < distance2 && (distance2 - distance1) > 5) { if (Speed > 0) RotationY -= 1 * Math.PI / 180; } // 右寄りなので左にハンドルを切る else if (distance1 > distance2 && (distance1 - distance2) > 5) { if (Speed > 0) RotationY += 1 * Math.PI / 180; } } } // 移動 X += Speed * Math.Sin(RotationY); Z += Speed * Math.Cos(RotationY); } } |
プレイヤーがゲームオーバーになった場合、そのPlayerオブジェクトはNPCとなります。そのためにはConnectionIDを空文字にします。また速度が0になっているので適当な値を設定します。またNameプロパティにNPCのもとの名前を設定します。
1 2 3 4 5 6 7 8 9 |
public class Player { public void ToNPC() { ConnectionID = ""; Name = "NPC 00" + npcNumber.ToString(); Speed = 0.7 + 0.1 * _random.Next(4); } } |