クソゲーに魂を!プロジェクト 残念ながらプログラミング以前の問題です!の続きです。
ゲームの仕様
まずゲームの仕様は以下のとおりです。
キー操作またはマウスで自機を操作することができる。餌を食べると体長が長くなる。
発射ボタン(PCであればSpaceキーまたはマウスクリック)を押すと自機の前後から弾丸が発射される。弾丸を発射すると自分の体長が 1 短くなるが、敵に命中させた場合は敵の体長を 8 短くすることができる。
自分よりも短い敵に体当たりをしたらその敵を倒すことができる。 ただし長い敵に体当たりをしたときは逆に自分が死ぬ。 同じ長さの場合は乱数で勝敗を決める。
フィールドは半径800の円とし、時間の経過とともに狭くなっていく(最小値は120)。ユーザーは任意のタイミングでゲームに参加できる。そのときの体長は各プレイヤーの長さの平均値とする。しかしミスをしたステージで再参戦するときは最小の長さ(=8)とする。倒されたNPCは同一ステージ内では復活せず、最後まで生き残ったプレイヤーをそのステージの優勝者とする。
餌を食べたときは10点加算。敵を倒したら自分の長さ×10点です。各ステージで最後まで生き残った場合は勝者となり、そのステージで獲得した点数×2がボーナスポイントとして加算される。
高速化の工夫
60fpsだとどうしてもときどきカクつきがおきます。Microsoft.AspNetCore.SignalRを使う以上仕方がないことなのかと思ったのですが、そうでもありません。
高頻度でメッセージを送信するアプリケーション (リアルタイム ゲーム アプリケーションなど) であっても、ほとんどのアプリケーションは 1 秒間に数個以上のメッセージを送信する必要はありません。
たしかにそのとおりでフィールド上にあるすべてのオブジェクトの座標を送信しようとするからデータ送信量が増えるのであって工夫すれば「1 秒間に数個」程度に減らすことができます。
プレイヤーの進行速度は同じなので進行方向が変化したことを送信すればクライアント側でもサーバー側で管理されている座標と同じ位置に描画をすることができるはずです。ただ通信障害でうまくいかないこともあるのでときどきオブジェクトの座標を送信してズレを修正することにします。
では、はじめましょう。名前空間は FireSnake とします。
定数の定義
Constantクラスを以下のように定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace FireSnake { public class Constant { public const int PLAYERS_INIT_COUNT = 12; // プレイヤーの初期の総数 public const int FOODS_INIT_COUNT = 48; // ゲーム開始時の餌の数 public const int INIT_FIELD_RADUUS = 800; // ゲーム開始時のフィールドの半径 public const double PLAYER_SPEED = 1.5; // プレイヤーの移動速度 public const double FOOD_SPEED = 0.8; // 餌の移動速度 public const int BULLET_LIFE = 64; // 弾丸の存在時間(約1秒) public const int PLAYER_INIT_LENGTH = 32; // プレイヤーの初期の長さ public const int PLAYER_MIN_LENGTH = 8; // プレイヤーの最短の長さ public const int EXTEND_LENGHT = 2; // 餌を食べたときに伸びる量 public const double TURN_ANGLE_PAR_UPDATE = 0.06; // 1更新あたりの旋回角度(単位はラジアン) public const int NPC_THINK_INTERVAL = 16; // NPCは16更新に1回、動作を変更する public const int SHOT_INTERVAL = 30; // 弾丸を発射したら30回更新後まで次の弾丸を発射できない public const int HIT_CHECK_TIMES = 4; // 4回更新ごとに当たり判定をする public const int UPDATE_PAR_SECOND = 60; // 更新は1秒間に60回 public const int INIT_REMAINING_TIME = 180; // 1回のバトルの時間は180秒とする public const int DAMAGE = 8; // 弾丸が命中したら 8 短くなる public const int INIT_NO_HITCHECK_VALUE = 120; // プレイに参加したら120更新までは当たり判定を無効とする } } |
Circleクラスの定義
プレイヤーの身体を構成する円の座標を管理するためにCircleクラスを定義します。Circleオブジェクトにはオブジェクトが生成されたときのプレイヤーの状態も記録します。ひとつ前に生成されたオブジェクトに記録されている値と比較して異なっている場合はプレイヤーの状態が変化したことを意味しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace FireSnake { public class Circle { public double X { get; } // 座標 public double Y { get; } public long PlayerID { get; } // どのプレイヤーの身体を構成する円か? public Player Player { get; } // Playerクラスの定義は後述 // このCircleオブジェクトが生成されたときのプレイヤーの状態 public int RotateCount { set; get; } // 左右どちらに旋回しているか? public int PlayerLength { set; get; } // 長さ public int KillCount { set; get; } // 他のプレイヤーをどれだけ倒したか? public int Score { set; get; } // スコア public Circle(Player player, double x, double y) { X = x; Y = y; Player = player; PlayerID = player.PlayerID; } } } |
Bulletクラスの定義
弾丸を操作するためのBulletクラスを定義します。弾丸には一意のIDであるIDプロパティとどのプレイヤーが発射したものかわかるようにPlayerIDプロパティを定義します。また弾丸はなにかに当たるまで永久に飛び続けるのではなく約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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
namespace FireSnake { public class Bullet { static long nextID = 0; public int Type = 0; // 1は前方へ、2は後方にむけて発射された弾丸 public long ID { get; } public long PlayerID { get; } public double Angle = 0; // 発射角度 public double X { private set; get; } // 座標 public double Y { private set; get; } public double VX { private set; get; } // 移動速度 public double VY { private set; get; } public const double Radius = 12; public bool IsDead { set; get; } public int Life { private set; get; } // 弾丸の生存時間 public Bullet(long playerID, double x, double y, double angle, int type) { ID = nextID++; Type = type; // 前方へは2倍の速度で射出される double speed = type == 1 ? Constant.PLAYER_SPEED * 2 : Constant.PLAYER_SPEED; PlayerID = playerID; X = x; Y = y; Angle = angle; VX = Math.Cos(angle) * speed; VY = Math.Sin(angle) * speed; IsDead = false; Life = Constant.BULLET_LIFE; } public void Move() { X += VX; Y += VY; Life--; if(Life <= 0) IsDead = true; } } } |
Foodクラスの定義
餌を操作するためのFoodクラスを定義します。餌も一意のIDであるIDプロパティを定義します。餌はフィールドの端に当たったら内部にむけて跳ね返るようにします。なので他のプレイヤーに食べられるまで存在しつづけることになります。
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 |
namespace FireSnake { public class Food { static long nextID = 0; public long ID { get; } public double X { private set; get; } // 座標 public double Y { private set; get; } public double VX { private set; get; } // 移動速度 public double VY { private set; get; } public double OldVX { private set; get; } // 前回更新時の移動速度 public double OldVY { private set; get; } public const double Radius = 12; public bool IsDead { set; get; } public int UpdateCount = 0; // 更新回数(生成されたばかりの餌を見分けるために必要) public Food(double x, double y, double vx, double vy) { ID = nextID++; X = x; Y = y; VX = vx; VY = vy; IsDead = false; UpdateCount = 0; } public void Move() { X += VX; Y += VY; UpdateCount++; OldVX = VX; OldVY = VY; } // 跳ね返り処理がおこなわれたとき移動速度を変更するために必要 public void SetVelocity(double x, double y, double vx, double vy) { X = x; Y = y; OldVX = VX; OldVY = VY; VX = vx; VY = vy; } } } |