クソゲーに魂を!プロジェクト(3)の続きです。今回はGameクラス(更新処理部分)を定義します。
インデントが深くなるので名前空間部分は省略して表記します。
1 2 3 4 5 6 |
namespace FireSnake { public class Game { } } |
1 2 3 |
public class Game { } |
Contents
残り時間の更新と時間切れ時の処理
時間の経過とともにバトルの残り時間を減らします。残り時間が 0 になったらバトル終了を意味するGameFinishedイベントを送信します。
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 Game { public void DecrementRemainingTime() { RemainingTime--; if (RemainingTime == 0) { Task.Run(async () => { await Task.Delay(100); GameFinished?.Invoke(this, new WinnerArgs(null, 0)); // 時間切れの場合は勝者はいないので // WinnerArgsクラスのコンストラクタに渡す引数は null }); } } public delegate void GameFinishedHandler(object sender, WinnerArgs e); public event GameFinishedHandler? GameFinished; public delegate void GameFinishedHandler(object sender, WinnerArgs e); public event GameFinishedHandler? GameFinished; public class WinnerArgs : EventArgs { public WinnerArgs(Player? player, int bonus) { Winner = player; Bonus = bonus; } public Player? Winner { get; } public int Bonus { get; } } } |
移動処理
各キャラクタを移動させる処理を示します。
Playerの移動
Playerを移動させる処理を示します。PlayerクラスのUpdateメソッドを呼び出しているだけです。
1 2 3 4 5 6 7 8 9 10 |
public class Game { void UpdatePlayers() { foreach (Player player in Players) player.Update(); foreach (Player player in NPCs) player.Update(); } } |
弾丸の移動
弾丸を移動させる処理を示します。フィールドの縁にぶつかったら消滅させます。またこのときは死亡フラグがセットされたオブジェクトをリストから取り除く処理が必要になるので戻り値を返しています。
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 Game { bool UpdateBullets() { bool done = false; foreach (Bullet bullet in Bullets) { bullet.Move(); if (IsOutOfField(bullet)) bullet.IsDead = true; if(bullet.IsDead) done = true; } return done; } bool IsOutOfField(Bullet bullet) { double a = Math.Pow(bullet.X - CenterX, 2) + Math.Pow(bullet.Y - CenterY, 2); double b = Math.Pow(FieldRadius - Bullet.Radius, 2); if (a > b) return true; else return false; } } |
餌の移動
餌を移動させる処理を示します。フィールドの縁にぶつかる直前に跳ね返りの処理をおこないます。ところがフィールドの縁自身が移動しているため、跳ね返りの処理がうまくいかない場合があります。この場合は消滅させます。このときも死亡フラグがセットされたオブジェクトをリストから取り除く処理が必要になるので戻り値を返しています。
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 |
public class Game { bool UpdateFoods() { bool done = false; foreach (Food food in Foods) { food.Move(); if (IsOutOfField(food.X + food.VX, food.Y + food.VY, 8)) // 跳ね返りに失敗したら消滅させる { food.IsDead = true; done = true; } else if (IsOutOfField(food.X + food.VX, food.Y + food.VY, 16)) // 跳ね返り { double nx = food.X - food.VX; double ny = food.Y - food.VY; double a = Math.Atan2(-food.VY, -food.VX); double b = Math.Atan2(CenterY - ny, CenterX - nx); double rad = b * 2 - a; double vx = Constant.FOOD_SPEED * Math.Cos(rad); double vy = Constant.FOOD_SPEED * Math.Sin(rad); food.SetVelocity(nx, ny, vx, vy); } } return done; } bool IsOutOfField(double x, double y, double margin) { double a = Math.Pow(x - CenterX, 2) + Math.Pow(y - CenterY, 2); double b = Math.Pow(FieldRadius - margin, 2); if (a > b) return true; else return false; } } |
あたり判定
各キャラクタのあたり判定と衝突時におこなう処理を示します。
Player同士のあたり判定
Player同士のあたり判定の処理を示します。Playerの頭が他のPlayerの身体に衝突したときは体長が短いほうが死にます。そこでPlayerの頭と他のPlayerの身体が衝突しているかどうかを高速で判定する必要があるのですが、ここではイベントソートのアルゴリズムを採用しています。
頭が他のプレイヤーの身体に衝突するための必要条件はX座標を比較したときにその差の絶対値が(頭の半径+身体の半径)より小さいことです。また頭や身体の半径は体長によって変化しますが、上限値を設定しているので、(頭の半径+身体の半径)は36を超えることはありません。
そこで頭と身体の中心のX座標、頭の中心のX座標に36を足したもの、頭の中心のX座標から36を引いたものをソートします。そして座標が小さいものから処理をしていきます。ループのなかで取り出されたものが頭の中心のX座標から36を引いたものであればHashSetに追加し、頭の中心のX座標に36を加えたものであればHashSetから削除します。
ループのなかで取り出されたものが身体であれば、HashSetのなかに格納されているものがあたり判定となるものの候補となります。これだとすべてを総当りで調べる必要はないので高速で当たり判定をすることができます。
死亡したPlayerオブジェクトはAllPlayersリストから取り除かないといけないのでHitCheckPlayerToPlayerメソッドは戻り値を返しています。
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 |
public class Game { public delegate void KillEventHandler(object sender, string id); public event KillEventHandler? KillEvent; bool HitCheckPlayerToPlayer() { bool isDead = false; var events = new List<(double X, Circle Circle, string Event)>(); foreach (Player player in AllPlayers) { // 死亡フラグが立っていたり当たり判定無効のPlayerはスキップ if (player.IsDead || player.NoHitCheckValue > 0) continue; // 頭の左側X座標と右側X座標をイベントに追加 Circle? head = player.Circles.First; if (head != null) { events.Add((X: head.X - 36, Circle: head, Event: "HeadStart")); events.Add((X: head.X + 36, Circle: head, Event: "HeadEnd")); } // 身体のX座標をイベントに追加(ただし時間短縮のため 8 個おき) for (int i = 0; i < player.Circles.Length; i += 8) { Circle? circle = player.Circles[i]; if (circle != null) events.Add((X: circle.X, Circle: circle, Event: "Circle")); } if (player.Circles.Length - 1 % 8 != 0) { Circle? last = player.Circles.Last; if (last != null) events.Add((X: last.X, Circle: last, Event: "Circle")); } } var sorted = events.OrderBy(_ => _.X); HashSet<Circle> set = new HashSet<Circle>(); foreach (var ev in sorted) { if (ev.Event == "HeadStart") set.Add(ev.Circle); if (ev.Event == "HeadEnd") set.Remove(ev.Circle); if (ev.Event == "Circle") { Circle circle = ev.Circle; if (circle.Player.IsDead) continue; // 当たり判定の候補となるものとだけ距離の比較をすればOK foreach (Circle head in set) { if (head.Player.IsDead || circle.PlayerID == head.PlayerID) continue; double a = Math.Pow(head.X - circle.X, 2) + Math.Pow(head.Y - circle.Y, 2); double b = Math.Pow(head.Player.Radius + circle.Player.Radius, 2); // 衝突したときはどちらが死ぬのかを調べて死亡処理とKillEventイベントを送信する if (a < b) { Player killer; Player killed; Player player1 = head.Player; Player player2 = circle.Player; if (player1.Length != player2.Length) { killer = player1.Length > player2.Length ? player1 : player2; killed = player1.Length < player2.Length ? player1 : player2; } else { int rnd = _random.Next(2); killer = rnd == 0 ? player1 : player2; killed = rnd != 0 ? player1 : player2; } PlayerDead(killed); // 死亡処理 // イベントの送信(スコアの加算などで必要) KillEvent?.Invoke(this, killer.ConnectionId); killer.KillCount++; killer.TotalScore += (int)killer.Length * 20; killer.Score += (int)killer.Length * 20; isDead = true; break; } } } } return isDead; } } |
Playerの死亡処理
死亡処理を示します。死亡したPlayerは餌になって他のPlayerの養分になってもらいます。そのあと死亡フラグをセットして身体を消滅させます。また死亡カウントをインクリメントします。これは復活時に新規参加時よりも短くする(ミスに対するペナリティ)ためのものです。
また死亡判定によって生き残っているPlayerが1つのみになったときはそのPlayerが勝者です。このときは勝者に与えるボーナスポイントを計算したあとGameFinishedイベントを送信します。
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 |
public class Game { public delegate void GameOveredEventHandler(object sender, string id); public event GameOveredEventHandler? GameOveredEvent; public void PlayerDead(Player player) { int playerCirclesLength = player.Circles.Length; for (int i = 0; i < playerCirclesLength; i += 8) { Circle? circle = player.Circles[i]; if (circle != null) BodyToFood(circle); // 身体は餌に } player.IsDead = true; player.DeadCount++; player.Circles.Clear(); // 身体は消滅 // 死亡したのがユーザーであれば // ゲームオーバーイベントを送信してスコアランキングに登録(後述) if (player.ConnectionId != "") { GameOveredEvent?.Invoke(this, player.ConnectionId); Task.Run(() => { ScoreRanking.Add(player.PlayerName, player.TotalScore, player.KillCount, player.JoinInCount, player.VictoryCount); }); } // 生き残りは1人のみ? int playersCount = Players.Count(_ => !_.IsDead); int npcsCount = NPCs.Count(_ =>!_.IsDead); if (playersCount + npcsCount <= 1) // 生き残りが1人のみならそれが優勝者となる { Task.Run(async () => { await Task.Delay(1000); Player? winner = AllPlayers.FirstOrDefault(_ => !_.IsDead); if (winner != null && winner.ConnectionId != "") // 優勝者はユーザー { winner.VictoryCount++; // 優勝回数を加算 int bonus = winner.Score * 2; // ボーナスはそのステージで獲得した点数の2倍 winner.TotalScore += bonus; GameFinished?.Invoke(this, new WinnerArgs(winner, bonus)); } else // 優勝者は NPC GameFinished?.Invoke(this, new WinnerArgs(winner, 0)); }); } } } |
BodyToFoodメソッドは引数で渡されたCircleの近くに餌を出現させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Game { void BodyToFood(Circle circle) { double foodX = circle.X + _random.Next(8) - 4; double foodY = circle.Y + _random.Next(8) - 4; double a = (foodX - CenterX) * (foodX - CenterX) + (foodY - CenterY) * (foodY - CenterY); int b = (FieldRadius - 10) * (FieldRadius - 10); if (a < b) { double r = ((double)_random.Next(614)) / 100; Food food = new Food(foodX, foodY, Constant.FOOD_SPEED * Math.Cos(r), Constant.FOOD_SPEED * Math.Sin(r)); Foods.Add(food); } } } |
Playerと弾丸のあたり判定
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 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 |
public class Game { bool HitCheckBulletToPlayer() { bool isHit = false; // イベントソートで当たり判定をする var events = new List<(double X, Bullet? Bullet, Circle? Circle, string Event)>(); foreach (Bullet bullet in Bullets) { if (!bullet.IsDead) { events.Add((X: bullet.X - 36, Bullet: bullet, Circle: null, Event: "BulletStart")); events.Add((X: bullet.X + 36, Bullet: bullet, Circle: null, Event: "BulletEnd")); } } foreach (Player player in AllPlayers) { if (player.IsDead || player.NoHitCheckValue > 0) continue; for (int i = 0; i < player.Circles.Length; i += 8) { Circle? circle = player.Circles[i]; if (circle != null) events.Add((X: circle.X, Bullet: null, Circle: circle, Event: "Circle")); } if (player.Circles.Length - 1 % 8 != 0) { Circle? last = player.Circles.Last; if (last != null) events.Add((X: last.X, Bullet: null, Circle: last, Event: "Circle")); } } var sorted = events.OrderBy(_ => _.X); HashSet<Bullet> set = new HashSet<Bullet>(); foreach (var ev in sorted) { if (ev.Event == "BulletStart" && ev.Bullet != null) set.Add(ev.Bullet); if (ev.Event == "BulletEnd" && ev.Bullet != null) set.Remove(ev.Bullet); if (ev.Event == "Circle") { Circle? circle = ev.Circle; if (circle == null || circle.Player.IsDead) continue; foreach (Bullet bullet in set) { if (circle.PlayerID == bullet.PlayerID || bullet.IsDead) continue; double a = Math.Pow(bullet.X - circle.X, 2) + Math.Pow(bullet.Y - circle.Y, 2); double b = Math.Pow(Bullet.Radius + circle.Player.Radius, 2); if (a < b) // 弾丸命中時の処理 { Player player = circle.Player; // 被弾したPlayerの体長をConstant.DAMAGEだけ短くする // ただしConstant.PLAYER_MIN_LENGTHよりは短くしない for (int i = 0; i < Constant.DAMAGE; i++) { Circle? removed = player.Circles.RemoveLast(); if (removed == null) break; if (i % 2 == 0) BodyToFood(removed); if (player.Circles.Length <= Constant.PLAYER_MIN_LENGTH) break; } player.Length = Math.Max(player.Circles.Length, Constant.PLAYER_MIN_LENGTH); bullet.IsDead = true; isHit = true; break; } } } } return isHit; } } |
餌と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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
public class Game { bool HitCheckPlayerToFood() { bool eat = false; var events = new List<(double X, Food? Food, Circle? Circle, string Event)>(); foreach (Player player in AllPlayers) { if (player.IsDead || player.NoHitCheckValue > 0) continue; Circle? head = player.Circles.First; if (head != null) { events.Add((X: head.X - 36, Food: null, Circle: head, Event: "HeadStart")); events.Add((X: head.X + 36, Food: null, Circle: head, Event: "HeadEnd")); } } foreach (Food food in Foods) { if (!food.IsDead) events.Add((X: food.X, Food: food, Circle: null, Event: "Food")); } var sorted = events.OrderBy(_ => _.X); HashSet<Circle> heads = new HashSet<Circle>(); foreach (var ev in sorted) { if (ev.Event == "HeadStart" && ev.Circle != null) heads.Add(ev.Circle); if (ev.Event == "HeadEnd" && ev.Circle != null) heads.Remove(ev.Circle); if (ev.Event == "Food") { Food? food = ev.Food; if (food == null) continue; foreach (Circle head in heads) { double a = Math.Pow(head.X - food.X, 2) + Math.Pow(head.Y - food.Y, 2); double b = Math.Pow(head.Player.Radius + Food.Radius, 2); if (a < b) { food.IsDead = true; Player player = head.Player; player.Length += Constant.EXTEND_LENGHT; player.TotalScore += 10; player.Score += 10; eat = true; break; } } } } return eat; } } |
Playerはフィールド外に出ていないか?
以下はPlayerがフィールド外に出ていないかを判定する処理です。判定の処理は頭の位置とフィールドの半径を比較しているだけです。場外判定された場合、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 |
public class Game { bool CheckOutOfField() { bool done = false; foreach (Player npc in NPCs) { if (!npc.IsDead && IsOutOfField(npc)) { double rad = Math.Atan2(CenterY - npc.HeadY, CenterX - npc.HeadX); npc.Angle = rad; npc.NpcTurnRightCount = 0; npc.NpcTurnLeftCount = 0; } } foreach (Player player in Players) { if (!player.IsDead && IsOutOfField(player)) { PlayerDead(player); done = true; } } return done; } bool IsOutOfField(Player player) { double a = Math.Pow(player.HeadX - CenterX, 2) + Math.Pow(player.HeadY - CenterY, 2); double b = Math.Pow(FieldRadius - player.Radius, 2); if (a > b) return true; else return false; } } |
NPCの行動決定
NPCの行動を決定する処理を示します。
まず壁にぶつかりそうになったら回避行動を優先します。壁にぶつかりそうになっているかどうかはフィールドの半径と自身の中心からの距離を比較すればわかります(IsOutOfFieldメソッドで判定している)。壁への衝突回避は進行方向を中心方向に変更することでおこないます。
それ以外のときは自分よりも短いユーザーがいたらその方向に回頭させます(NPC同士で撃ち合いはしないようにする)。ただしすべてのNPCがひとりのユーザーを集中攻撃してしまってはよくないので、これをやるのは一体だけです。また自分よりも長いユーザーがいたら逃げるようにします。これらの処理は常におこなうのではなく、NPCのIntelligenceの値によっておこなったりおこなわなかったりと幅を持たせます。
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 |
public class Game { void NpcThink() { HashSet<Player> targets = new HashSet<Player>(); foreach (Player npc in NPCs) { Circle? head = npc.Circles.First; if (head == null) continue; // 壁にぶつかりそうなときは回避行動を優先させる if (IsOutOfField(head.X, head.Y, 60)) { double rad1 = npc.Angle; double rad2 = Math.Atan2(CenterY - npc.HeadY, CenterX - npc.HeadX); if (rad2 < 0) rad2 += Math.PI * 2; double rad = rad2 - rad1; if (Math.Max(rad1, rad2) - Math.Min(rad1, rad2) > Math.PI) rad *= -1; if (rad > 0) npc.NpcTurnRightCount += Constant.NPC_THINK_INTERVAL; else npc.NpcTurnLeftCount += Constant.NPC_THINK_INTERVAL; continue; } // 自分よりも短いプレイヤーがいたらそちらに回頭する Player? target = Players.FirstOrDefault(_ => _.Length < npc.Length && !_.IsDead); if (_random.Next(100) < npc.Intelligence) target = Players.FirstOrDefault(_ => _.Length < npc.Length && !_.IsDead); if (target != null && !targets.Contains(target)) { double rad1 = npc.Angle; double rad2 = Math.Atan2(target.HeadY - npc.HeadY, target.HeadX - npc.HeadX); if (rad2 < 0) rad2 += Math.PI * 2; double rad = rad2 - rad1; if (Math.Max(rad1, rad2) - Math.Min(rad1, rad2) > Math.PI) rad *= -1; if (rad > 0) npc.NpcTurnRightCount += Constant.NPC_THINK_INTERVAL; else npc.NpcTurnLeftCount += Constant.NPC_THINK_INTERVAL; targets.Add(target); } else { // 自分よりも長いプレイヤーがいたら逃げる Player? target2 = null; if(_random.Next(100) < npc.Intelligence) target2 = Players.FirstOrDefault(_ => _.Length > npc.Length && !_.IsDead && Math.Pow(_.HeadX - npc.HeadX, 2) + Math.Pow(_.HeadY - npc.HeadY, 2) < 10000); if (target2 != null) { double rad1 = npc.Angle; double rad2 = Math.Atan2(npc.HeadY - target2.HeadY, npc.HeadX - target2.HeadX); if (rad2 < 0) rad2 += Math.PI * 2; double rad = rad2 - rad1; if (Math.Max(rad1, rad2) - Math.Min(rad1, rad2) > Math.PI) rad *= -1; if (rad > 0) npc.NpcTurnRightCount += Constant.NPC_THINK_INTERVAL; else npc.NpcTurnLeftCount += Constant.NPC_THINK_INTERVAL; } else { int r = _random.Next(8); if (r == 0) npc.NpcTurnRightCount += Constant.NPC_THINK_INTERVAL; else if (r == 1) npc.NpcTurnLeftCount += Constant.NPC_THINK_INTERVAL; } } } } } |
更新処理の全体
更新処理の全体を示します。更新処理8回に1回の割合でフィールドを狭くしていきます。そのあとNPCの行動決定の処理をいれたあとPlayer、弾丸、餌の更新処理をして当たり判定の処理をおこないます。
当たり判定があった場合は死亡フラグが立っているオブジェクトをリストから取り除く処理をおこないます。ただしユーザーのPlayerが死亡してもPlayersリストからは取り除きません。これはゲームオーバー後も描画処理を継続したいからです(このとき死亡位置が必要となる)。
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 |
public class Game { public (List<long> DeadPlayers, List<long> DeadFoods, List<long> DeadBullets) Update() { UpdateCount++; if (UpdateCount % 8 == 0 && FieldRadius > 120) FieldRadius--; if (UpdateCount % Constant.NPC_THINK_INTERVAL == 0) NpcThink(); var ret = (DeadPlayers:new List<long>(), DeadFoods: new List<long>(), DeadBullets: new List<long>()); UpdatePlayers(); bool removed1 = CheckOutOfField(); bool removed2 = UpdateBullets(); bool removed3 = UpdateFoods(); if (UpdateCount % Constant.HIT_CHECK_TIMES == 0 && !Constant.NoHitCheck) { removed1 |= HitCheckPlayerToPlayer(); removed2 |= HitCheckBulletToPlayer(); removed3 |= HitCheckPlayerToFood(); } if (removed1) { ret.DeadPlayers = AllPlayers.Where(_ => _.IsDead).Select(_ => _.PlayerID).ToList(); AllPlayers = AllPlayers.Where(_ => !_.IsDead).ToList(); } if (removed2) { ret.DeadBullets = Bullets.Where(_ => _.IsDead).Select(_ => _.ID).ToList(); Bullets = Bullets.Where(_ => !_.IsDead).ToList(); } if (removed3) { ret.DeadFoods = Foods.Where(_ => _.IsDead).Select(_ => _.ID).ToList(); Foods = Foods.Where(_ => !_.IsDead).ToList(); } foreach (Player npc in NPCs) { if(!npc.IsDead) npc.Shot(Bullets); } return ret; } } |
スコアランキング
スコアランキングはひとつのプレイ(スタートボタンを押下してゲームオーバーになるまで)と同じユーザーが繰り返し参加したときの累計ランキングの2つをつくります。
スコアランキングを実装するために以下のクラスを定義します。
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 |
namespace FireSnake { public class Hiscore { public Hiscore(string name, int score, int killCount, string date) { Name = name; Score = score; KillCount = killCount; Date = date; } public string Name { get; } public int Score { get; } public int KillCount { get; } public string Date { get; } } public class TotalScore { public TotalScore(string name, int score, int killCount, int joinInCount, int victoryCount, string date) { Name = name; Score = score; KillCount = killCount; JoinInCount = joinInCount; VictoryCount = victoryCount; Date = date; } public string Name { get; } public int Score { set; get; } public int KillCount { set; get; } public int JoinInCount { set; get; } public int VictoryCount { set; get; } public string Date { set; get; } } } |
次にScoreRankingクラスですが、インデントが深くなるので名前空間部分は省略して表記します。
1 2 3 4 5 6 |
namespace FireSnake { public class ScoreRanking { } } |
1 2 3 |
public class ScoreRanking { } |
まずフィールド変数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class ScoreRanking { static string FilePath = "../hiscore-fire-snake-game.json"; static string FilePath2 = "../total-score-fire-snake-game.json"; static int MaxCount = 100; static object syncObject = new object(); // 非同期で書き込みが発生して例外が発生しないようにする public ScoreRanking() { // なにもしないコンストラクタ } } |
データを保存する処理を示します。普通のランキング用と累計ランキング用のふたつのJsonファイルに書き込みをしています。
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 |
public class ScoreRanking { public static void Add(string playerName, int score, int killCount, int joinInCount, int victoryCount) { AddHiscore(playerName, score, killCount); AddTotalHiscore(playerName, score, killCount, joinInCount, victoryCount); } // スコア、kill数の上位100件をそれぞれ取得して重複を取り除いて保存 static void AddHiscore(string playerName, int score, int killCount) { lock (syncObject) { List<Hiscore>? hiscores = null; string json = ""; if (System.IO.File.Exists(FilePath)) { System.IO.StreamReader sr = new StreamReader(FilePath); json = sr.ReadToEnd(); sr.Close(); hiscores = System.Text.Json.JsonSerializer.Deserialize<List<Hiscore>>(json); } if (hiscores == null) hiscores = new List<Hiscore>(); DateTime now = DateTime.Now; string date = $"{now.Year}-{now.Month:00}-{now.Day:00} {now.Hour}:{now.Minute:00}:{now.Second:00}"; hiscores.Add(new Hiscore(playerName, score, killCount, date)); List<Hiscore> saves = new List<Hiscore>(); List<Hiscore> scores = hiscores.OrderByDescending(_ => _.Score).Take(MaxCount).ToList(); List<Hiscore> killCounts = hiscores.OrderByDescending(_ => _.KillCount).Take(MaxCount).ToList(); saves.AddRange(scores); saves.AddRange(killCounts); saves = saves.Distinct().ToList(); json = System.Text.Json.JsonSerializer.Serialize(saves); System.IO.StreamWriter sw = new StreamWriter(FilePath, false, System.Text.Encoding.UTF8); sw.Write(json); sw.Close(); } } // スコア、優勝回数の上位100件をそれぞれ取得して重複を取り除いて保存 static void AddTotalHiscore(string playerName, int score, int killCount, int joinInCount, int victoryCount) { lock (syncObject) { List<TotalScore>? hiscores = null; string json = ""; if (System.IO.File.Exists(FilePath2)) { System.IO.StreamReader sr = new StreamReader(FilePath2); json = sr.ReadToEnd(); sr.Close(); hiscores = System.Text.Json.JsonSerializer.Deserialize<List<TotalScore>>(json); } if (hiscores == null) hiscores = new List<TotalScore>(); DateTime now = DateTime.Now; string date = $"{now.Year}-{now.Month:00}-{now.Day:00} {now.Hour}:{now.Minute:00}:{now.Second:00}"; TotalScore? totalScore = hiscores.FirstOrDefault(_ => _.Name == playerName); if (totalScore != null) { totalScore.Score += score; totalScore.KillCount += killCount; totalScore.JoinInCount += joinInCount; totalScore.VictoryCount += victoryCount; totalScore.Date = date; } else hiscores.Add(new TotalScore(playerName, score, killCount, joinInCount, victoryCount, date)); List<TotalScore> saves = new List<TotalScore>(); List<TotalScore> scores = hiscores.OrderByDescending(_ => _.Score).Take(MaxCount).ToList(); List<TotalScore> victoryCounts = hiscores.OrderByDescending(_ => _.VictoryCount).Take(MaxCount).ToList(); saves.AddRange(scores); saves.AddRange(victoryCounts); saves = saves.Distinct().ToList(); json = System.Text.Json.JsonSerializer.Serialize(saves); System.IO.StreamWriter sw = new StreamWriter(FilePath2, false, System.Text.Encoding.UTF8); sw.Write(json); sw.Close(); } } } |
保存されたデータを取得する処理を示します。
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 |
public class ScoreRanking { public static (List<Hiscore> Hiscores, List<Hiscore> KillCounts) GetHiscores() { lock (syncObject) { List<Hiscore>? hiscores = null; if (System.IO.File.Exists(FilePath)) { System.IO.StreamReader sr = new StreamReader(FilePath); string json = sr.ReadToEnd(); sr.Close(); hiscores = System.Text.Json.JsonSerializer.Deserialize<List<Hiscore>>(json); } if (hiscores == null) hiscores = new List<Hiscore>(); (List<Hiscore> Hiscores, List<Hiscore> KillCounts) ret; ret.Hiscores = hiscores.OrderByDescending(_ => _.Score).ThenByDescending(_ => _.KillCount).ToList(); ret.KillCounts = hiscores.OrderByDescending(_ => _.KillCount).ThenByDescending(_ => _.Score).ToList(); return ret; } } public static (List<TotalScore> TotalHiscores, List<TotalScore> VictoryCounts) GetTotalScores() { lock (syncObject) { List<TotalScore>? hiscores = null; if (System.IO.File.Exists(FilePath2)) { System.IO.StreamReader sr = new StreamReader(FilePath2); string json = sr.ReadToEnd(); sr.Close(); hiscores = System.Text.Json.JsonSerializer.Deserialize<List<TotalScore>>(json); } if (hiscores == null) hiscores = new List<TotalScore>(); (List<TotalScore> TotalHiscores, List<TotalScore> VictoryCounts) ret; ret.TotalHiscores = hiscores.OrderByDescending(_ => _.Score).ThenByDescending(_ => _.VictoryCount).ToList(); ret.VictoryCounts = hiscores.OrderByDescending(_ => _.VictoryCount).ThenByDescending(_ => _.Score).ToList(); return ret; } } } |