クソゲーに魂を!プロジェクト(2)の続きです。今回はGameクラスを定義します。
インデントが深くなるので名前空間部分は省略して表記します。
1 2 3 4 5 6 |
namespace FireSnake { public class Game { } } |
1 2 3 |
public class Game { } |
またPositionクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace FireSnake { public class Position { public Position(int x , int y, int priority) { X = x; Y = y; Priority = priority; } public int X { get; } public int Y { get; } public int Priority { get; } // 優先度 } } |
Contents
フィールド変数
以下のフィールド変数を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class Game { public const int InitRadius = Constant.INIT_FIELD_RADUUS; // バトル開始時のフィールドの半径 public const int CenterX = Constant.INIT_FIELD_RADUUS; // フィールドの中心座標 public const int CenterY = Constant.INIT_FIELD_RADUUS; Random _random = new Random(); // 乱数生成用 long UpdateCount = 0; // 更新処理がおこなわれた回数 // ASP.NET SignalRで使われる一意の接続IDからPlayerオブジェクトを取得できるようにする Dictionary<string, Player> ConnectionIdPlayerPairs = new Dictionary<string, Player>(); // 弾丸と餌のリスト public List<Bullet> Bullets = new List<Bullet>(); public List<Food> Foods = new List<Food>(); public List<Player> Players = new List<Player>(); // プレイヤー public List<Player> NPCs = new List<Player>(); // NPC public List<Player> AllPlayers = new List<Player>(); // Players と NPCs 双方 public int RemainingTime = Constant.INIT_REMAINING_TIME; // バトル終了までの時間 public int FieldRadius = InitRadius; // 現在のフィールドの半径 } |
初期化
Gameオブジェクトを初期化する処理を示します。
フィールドの半径と終了までの残り時間を初期値に戻します。そしてPlayers、NPCs、AllPlayersに格納されているPlayerオブジェクトをクリアしてNPCを生成し直します。餌も生成しなおします。そのあとMatchBegunイベントを送信します(それぞれ詳細後述)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Game { public Game() { } public void Init() { FieldRadius = InitRadius; RemainingTime = Constant.INIT_REMAINING_TIME; // Playerオブジェクトの初期化とNPCの生成 AllPlayers.Clear(); InitNpcs(GetPlayerInitPositions().OrderBy(_ => _.Priority).Take(Constant.PLAYERS_INIT_COUNT).ToList()); SetIntelligenceToNPCs(); InitFoods(); // 餌の初期化 MatchBegun?.Invoke(this, new EventArgs()); } } |
NPCの初期化
NPCを初期化する処理を示します。まずNPCをどこに生成するかを決めなければなりません。この処理をおこなうのがGetPlayerInitPositionsメソッドです。フィールドの全体を 5 × 5 の区画にわけそれぞれの区画からランダムに座標を取得します。そのときにランダムに優先度も設定します。優先度でソートして最初のConstant.PLAYERS_INIT_COUNT個を取ることでNPCの初期座標を決定することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class Game { List<Position> GetPlayerInitPositions() { double d = (InitRadius - 100) / 1.414 / 2; List<Position> positions = new List<Position>(); for (int a = -2; a <= 2; a++) { for (int b = -2; b <= 2; b++) { double x = InitRadius + d * a + GetRandom(-30, 30); double y = InitRadius + d * b + GetRandom(-30, 30); positions.Add(new Position((int)x, (int)y, (a + b) % 2 * 1000 + _random.Next(500))); } } return positions; } int GetRandom(int min, int max) { return _random.Next(max - min) + min; } } |
取得されたNPCの初期座標から実際にPlayerオブジェクトを生成する処理を示します。NPCsリストをクリアして生成されたPlayerオブジェクトをNPCsリストとAllPlayersリストに追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Game { void InitNpcs(List<Position> positions) { NPCs.Clear(); foreach (Position position in positions) { Player npc = new Player(""); // 第三引数は方向 npc.Init(position.X, position.Y, 1.0 * _random.Next(62800) / 10000); NPCs.Add(npc); AllPlayers.Add(npc); } } } |
SetIntelligenceToNPCsメソッドはNPCの知性(プレイヤーを攻撃するために適切に方向転換する頻度)を設定します。最大100、最低20と段階的に決めます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Game { void SetIntelligenceToNPCs() { int intelligence = 100; for (int i = 0; i < NPCs.Count; i++) { NPCs[i].Intelligence = intelligence; intelligence -= 10; if(intelligence < 20) intelligence = 20; } } } |
餌の初期化
餌を初期化する処理を示します。とりあえず乱数で座標を求め、それがフィールドの内部(境界から40以上離れている)であればその位置に餌を生成するという処理を初期の餌の数になるまで繰り返しています。
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 Game { void InitFoods() { Foods.Clear(); int count = 0; while (count < Constant.FOODS_INIT_COUNT) { // とりあえず乱数で座標を求める int x = _random.Next(CenterX * 2); int y = _random.Next(CenterY * 2); // 中心からの距離と(半径 - 40)を比較する int a = (int)Math.Pow(x - CenterX, 2) + (int)Math.Pow(y - CenterY, 2); int b = (FieldRadius - 40) * (FieldRadius - 40); if (a < b) { double r = ((double)_random.Next(614)) / 100; Food food = new Food(x, y, Constant.FOOD_SPEED * Math.Cos(r), Constant.FOOD_SPEED * Math.Sin(r)); Foods.Add(food); count++; } } } } |
MatchBegunイベント
MatchBegunイベントを以下の通り定義します。
1 2 3 4 |
public class Game { public event EventHandler? MatchBegun; } |
Uninitialize
すべてのユーザーが離脱して接続数が0になったらUninitializeの処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Game { public void UnInit() { Bullets.Clear(); Foods.Clear(); AllPlayers.Clear(); Players.Clear(); NPCs.Clear(); ConnectionIdPlayerPairs.Clear(); FieldRadius = InitRadius; } } |
ユーザーが接続・離脱したときの処理
新規にユーザーが接続したときの処理を示します。このときはPlayerオブジェクトを生成してPlayersリストとAllPlayersリストに追加します。またASP.NET SignalRで使われる一意の接続IDからPlayerオブジェクトをすぐに取得できるようにConnectionIdPlayerPairs辞書にも登録します。
位置はフィールドの中央としますが、ゲーム開始ボタンを押すまでプレイヤーは描画されず当たり判定もないので死亡フラグをセットした状態にしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Game { public Player? AddPlayer(string connectionId) { Player player = new Player(connectionId); Players.Add(player); AllPlayers.Add(player); ConnectionIdPlayerPairs.Add(connectionId, player); player.SetCenter(); player.IsDead = true; return player; } } |
これはASP.NET SignalRで使われる一意の接続IDから対応するPlayerオブジェクトを取得するメソッドです。
1 2 3 4 5 6 7 8 9 10 |
public class Game { public Player? GetPlayer(string connectionId) { if (ConnectionIdPlayerPairs.ContainsKey(connectionId)) return ConnectionIdPlayerPairs[connectionId]; else return null; } } |
ユーザーがゲーム開始ボタンを押したらプレイヤーの初期座標が決定しバトルに参加します。そのときの初期座標を取得する処理を示します。
長々と書いていますが、できるだけ他のプレイヤーがいない区画を探してそこに出現させようとしています。
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 |
public class Game { public (int X, int Y, double Angle) GetPlayerInitPositionAngle() { List<(int X, int Y, double Angle)> rets = new List<(int X, int Y, double Angle)>(); // 全体を100ピクセルの区画に分割してそれぞれに他のプレイヤーの身体があるかどうか調べる int cellSize = 100; int count = Constant.INIT_FIELD_RADUUS * 2 / cellSize; bool[,] used = new bool[count, count]; foreach (Player player in AllPlayers) { if (player.IsDead) continue; int len = player.Circles.Length; for (int i = 0; i < len; i += 8) { Circle? circle = player.Circles[i]; if (circle == null) continue; int c = (int)(circle.X / cellSize); int r = (int)(circle.Y / cellSize); used[r, c] = true; } Circle? last = player.Circles.Last; if (last != null) { int c = (int)(last.X / cellSize); int r = (int)(last.Y / cellSize); used[r, c] = true; } } // 他のプレイヤーの身体がない区画でフィールドの内部である区画を取得する for (int row = 0; row < count; row++) { for (int col = 0; col < count; col++) { int x = col * cellSize + cellSize / 2; int y = row * cellSize + cellSize / 2; int dx = x - CenterX; int dy = y - CenterY; if (Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)) > FieldRadius - 50) continue; bool ok = true; for (int r = row - 1; r <= row + 1; r++) { for (int c = col - 1; c <= col + 1; c++) { if (r < 0 || c < 0 || r >= count || c >= count) continue; if (used[r, c]) { ok = false; break; } } if (!ok) break; } if (ok) { (int X, int Y, double Angle) ret; ret.X = x; ret.Y = y; ret.Angle = Math.Atan2(CenterY - y, CenterX - x); rets.Add(ret); } } } // 新しいプレイヤーを出現させる区画が見つかったらその位置に出現させる // 見つからない場合は中央に出現させる if (rets.Count > 0) { int r = _random.Next(rets.Count); return rets[r]; } else { (int X, int Y, double Angle) ret; ret.X = CenterX; ret.Y = CenterY; ret.Angle = 1.0 * _random.Next(62800) / 10000; return ret; } } } |
ユーザーが離脱したときの処理を示します。このときは対応するPlayerオブジェクトをPlayersリストとAllPlayersリストから削除します。またConnectionIdPlayerPairs辞書からも削除します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Game { public Player? RemovePlayer(string connectionId) { Player? player = GetPlayer(connectionId); if (player == null) return null; Players.Remove(player); player.IsDead = true; player.Circles.Clear(); // 離脱したPlayerの身体も消滅させる // リストと辞書からPlayerオブジェクトを削除する ConnectionIdPlayerPairs.Remove(connectionId); return player; } } |
なんらかの原因でゲームから離脱するつもりはないのに通信が切れてしまう場合があります。この場合、再接続の処理に成功したときASP.NET SignalRで使われる一意の接続IDが別のものになってしまいます。
以下の処理はASP.NET SignalRで使われる一意の接続IDが変更されたときに以前と同じPlayerオブジェクトを取得できるようにするためのものです。
1 2 3 4 5 6 7 8 9 |
public class Game { public void ChangeConnectionID(Player player, string oldID, string newID) { player.ConnectionId = newID; ConnectionIdPlayerPairs.Remove(oldID); ConnectionIdPlayerPairs.Add(newID, 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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
public class Game { public void NewStage() { // 餌と弾丸はクリアしてよいが // 生き残ったプレイヤーは次のゲームに参加するのでクリアしてはならない AllPlayers = AllPlayers.Where(_ => _.ConnectionId != "").ToList(); Bullets.Clear(); Foods.Clear(); FieldRadius = InitRadius; // 各プレイヤーの初期座標を決める List<Position> positions = GetPlayerInitPositions().OrderBy(_ => _.Priority).ToList(); Queue<Position> queue = new Queue<Position>(); foreach (Position pos in positions) queue.Enqueue(pos); // 前のステージで死亡した回数をリセットする foreach (Player player in Players) player.DeadCount = 0; // 生き残ったプレイヤーに初期座標をセットする var players = Players.Where(_ => !_.IsDead); foreach (Player player in players) { player.Circles.Clear(); // 身体部分はいったん消滅させる double angle = 1.0 * _random.Next(62800) / 10000; // 初期方向 if (queue.Count > 0) { Position pos = queue.Dequeue(); // 上記で求めた座標のセット player.Init(pos.X, pos.Y, angle); } else player.Init(CenterX + GetRandom(80, 120), CenterY + GetRandom(80, 120), angle); // 求めた座標が足りないときは中央付近にセット } // プレイヤーの数から必要なNPCの数を算出してNPCも生成する int npcCount = Math.Max(Constant.PLAYERS_INIT_COUNT - players.Count(), 0); npcCount = Math.Min(npcCount, queue.Count); if(npcCount > 0) InitNpcs(queue.Take(npcCount).ToList()); SetIntelligenceToNPCs(); InitFoods(); // 餌も初期化する RemainingTime = Constant.INIT_REMAINING_TIME; // 残り時間のリセット MatchBegun?.Invoke(this, new EventArgs()); // バトル開始イベントの送信 } } |