これからASP.NET Core版 対戦型Pengoをつくります。
『ペンゴ』(Pengo)とは
『ペンゴ』(Pengo)は、1982年9月にリリースされたセガ販売のアーケードゲームです。ペンギンのペンゴを操り、敵キャラクターであるスノービーを全滅させることが目的のアクションパズルゲームです。画面には氷が迷路状に配置されており、この氷を飛ばしてスノービーを押し潰して倒します。
作ろうとしているゲームの主な仕様
今回は対戦型のPengoをつくります。敵キャラであるスノービーは作らず、プレイヤー同士で氷を投げ飛ばしあってライバルのプレイヤーを倒します。原作では3つのダイヤモンドブロックがあり、これを並べることでボーナス点が加算されますが、これに関しては無視します。
フィールドは15×15とし、プレイヤーは8人とする。8人に満たない場合はNPCで不足分を補う。
プレイヤーが移動しようとした先に氷がある場合、キー入力で氷を投げたり破壊できる
氷を投げることができるのは、氷の向こうに壁や氷がない場合である
氷の向こうに壁または別の氷がある場合は氷は破壊される
ゲームの進行とともに投げることができる氷がなくなっていくので途中でランダムに追加する
これではPengoではないではないか!というツッコミもあるかもしれませんが、これでゲームをつくることにします。
迷路をつくる
最初にブロックで迷路をつくります。
以下はフィールド上における位置を管理するためのクラスです。
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 |
namespace PengoGame { public class Position { public Position() { } public Position(int col, int row) { Column = col; Row = row; } public int Column { protected set; get; } public int Row { protected 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
namespace PengoGame { public class Branch { public Branch(int colum, int row) { Colum = colum; Row = row; Used = false; _position = new Position(colum * 2, row * 2); } public int Colum { get; } public int Row { get; } public bool Used { set; get; } Position _position; public Position Position { get { return _position; } } List<Direct> _moveDirects = new List<Direct>(); public List<Direct> MoveDirects { get { return _moveDirects; } } } } |
以下は方向の列挙体です。
1 2 3 4 5 6 7 8 |
public enum Direct { None, Up, Down, Left, Right, } |
以下は迷路を生成するためのクラスです。
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 187 188 189 190 191 192 193 194 195 196 197 198 199 |
namespace PengoGame { public class MaseGenerator { Random _random = new Random(); List<Branch> _branchs = new List<Branch>(); Branch? CurBranch = null; List<Branch> ConnectedBranchs = new List<Branch>(); public List<Position> Positions = new List<Position>(); public MaseGenerator(int columMax, int rowMax) { RowMax = rowMax; ColumMax = columMax; // 分岐点をつくる for (int y = 0; y < RowMax; y++) { for (int x = 0; x < ColumMax; x++) _branchs.Add(new Branch(x, y)); } // 最初は中心から CurBranch = _branchs.First(x => x.Colum == ColumMax / 2 && x.Row == RowMax / 2); CurBranch.Used = true; ConnectedBranchs.Add(CurBranch); // 迷路をつくる CreateMaze(); } public int ColumMax { get; } public int RowMax { get; } public List<Branch> BranchPoints { get { return _branchs.ToList(); } } void CreateMaze() { while (true) { // ConnectedPointsの最後の要素を取得する Branch branch = ConnectedBranchs.Last(); // ConnectedPointsの最後の要素が移動できる点が存在する点である場合は // そこを新しい現在位置にしてループをぬける if (GetMoveDirect(branch).Count > 0) { CurBranch = branch; break; } // 現在位置から移動できる点が見つからない場合は現在位置をConnectedPointsから取り除く // もし出発点まで戻ってしまった場合は終了 ConnectedBranchs.Remove(branch); if (ConnectedBranchs.Count == 0) return; } while (true) { // 現在位置から移動できる分岐点を探して移動する // これを移動先が見つからなくなるまで繰り返す bool ret = Move(); // 移動先が見つからない場合 // すでにすべての分岐点が使用されているのであれば終了 if (!_branchs.Any(x => !x.Used)) return; // 移動先が見つからない場合は自身を再帰呼び出しする if (!ret) CreateMaze(); } } bool Move() { if(CurBranch == null) return false; List<Direct> moveDirects = GetMoveDirect(CurBranch); if (moveDirects.Count == 0) return false; int r = _random.Next(moveDirects.Count); Direct direct = moveDirects[r]; if (direct == Direct.Left) Move(Direct.Left); if (direct == Direct.Up) Move(Direct.Up); if (direct == Direct.Right) Move(Direct.Right); if (direct == Direct.Down) Move(Direct.Down); return true; } List<Direct> GetMoveDirect(Branch curPoint) { List<Direct> ret = new List<Direct>(); Branch? branch = _branchs.FirstOrDefault(x => x.Colum == curPoint.Colum - 1 && x.Row == curPoint.Row); if (curPoint.Colum != 0 && branch != null && !branch.Used) ret.Add(Direct.Left); branch = _branchs.FirstOrDefault(x => x.Colum == curPoint.Colum && x.Row == curPoint.Row - 1); if (curPoint.Row != 0 && branch != null && !branch.Used) ret.Add(Direct.Up); branch = _branchs.FirstOrDefault(x => x.Colum == curPoint.Colum + 1 && x.Row == curPoint.Row); if (curPoint.Colum + 1 < ColumMax && branch != null && !branch.Used) ret.Add(Direct.Right); branch = _branchs.FirstOrDefault(x => x.Colum == curPoint.Colum && x.Row == curPoint.Row + 1); if (curPoint.Row + 1 < RowMax && branch != null && !branch.Used) ret.Add(Direct.Down); return ret; } void Move(Direct direct) { if (CurBranch == null) return; Position oldPosition = CurBranch.Position; CurBranch.MoveDirects.Add(direct); if (direct == Direct.Left && CurBranch != null) { CurBranch = _branchs.FirstOrDefault(x => x.Colum == CurBranch.Colum - 1 && x.Row == CurBranch.Row); if (CurBranch != null) CurBranch.MoveDirects.Add(Direct.Right); } if (direct == Direct.Up && CurBranch != null) { CurBranch = _branchs.FirstOrDefault(x => x.Colum == CurBranch.Colum && x.Row == CurBranch.Row - 1); if (CurBranch != null) CurBranch.MoveDirects.Add(Direct.Down); } if (direct == Direct.Right && CurBranch != null) { CurBranch = _branchs.FirstOrDefault(x => x.Colum == CurBranch.Colum + 1 && x.Row == CurBranch.Row); if (CurBranch != null) CurBranch.MoveDirects.Add(Direct.Left); } if (direct == Direct.Down && CurBranch != null) { CurBranch = _branchs.FirstOrDefault(x => x.Colum == CurBranch.Colum && x.Row == CurBranch.Row + 1); if (CurBranch != null) CurBranch.MoveDirects.Add(Direct.Up); } if (CurBranch == null) return; CurBranch.Used = true; ConnectedBranchs.Add(CurBranch); Positions.Add(oldPosition); Positions.Add(CurBranch.Position); if (oldPosition.Column == CurBranch.Position.Column) { int startRow = oldPosition.Row < CurBranch.Position.Row ? oldPosition.Row : CurBranch.Position.Row; int endRow = oldPosition.Row > CurBranch.Position.Row ? oldPosition.Row : CurBranch.Position.Row; for (int row = startRow; row <= endRow; row++) { if (!Positions.Any(pt => pt.Column == oldPosition.Column && pt.Row == row)) Positions.Add(new Position(oldPosition.Column, row)); } } if (oldPosition.Row == CurBranch.Position.Row) { int startCol = oldPosition.Column < CurBranch.Position.Column ? oldPosition.Column : CurBranch.Position.Column; int endCol = oldPosition.Column > CurBranch.Position.Column ? oldPosition.Column : CurBranch.Position.Column; for (int col = startCol; col <= endCol; col++) { if (!Positions.Any(pt => pt.Column == col && pt.Row == oldPosition.Row)) Positions.Add(new Position(col, oldPosition.Row)); } } } } } |
Gameクラスの定義
次にGameクラスを定義します。このなかでBlock型とPlayer型が出てきますが、次回の記事を参照してください。
1 2 3 4 5 6 |
namespace PengoGame { public class Game { } } |
以降は以下のように名前空間を省略して書きます。
1 2 3 |
public class Game { } |
まず定数とフィールド変数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Game { public const int CHARACTER_SIZE = 32; // キャラクタサイズは32ピクセルとする public const int RowMax = 15; // フィールドは15×15 public const int ColMax = 15; // 火花と破壊されたブロックが消滅するまでの時間 約1/24秒×12 public const int TIME_UNTIL_DISAPPEARS = 12; // 同時にプレイできる人数 public const int PLAYER_MAX = 8; // 残機数 public const int REST_MAX = 5; // 復帰後無敵である時間 約1/24秒×24×3 public const int INVINCIBLE_TIME = 24 * 3; // プレイヤーの辞書 public static Dictionary<string, Player> Players = new Dictionary<string, Player>(); public static List<Player> NPCs = new List<Player>(); static Random _random = new Random(); } |
プロパティ
Blocksプロパティはフィールド上に残っているブロックです。
1 2 3 4 5 6 7 8 |
public class Game { static List<Block> _blocks = new List<Block>(); public static List<Block> Blocks { get { return _blocks; } } } |
プレイヤーが撃破されたときに火花を周囲に飛ばします。Sparksプロパティは消滅前の火花です。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Game { public static List<Spark> _sparks = new List<Spark>(); public static List<Spark> Sparks { get { _sparks = _sparks.Where(fire => fire.TimeToDisappearance > 0).ToList(); return _sparks; } } } |
AllPlayersプロパティはNPC(non player character)を含む全プレイヤーです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Game { public static List<Player> AllPlayers { get { List<Player> players = new List<Player>(NPCs); foreach (Player player in Players.Values) players.Add(player); return players.OrderBy(player => player.PlayerNumber).ToList(); } } } |
初期化の処理
前述のMaseGeneratorクラスからアイスブロックで迷路をつくります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Game { public static void Init() { // 既存のブロックは破棄する Blocks.Clear(); // 迷路を生成する MaseGenerator maseGenerator = new MaseGenerator(8, 8); List<Position> positions = maseGenerator.Positions; for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { // 生成した迷路の通路ではない部分にブロックを配置する if(!positions.Any(pos => pos.Column == col && pos.Row == row)) Blocks.Add(new Block(col, row)); } } } } |
ゲームが進行してブロックが少なくなるとブロックを追加します。まずGetAddBlockPositionsメソッドでブロックを追加する位置を決めます。追加する場所が決まったら一定時間経過後にブロックを追加します。このときプレイヤーが存在する場所とすでにブロックが存在する場所には追加しません。
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 |
public class Game { // 追加する場所が決まってもすぐに追加するわけではないので一時保存しておく static List<Block> _addBlocks = new List<Block>(); public static List<Block> GetAddBlockPositions() { MaseGenerator maseGenerator = new MaseGenerator(8, 8); List<Position> positions = maseGenerator.Positions; for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { if (!positions.Any(pos => pos.Column == col && pos.Row == row)) _addBlocks.Add(new Block(col, row)); } } return _addBlocks; } public static List<Block> AddBlocks() { List<Block> addblocks = new List<Block>(); List<Player> players = AllPlayers; foreach (Block block in _addBlocks) { if(players.Any(player => (player.CurrentColumn == block.Column || player.NextColumn == block.Column) && (player.CurrentRow == block.Row || player.NextRow == block.Row) )) continue; if (Blocks.Any(oldBlock => oldBlock.Column == block.Column && oldBlock.Row == block.Row)) continue; Blocks.Add(block); addblocks.Add(block); } // 一時保存しておいたものをクリア _addBlocks.Clear(); return addblocks; } } |
火花の生成
プレイヤーが撃破されたら火花を周囲に飛ばしますが、そのための処理を示します。
まず火花の位置と状態を管理するクラスを示します。コンストラクタの引数は出現場所のXY座標とXY方向の初速です。
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 PengoGame { public class Spark { int _x = 0; int _y = 0; double _vx = 0; double _vy = 0; int _update = 0; public Spark(int x, int y, double vx, double vy) { TimeToDisappearance = Game.TIME_UNTIL_DISAPPEARS; // 消滅するまでの時間 約0.5秒 _x = x; _y = y; _vx = vx; _vy = vy; } public int X { private set; get; } public int Y { private set; get; } public int TimeToDisappearance { private set; get; } public void Update() { _update++; X = (int)(_x + _vx * _update); Y = (int)(_y + _vy * _update); TimeToDisappearance--; } } } |
プレイヤーが撃破されたらその座標で24個のSparkオブジェクトを生成し、ランダムに初速を与えます。そして更新処理がおこなわれるたびに周囲に移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Game { public static void SetSparks(int x, int y) { for (int i = 0; i < 24; i++) { int r = _random.Next(100); double rad = Math.PI * 2 * r / 100; double v = 8 + _random.Next(8); _sparks.Add(new Spark(x, y, v * Math.Cos(rad), v * Math.Sin(rad))); } } } |