ASP.NET Coreで3DのSpaceWar!のような対戦型ゲームをつくる(1)の続きです。Playerクラスを定義します。
1 2 3 4 5 6 |
namespace SpaceWar { public class Player { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class Player { } |
Contents
プロパティ
最初に各プロパティを示します。
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
public class Player { // AspNetCore.SignalRで接続したときに付与されるID public string ConnectionId { private set; get; } // 各プレイヤーに与えられる1~8の固有の番号 public int PlayerNumber { private set; get; } // X Y Z 座標 double _x = 0; public int X { get { return (int)_x; } } double _y = 0; public int Y { get { return (int)_y; } } double _z = 0; public int Z { get { return (int)_z; } } // X Y Z 方向の速度 public double VelocityX { private set; get; } public double VelocityY { private set; get; } public double VelocityZ { private set; get; } // 機首方位角 double _headingAngle = 0; public double HeadingAngle { private set { _headingAngle = value; if (_headingAngle > Math.PI * 2) _headingAngle -= Math.PI * 2; else if (_headingAngle < 0) _headingAngle += Math.PI * 2; } get { return _headingAngle; } } // ピッチ角(機体は上向きか?下向きか?) public double AttitudeAngle { private set; get; } // バンク角(機体の左右の揺れ) public double BankAngle { private set; get; } // 現在死亡状態か? public bool IsDead { private set; get; } // Lifeが0になったら死亡 public int Life { set; get; } // InvincibleTimeが0より大きいときは無敵状態 public int InvincibleTime { set; get; } // プレイヤーの名前(""のときは「名無しさん」) string _name = ""; public string Name { set { if (value == "") _name = "名無しさん"; else _name = value; } get { return _name; } } // スコア public int Score { set; get; } // 残機 public int Rest { set; get; } // 方向キーが押されているかどうか? bool _isUpKeyDown = false; public bool IsUpKeyDown { set { _isUpKeyDown = value; } get { return _isUpKeyDown; } } bool _isDownKeyDown = false; public bool IsDownKeyDown { set { _isDownKeyDown = value; } get { return _isDownKeyDown; } } bool _isLeftKeyDown = false; public bool IsLeftKeyDown { set { _isLeftKeyDown = value; } get { return _isLeftKeyDown; } } bool _isRightKeyDown = false; public bool IsRightKeyDown { set { _isRightKeyDown = value; } get { return _isRightKeyDown; } } // 自機を撮影するカメラの座標 public int CameraX { set; get; } public int CameraY { set; get; } public int CameraZ { set; get; } } |
初期化
コンストラクタを示します。
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 |
public class Player { SpaceWarGame _game; public Player(string connectionId, int playerNumber, SpaceWarGame game) { Rest = SpaceWarGame.REST_MAX; Score = 0; Name = ""; ConnectionId = connectionId; PlayerNumber = playerNumber; _game = game; SetInitPosition(); // 初期の座標を設定する(後述) if (connectionId == "") { Life = 1; _name = "NPC " + playerNumber.ToString(); SetInitVelocity(); // NPCの場合、適当な機首方位角とピッチ角を設定してXYZ各方向の速度を設定する } else { Life = SpaceWarGame.LIFE_MAX; InvincibleTime = SpaceWarGame.INVINCIBLE_TIME_MAX; SetVelocity(); // 現在の機首方位角とピッチ角を設定からXYZ各方向の速度を設定する } } } |
初期状態の速度の設定
現在の機首方位角とピッチ角を設定からXYZ各方向の速度を設定する処理を示します。また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 |
public class Player { public void SetVelocity() { double speed; if (ConnectionId != "") speed = SpaceWarGame.PLAYER_SPEED; else speed = SpaceWarGame.NPC_SPEED; double vx0 = speed * Math.Sin(_headingAngle); double vz0 = speed * Math.Cos(_headingAngle); VelocityY = speed * Math.Sin(AttitudeAngle); VelocityX = vx0 * Math.Cos(AttitudeAngle); VelocityZ = vz0 * Math.Cos(AttitudeAngle); } static Random _random = new Random(); public const double RotationParUpdate = Math.PI / 32; // 1回の更新処理で変化する角度 void SetInitVelocity() { int max = (int)(2 * Math.PI / RotationParUpdate); int r = _random.Next(max); _headingAngle = RotationParUpdate * r; r = _random.Next(max / 3) - max / 6; AttitudeAngle = RotationParUpdate * r; SetVelocity(); } } |
初期座標の設定
以下はPlayerNumberを参照して各プレイヤーの初期の座標を設定するメソッドです。
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 |
public class Player { void SetInitPosition() { if (PlayerNumber == 1) { _x = SpaceWarGame.FIELD_SIZE / 4; _y = SpaceWarGame.FIELD_SIZE / 4; _z = SpaceWarGame.FIELD_SIZE / 4; } if (PlayerNumber == 2) { _x = SpaceWarGame.FIELD_SIZE / 4; _y = -SpaceWarGame.FIELD_SIZE / 4; _z = SpaceWarGame.FIELD_SIZE / 4; } if (PlayerNumber == 3) { _x = -SpaceWarGame.FIELD_SIZE / 4; _y = -SpaceWarGame.FIELD_SIZE / 4; _z = SpaceWarGame.FIELD_SIZE / 4; } if (PlayerNumber == 4) { _x = -SpaceWarGame.FIELD_SIZE / 4; _y = SpaceWarGame.FIELD_SIZE / 4; _z = SpaceWarGame.FIELD_SIZE / 4; } if (PlayerNumber == 5) { _x = SpaceWarGame.FIELD_SIZE / 4; _y = SpaceWarGame.FIELD_SIZE / 4; _z = -SpaceWarGame.FIELD_SIZE / 4; } if (PlayerNumber == 6) { _x = SpaceWarGame.FIELD_SIZE / 4; _y = -SpaceWarGame.FIELD_SIZE / 4; _z = -SpaceWarGame.FIELD_SIZE / 4; } if (PlayerNumber == 7) { _x = -SpaceWarGame.FIELD_SIZE / 4; _y = -SpaceWarGame.FIELD_SIZE / 4; _z = -SpaceWarGame.FIELD_SIZE / 4; } if (PlayerNumber == 8) { _x = -SpaceWarGame.FIELD_SIZE / 4; _y = SpaceWarGame.FIELD_SIZE / 4; _z = -SpaceWarGame.FIELD_SIZE / 4; } } } |
弾丸の発射
弾丸を発射する処理をするにあたってBulletクラスを定義します。
Bulletクラスの定義
コンストラクタの引数は発射位置のXYZ座標、XYZ方向の速度、発射したプレイヤーです。弾丸はなにかに命中するまでどこまでも飛び続けるのではなく1回の更新処理ごとにLifeがデクリメントされ、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 |
namespace SpaceWar { public class Bullet { public Bullet(int x, int y, int z, double vx, double vy, double vz, Player player) { _x = x; _y = y; _z = z; VelocityX = vx; VelocityY = vy; VelocityZ = vz; IsDead = false; Life = 128; Player = player; } double _x = 0; public int X { get { return (int)_x; } } double _y = 0; public int Y { get { return (int)_y; } } double _z = 0; public int Z { get { return (int)_z; } } public int Life { private set; get; } public double VelocityX { set; get; } public double VelocityY { set; get; } public double VelocityZ { set; get; } public bool IsDead { set; get; } public Player Player { get; } public void Update() { _x += VelocityX; _y += VelocityY; _z += VelocityZ; Worp(); Life--; if (Life <= 0) IsDead = true; } void Worp() { if (_x > SpaceWarGame.FIELD_SIZE / 2) _x -= SpaceWarGame.FIELD_SIZE; else if (_x < -SpaceWarGame.FIELD_SIZE / 2) _x += SpaceWarGame.FIELD_SIZE; if (_y > SpaceWarGame.FIELD_SIZE / 2) _y -= SpaceWarGame.FIELD_SIZE; else if (_y < -SpaceWarGame.FIELD_SIZE / 2) _y += SpaceWarGame.FIELD_SIZE; if (_z > SpaceWarGame.FIELD_SIZE / 2) _z -= SpaceWarGame.FIELD_SIZE; else if (_z < -SpaceWarGame.FIELD_SIZE / 2) _z += SpaceWarGame.FIELD_SIZE; } } } |
発射処理
弾丸を発射する処理を示します。連続で発射できるのは20発までです。効果音を鳴らすためにイベントを発生させます。
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 Player { public EventHandler? ShotEvent; List<Bullet> _bullets = new List<Bullet>(); public void Shot() { if (IsDead || _bullets.Count > 20) return; // 機体の方向に対応した初速を与える double vx0 = 30 * Math.Sin(_headingAngle); double vz0 = 30 * Math.Cos(_headingAngle); double vy = 30 * Math.Sin(AttitudeAngle); double vx = vx0 * Math.Cos(AttitudeAngle); double vz = vz0 * Math.Cos(AttitudeAngle); _bullets.Add(new Bullet(X, Y, Z, vx, vy, vz, this)); ShotEvent?.Invoke(this, new EventArgs()); } public List<Bullet> GetBullets() { return _bullets; } } |
NPCがプレイヤーにむけて弾丸を発射したら機関銃を乱射するような効果音を出すことにします。NPCがプレイヤーにむけて弾丸を発射したらBeingAttackedメソッドが呼び出されてイベントが発生します。
1 2 3 4 5 6 7 8 9 10 11 |
public class Player { public event EventHandler? BeingAttackedEvent; void BeingAttacked() { if (!IsDead && ConnectionId != "") { BeingAttackedEvent?.Invoke(this, new EventArgs()); } } } |
移動と更新処理
更新処理のメインの部分を示します。プレイヤーであればChangeDirectメソッド、NPCであればNPC_ChangeDirectAndShotメソッドを呼び出して進行方向を設定します。そのあとXYZ方向の速度をそれぞれ算出して移動処理をおこなっています。
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 { public void Update() { InvincibleTime--; _updateCount++; if (this.ConnectionId != "") ChangeDirect(); // 後述 else NPC_ChangeDirectAndShot(); // 後述 BankAngle = GetBankAngle(); // 後述 SetVelocity(); // 既出 _x += VelocityX; _y += VelocityY; _z += VelocityZ; Worp(); // 後述 if(this.ConnectionId != "") GetCameraPosition(); // 後述 _bullets = _bullets.Where(b => !b.IsDead).ToList(); foreach (Bullet bullet in _bullets) bullet.Update(); } } |
自機の方向転換
方向キーの状態を調べてプレイヤーの機体の方向を変更する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Player { void ChangeDirect() { if (IsLeftKeyDown) HeadingAngle += RotationParUpdate; if (IsRightKeyDown) HeadingAngle -= RotationParUpdate; if (IsUpKeyDown) { if (AttitudeAngle < Math.PI / 3) AttitudeAngle += RotationParUpdate; } if (IsDownKeyDown) { if (AttitudeAngle > -Math.PI / 3) AttitudeAngle -= RotationParUpdate; } } } |
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 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 Player { int _updateCount = 0; void NPC_ChangeDirectAndShot() { // 更新処理ごとに毎回実行するのではなく2回に1回 if (_updateCount % 2 == 1) return; // 自分の一番近くにいる敵を取得 Player? target = null; List<Player> enemies = _game.GetAllPlayers().Where(p => p != this && p.InvincibleTime <= 0).ToList(); { double minDistance = double.MaxValue; foreach (Player player in enemies) { double distance = Math.Pow(player.X - X, 2) + Math.Pow(player.Y - Y, 2) + Math.Pow(player.Z - Z, 2); if (minDistance > distance) { minDistance = distance; target = player; } } } if (target != null) { // ターゲットに接近するための最適の角度を取得 double idealHeadingAngle = Math.Atan2(target.X - X, target.Z - Z); double distance = Math.Sqrt(Math.Pow(target.X - X, 2) + Math.Pow(target.Z - Z, 2)); double idealAttitudeAngle = Math.Atan2(target.Y - Y, distance); // 最適の角度にRotationParUpdateずつ近づける if (idealAttitudeAngle < AttitudeAngle && AttitudeAngle > -Math.PI / 3) AttitudeAngle -= RotationParUpdate; else if (idealAttitudeAngle > AttitudeAngle && AttitudeAngle < Math.PI / 3) AttitudeAngle += RotationParUpdate; // 機種方位角は0~2πなので時計回りと反時計回りのどちらが速いか調べる if (idealHeadingAngle > Math.PI * 2) idealHeadingAngle -= Math.PI * 2; else if (idealHeadingAngle < 0) idealHeadingAngle += Math.PI * 2; if (idealHeadingAngle < HeadingAngle && idealHeadingAngle + Math.PI > HeadingAngle) HeadingAngle -= RotationParUpdate; if (idealHeadingAngle < HeadingAngle && idealHeadingAngle + Math.PI < HeadingAngle) HeadingAngle += RotationParUpdate; if (idealHeadingAngle > HeadingAngle && HeadingAngle + Math.PI > idealHeadingAngle) HeadingAngle += RotationParUpdate; if (idealHeadingAngle > HeadingAngle && HeadingAngle + Math.PI < idealHeadingAngle) HeadingAngle -= RotationParUpdate; // 現在の機種方位角とピッチ角が最適の角度に近い場合は弾丸を発射する // ただし8更新処理に1回の割合 if (_updateCount % 8 == 0 && Math.Abs(idealHeadingAngle - HeadingAngle) < 0.5 && Math.Abs(idealAttitudeAngle - AttitudeAngle) < 0.5) { Shot(); target.BeingAttacked(); } } } } |
バンク角の取得
_updateCountの値からバンク角を取得します。これによって左右に揺れながら飛んでいるように見せます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Player { double GetBankAngle() { int i = _updateCount + PlayerNumber * 3; int denominator = 512; int max = 32; if (i % (max * 4) < max) return (i % (max * 4)) * Math.PI / denominator; else if (i % (max * 4) < max * 3) return (max * 2 - (i % (max * 4))) * Math.PI / denominator; else return (-(max * 4) + (i % (max * 4))) * Math.PI / denominator; } } |
機体がフィールドの端に来たら反対側にワープさせます。そのための処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Player { void Worp() { if (_x > SpaceWarGame.FIELD_SIZE / 2) _x -= SpaceWarGame.FIELD_SIZE; else if (_x < -SpaceWarGame.FIELD_SIZE / 2) _x += SpaceWarGame.FIELD_SIZE; if (_y > SpaceWarGame.FIELD_SIZE / 2) _y -= SpaceWarGame.FIELD_SIZE; else if (_y < -SpaceWarGame.FIELD_SIZE / 2) _y += SpaceWarGame.FIELD_SIZE; if (_z > SpaceWarGame.FIELD_SIZE / 2) _z -= SpaceWarGame.FIELD_SIZE; else if (_z < -SpaceWarGame.FIELD_SIZE / 2) _z += SpaceWarGame.FIELD_SIZE; } } |
カメラ座標の取得
クライアントサイドでプレイヤーを描画するときにカメラ座標が必要です。それを取得する処理を示します。
以下の処理は進行方向に対して後ろ側から追いかけるカメラの座標を取得し、その結果をCameraXプロパティ、CameraYプロパティ、CameraZプロパティにセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Player { public void GetCameraPosition() { double vx1 = 300 * Math.Sin(_headingAngle); double vz1 = 300 * Math.Cos(_headingAngle); double vy2 = 300 * Math.Sin(AttitudeAngle - 0.2); double vx2 = vx1 * Math.Cos(AttitudeAngle - 0.2); double vz2 = vz1 * Math.Cos(AttitudeAngle - 0.2); CameraX = (int)(X - vx2); CameraY = (int)(Y - vy2); CameraZ = (int)(Z - vz2); } } |
被弾時の処理
被弾したらLifeを1減らしLifeが0のときは後述するDeadメソッドを呼び出し、そうでない場合はSetShortInvincibleTimeメソッドを呼び出して1秒間無敵状態にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Player { public void BeHit() { Life--; if (Life <= 0) Dead(); else SetShortInvincibleTime(); } void SetShortInvincibleTime() { InvincibleTime = SpaceWarGame.INVINCIBLE_TIME_MAX; } } |
死亡時の処理
死亡時の処理を示します。残機を1減らして3秒後に復活させるのですが、プレイヤーで残機0の場合はゲームオーバーになります。NPCの場合と残機がある場合はResetメソッドを呼び出してゲームを続行します。
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 Player { public event EventHandler? PlayerDeadEvent; void Dead() { IsDead = true; Rest--; PlayerDeadEvent?.Invoke(this, new EventArgs()); System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 3000; timer.Elapsed += Revival; timer.Start(); void Revival(object? sender, System.Timers.ElapsedEventArgs e) { System.Timers.Timer? t = (System.Timers.Timer?)sender; t?.Stop(); t?.Dispose(); if (Rest > 0 || ConnectionId == "") Reset(); else GameOver(); } } } |
死亡処理から復活する処理
死亡処理から復活する処理を示します。初期座標に移動させ、NPCの場合はSetInitVelocityメソッドを呼び出して初期の機体方位角とピッチ角を乱数で設定します。プレイヤーの場合は機体方位角とピッチ角はともに0とします。そしてしばらくのあいだ無敵状態にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Player { void Reset() { IsDead = false; SetInitPosition(); if (ConnectionId == "") { Life = 1; SetInitVelocity(); } else { Life = SpaceWarGame.LIFE_MAX; HeadingAngle = 0; AttitudeAngle = 0; InvincibleTime = SpaceWarGame.INVINCIBLE_TIME_MAX; } } } |
ゲームオーバー時の処理
ゲームオーバー時の処理を示します。SpaceWarGame.RemovePlayerメソッドを呼び出して辞書に登録されているPlayerを削除します。そしてGameOverEventイベントを発生させます。
1 2 3 4 5 6 7 8 9 |
public class Player { public event EventHandler? GameOverEvent; public void GameOver() { _game.RemovePlayer(ConnectionId); GameOverEvent?.Invoke(this, new EventArgs()); } } |
敵を倒したときの処理
敵を倒したらEnemyDeadメソッドが呼び出されます。するとEnemyDeadEventイベントが発生します。
1 2 3 4 5 6 7 8 |
public class Player { public event EventHandler? EnemyDeadEvent; public void EnemyDead() { EnemyDeadEvent?.Invoke(this, new EventArgs()); } } |