今回はマッピー(MAPPY)をつくります。
Contents
マッピー(MAPPY)とは
マッピー(MAPPY)は1983年5月25日にナムコ(現・バンダイナムコエンターテインメント)よりリリースされたアーケードゲーム。 主人公であるネズミの警官・マッピーを操り、トランポリンやパワードアを使って泥棒猫・ニャームコとその手下の猫・ミューキーズを避けながら、ステージに点在する盗品の数々を取り返していく、アイテム回収型アクションゲームです。
下は本物のマッピーのプレイ動画です。
主な仕様
プレイヤーは黄色いひよこ、敵は青いひよことする。ニャームコとミューキーズの区別はない。
トランポリンで上昇降下しているときは当たり判定はない。
本物のマッピーでは同じアイテムを連続して回収すると得点が2倍になるルールがあるが、これは無視する。
本物のマッピーでは一定時間が経過すると “HURRY” の文字が画面を横切り、音楽のテンポが上がると同時に敵が増え、動きも早くなる。さらに一定時間経過するとご先祖様が登場するが、これも無視する。
ではさっそく作成することにします。名前空間はMappyとします。
マップをつくる
最初に画面上に表示する床やアイテムの位置を定義するためのマップをつくります。
Wは左側の壁、wは右側の壁、Fは床、Eは敵の初期位置、Pはプレイヤーの初期位置、Lは左に開くパワードア、lは左に開く普通のドア、Rは右に開くパワードア、rは右に開く普通のドア、Tはトランポリン、tはトランポリンで上下に移動できる場所、Cはトランポリンの天井、Aは左右に移動できる部分、Sは画面の外に飛ばされた敵が復活する場所です。
ステージの番号が奇数の場合はmappy-map1を使い、偶数の場合はmappy-map2を使います。
mappy-map1.txt
1 2 3 4 5 6 7 8 9 10 11 12 |
S # WCFFFFFCFFFFFCFFFFFCw WtAERAAtAEAlAtAElAAtw WtFFFFFtFFFFFtFFFFFtw WtAAEAAtAAEAAtALEAAtw WtFFFFFtFFFFFtFFFFFtw WtAElAAtAEAAAtAErAAtw WtFFFFFtFFFFFtFFFFFtw WtAAEAAtARAEAtAAAAAtw WtFFFFFtFFFFFtFFFFFtw WtAEArAtAEAAAtAALAPtw WTFFFFFTFFFFFTFFFFFTw |
mappy-map2.txt
1 2 3 4 5 6 7 8 9 10 11 12 |
S # WCFFFFFCFFFFFCFFFFFCw WtAERAAtAEAlAtAElAAtw WtFFFFFtFFFFFtFFFFFtw WtAAEAAtAAEAAtALEAAtw WtFFFFFtFFCFFtFFFFFtw WtAElAAtAEtAAtAErAAtw WtFFFFFTFFtFFTFFFFFtw WtAAEAAARAtEAAAAAAAtw WtFFFFFFFFtFFFFFFFFtw WtAEArAAAEtAAAAALAPtw WTFFFFFFFFTFFFFFFFFTw |
Cell列挙体
マップを構成するセルの列挙体を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
namespace Mappy { public enum Cell { None, // なにもない LeftWall, // ステージの左端の壁 RightWall, // ステージの右端の壁 Floor, // 床 Hole, // 穴(トランポリンで上下に移動できる) Aisle, // 通路 Trampoline, // トランポリン Ceiling, // トランポリンで移動できる部分の天井 LeftOpenDoor, // 左に開く普通のドア RightOpenDoor, // 右に開く普通のドア LeftOpenPowerDoor, // 左に開くパワードア RightOpenPowerDoor, // 右に開くパワードア } } |
Positionクラス
座標を格納するためのPositionクラスを定義します。
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 |
namespace Mappy { public class Position { public Position() { } public Position(int row, int col) { Column = col; Row = row; } public int Column { protected set; get; } public int Row { protected set; get; } public int X { get { return Column * MappyGame.CHARACTER_SIZE; } } public int Y { get { return Row * MappyGame.CHARACTER_SIZE; } } } } |
Itemクラス
アイテムの座標と状態を格納するためのItemクラスを定義します。
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 |
namespace Mappy { public class Item : Position { public Item(Position position, int number) { Column = position.Column; Row = position.Row; Number = number; Passed = false; } public int Number { private set; get; } public bool Passed // trueなら回収されている { set; get; } } } |
Trampolineクラス
トランポリンの座標と状態を格納するためのTrampolineクラスを定義します。
プレイヤーが同じトランポリンを連続で使用できる回数は4回までです。4回使用するとトランポリンは消滅し、5回目に着地するとミス時の処理がおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace Mappy { public class Trampoline : Position { public Trampoline(Position position) { Column = position.Column; Row = position.Row; Life = 4; } public int Life { set; get; } public void Reset() { Life = 4; } } } |
Doorクラス
ドアの座標と状態を格納するためのDoorクラスを定義します。プロパティは左右どちら側に開くドアなのか、現在の状態は開いているのか閉まっているのか、それはパワードアなのかどうかです。パワードアは一度開くと弾丸を発射し、そのあとは普通のドアになってしまいます。
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 |
namespace Mappy { public class Door: Position { public Door(int row, int col, Direct direct, bool isPowerDoor) { Row = row; Column = col; OpenDirect = direct; IsOpen = false; IsPowerDoor = isPowerDoor; } public Direct OpenDirect { get; } public bool IsOpen { set; get; } public bool IsPowerDoor { set; get; } } } |
Sparkクラス
キャラクタ死亡時に発生する火花の座標と状態を格納するSparkクラスを定義します。コンストラクタの引数は発生場所のX座標とY座標、初速のX成分とY成分です。
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 |
namespace Mappy { public class Spark { public Spark(int x, int y, int vx, int vy) { Life = MappyGame.SPARK_LIFE_MAX; X = x; Y = y; VelocityX = vx; VelocityY = vy; } public int X { private set; get; } public int Y { private set; get; } int VelocityX { get; } int VelocityY { get; } public int Life { private set; get; } public void Update() { X += VelocityX; Y += VelocityY; Life--; } } } |
MappyGameクラスを定義する
ゲーム全体を管理するMappyGameクラスを定義します。
1 2 3 4 5 6 |
namespace Mappy { public class MappyGame { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class MappyGame { } |
定数部分
キャラクターのサイズは32ピクセルとし、1秒あたりの更新回数は24回とします。残機制で最初は5機、残機が0になったらゲームオーバーです。TIME_TO_START_MAXはステージクリアや自機死亡などでゲームが中断される更新回数、CANT_MOVE_COUNT_MAXは開閉するドアに巻き込まれた敵が動けなくなる更新回数、TIME_TO_ENEMY_REVIVE_MAXは画面外に押し出された敵が復活するまでの更新回数、SPARK_LIFE_MAXは爆発時の描画を繰り返す更新回数です。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MappyGame { public const int CHARACTER_SIZE = 32; public const int UPDATES_PER_SECOND = 24; public const int PLAYER_MAX = 5; public const int TIME_TO_START_MAX = UPDATES_PER_SECOND * 3; public const int CANT_MOVE_COUNT_MAX = UPDATES_PER_SECOND * 4; public const int TIME_TO_ENEMY_REVIVE_MAX = UPDATES_PER_SECOND * 5; public const int SPARK_LIFE_MAX = UPDATES_PER_SECOND / 2; } |
コンストラクタ
StageNumberはステージの番号、ColMaxとRowMaxはマップを構成する横の要素数と縦の要素数、Playerはプレイヤー、Enemies、Doors、Items、Trampolinesは敵、ドア、アイテム、トランポリンのリスト、EnemyRevivePositionは画面の外に飛ばれてた敵が復活する座標です。TimeToStartとTimeToPlayerReviveがゼロより大きいときは更新処理は行なわれません。MapArrayはマップの要素を格納する二次元配列です。
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 MappyGame { public MappyGame() { StageNumber = 0; ColMax = 0; RowMax = 0; Player = new Player(this); Enemies = new List<Enemy>(); Doors = new List<Door>(); Items = new List<Item>(); Trampolines = new List<Trampoline>(); EnemyRevivePosition = new Position(); TimeToStart = 0; TimeToPlayerRevive = 0; MapArray = new Cell[1, 1]; // 警告を消すためになにか入れておく Init(); // マップの初期化の処理(後述) } } |
各プロパティ
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 MappyGame { // ステージの番号 public int StageNumber { private set; get; } // マップ上の要素を格納する二次元配列 public Cell[,] MapArray { set; get; } // 二次元配列の列数 public int ColMax { set; get; } // 二次元配列の行数 public int RowMax { set; get; } // プレイヤー public Player Player { private set; get; } // 敵のリスト public List<Enemy> Enemies { private set; get; } // ドアのリスト public List<Door> Doors { private set; get; } // アイテムのリスト public List<Item> Items { private set; get; } // トランポリンのリスト public List<Trampoline> Trampolines { private set; get; } // TimeToStartが0より大きいときはゲームが中断されている public int TimeToStart { private set; get; } // 死亡した自機が復活するまでの時間 public int TimeToPlayerRevive { set; get; } // 画面の外に飛ばされた敵が復活する座標 public Position EnemyRevivePosition { private set; get; } } |
マップの初期化
文字列をリソースから読み込みマップを初期化をする処理を示します。
StageNumberの初期値は0なので最初にインクリメントして1にします。ステージの番号が偶数か奇数かで読み取るリソースを変更します。そのあと読み取った文字列をCreateArrayメソッドとCreateObjectListメソッドに渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class MappyGame { public void Init() { StageNumber++; string str; if (StageNumber % 2 == 1) str = Zero.Properties.Resources.mappy_map_1; else str = Zero.Properties.Resources.mappy_map_2; CreateArray(str); CreateObjectList(str); } } |
CreateArrayメソッドは二次元配列に値をセットしているだけです。
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 |
public class MappyGame { void CreateArray(string mapString) { string[] vs = mapString.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); RowMax = vs.Length; ColMax = vs[0].Length; MapArray = new Cell[RowMax, ColMax]; for (int row = 0; row < RowMax; row++) { char[] chars = vs[row].ToCharArray(); for (int col = 0; col < ColMax; col++) { if (chars[col] == 'A') MapArray[row, col] = Cell.Aisle; if (chars[col] == 'F') MapArray[row, col] = Cell.Floor; if (chars[col] == 'C') MapArray[row, col] = Cell.Ceiling; if (chars[col] == 'T') MapArray[row, col] = Cell.Trampoline; if (chars[col] == 't') MapArray[row, col] = Cell.Hole; if (chars[col] == 'l') MapArray[row, col] = Cell.LeftOpenDoor; if (chars[col] == 'r') MapArray[row, col] = Cell.RightOpenDoor; if (chars[col] == 'L') MapArray[row, col] = Cell.LeftOpenPowerDoor; if (chars[col] == 'R') MapArray[row, col] = Cell.RightOpenPowerDoor; if (chars[col] == 'E') MapArray[row, col] = Cell.Aisle; if (chars[col] == 'P') MapArray[row, col] = Cell.Aisle; if (chars[col] == 'W') MapArray[row, col] = Cell.LeftWall; if (chars[col] == 'w') MapArray[row, col] = Cell.RightWall; } } } } |
CreateObjectListメソッドでは自機の初期座標の取得、敵、トランポリン、ドア、アイテムを生成してリストに格納しています。
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 |
public class MappyGame { void CreateObjectList(string mapString) { string[] vs = mapString.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); Enemies.Clear(); Trampolines.Clear(); Doors = new List<Door>(); Items.Clear(); // 敵の初期座標になりそうな場所がたくさんあるが、第一ステージはこのなかから4箇所選ぶ List<Position> enemyInitPositions = new List<Position>(); List<Position> itemPositions = new List<Position>(); for (int row = 0; row < RowMax; row++) { char[] chars = vs[row].ToCharArray(); for (int col = 0; col < ColMax; col++) { if (chars[col] == 'T') Trampolines.Add(new Trampoline(new Position(row, col))); if (chars[col] == 'l') Doors.Add(new Door(row, col, Direct.Left, false)); if (chars[col] == 'r') Doors.Add(new Door(row, col, Direct.Right, false)); if (chars[col] == 'L') Doors.Add(new Door(row, col, Direct.Left, true)); if (chars[col] == 'R') Doors.Add(new Door(row, col, Direct.Right, true)); if (chars[col] == 'E') { enemyInitPositions.Add(new Position(row, col)); itemPositions.Add(new Position(row, col)); } if (chars[col] == 'P') Player.SetInitPosition(new Position(row, col)); if (chars[col] == 'S') EnemyRevivePosition = new Position(row, col); } } // 敵の初期座標の候補から(3 + StageNumber)個だけ選ぶ for (int i = 0; i < 3 + StageNumber; i++) { if (enemyInitPositions.Count == 0) break; int r = _random.Next(enemyInitPositions.Count); Enemies.Add(new Enemy(this, enemyInitPositions[r])); enemyInitPositions.RemoveAt(r); } // アイテムの順序をランダムに変更して配点を変更する for (int i = 0; i < 10; i++) { if (itemPositions.Count == 0) break; int r = _random.Next(itemPositions.Count); Items.Add(new Item(itemPositions[r], i / 2 + 1)); itemPositions.RemoveAt(r); } } } |
GetTrampolineメソッドとGetDoorメソッド
GetTrampolineメソッドとGetDoorメソッドはマップの行番号と列番号からトランポリンやドアがあるかどうかを調べて、存在する場合はそのオブジェクトを返します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MappyGame { public Trampoline? GetTrampoline(int row, int col) { return Trampolines.FirstOrDefault(_ => _.X == col * 32 && _.Y == row * 32); } public Door? GetDoor(int row, int col) { return Doors.FirstOrDefault(_ => _.X == col * 32 && _.Y == row * 32); } } |