今回はクラッシュローラーのようなゲームを作成します。これが完成したゲームです。
『クラッシュローラー』(CRUSH ROLLER)は、1981年にアルファ電子(後のADK)が開発したアーケードゲームです。パックマンのようなドットイートタイプのゲームです。プレイヤーは刷毛のようなキャラクターで、4方向レバーで敵を避けながら画面上の迷路を全て塗りつぶすことでステージクリアを目指します。
以下はクラッシュローラーのプレイ動画です。
Contents
通路を作成する
まず通路を作成しなければなりません。通路はプレイ動画をキャプチャして通路の部分のみを直線に置き換えたものを使います。
加工前
加工後
最初にフィード変数とコンストラクタを示します。
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 partial class Form1 : Form { int[,] Map = null; bool[,] IsVisits = null; // 画面の左と上側の余白 public const int TOP_MARGIN = 30; public const int LEFT_MARGIN = 30; // 通路を作成するために読み込んだBitmapの幅と高さ int MapSourceWidth = 0; int MapSourceHeight = 0; int ExpansionRate = 2; // 拡大率 int CharactorSize = 28; // キャラクタの大きさ int BorderWidth = 4; // 通路の輪郭の幅 int PlayerStartX = 0; // プレイヤーの座標 int PlayerStartY = 0; int PlayerX = 0; int PlayerY = 0; // 通路を作成するために読み込んだBitmapの各ピクセルが public const int POSITION_NONE = -1; // 通路ではない public const int POSITION_ROAD = 1; // 普通の通路 public const int POSITION_BRIDGE_NS = 2; // 南北の橋 public const int POSITION_BRIDGE_WE = 3; // 東西の橋 public const int POSITION_BRIDGE_NS_CROSS = 4; // 東西の橋と通路が交差している点 public const int POSITION_BRIDGE_WE_CROSS = 5; // 東西の橋と通路が交差している点 public const int POSITION_BRIDGE_EDGE = 6; // 橋の開始地点 public const int POSITION_BRIDGE_NEAR_EDGE = 7; // 橋の開始地点の周辺 Timer Timer1 = new Timer(); Timer Timer2 = new Timer(); List<Position> PositionsBridgeNearEdge = new List<Position>(); } |
最後の行で使われているPositionクラスは以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Position { public Position(int x, int y) { X = x; Y = y; } public int X { private set; get; } public int Y { private 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 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 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); BackColor = Color.Black; CreateMapFromBitmap(); InitTimer(); } void CreateMapFromBitmap() { Bitmap bitmap = Properties.Resources.maze1; MapSourceWidth = bitmap.Width; MapSourceHeight = bitmap.Height; Map = new int[MapSourceHeight, MapSourceWidth]; IsVisits = new bool[MapSourceHeight, MapSourceWidth]; for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { Color color = bitmap.GetPixel(x, y); if (color == Color.FromArgb(255, 255, 255)) Map[y, x] = POSITION_NONE; else if (color == Color.FromArgb(255, 0, 0)) Map[y, x] = POSITION_BRIDGE_NS; else if (color == Color.FromArgb(0, 0, 255)) Map[y, x] = POSITION_BRIDGE_WE; else if (color == Color.FromArgb(0, 255, 0)) { if ( bitmap.GetPixel(x, y + 1) == Color.FromArgb(255, 0, 0) && bitmap.GetPixel(x, y - 1) == Color.FromArgb(255, 0, 0) ) Map[y, x] = POSITION_BRIDGE_NS_CROSS; else if ( bitmap.GetPixel(x + 1, y) == Color.FromArgb(0, 0, 255) && bitmap.GetPixel(x - 1, y) == Color.FromArgb(0, 0, 255) ) Map[y, x] = POSITION_BRIDGE_WE_CROSS; else { Map[y, x] = POSITION_BRIDGE_EDGE; PositionsBridgeNearEdge.Add(new Position(x, y)); } } else if (color == Color.FromArgb(255, 255, 0)) { Map[y, x] = POSITION_ROAD; PlayerX = PlayerStartX = x; PlayerY = PlayerStartY = y; } else Map[y, x] = POSITION_ROAD; } } bitmap.Dispose(); for (int i = 0; i < PositionsBridgeNearEdge.Count; i++) { int x = PositionsBridgeNearEdge[i].X; int y = PositionsBridgeNearEdge[i].Y; for (int k = 1; k <= CharactorSize / ExpansionRate; k++) { if (Map[y, x + k] == POSITION_ROAD) Map[y, x + k] = POSITION_BRIDGE_NEAR_EDGE; if (Map[y, x - k] == POSITION_ROAD) Map[y, x - k] = POSITION_BRIDGE_NEAR_EDGE; if (Map[y + k, x] == POSITION_ROAD) Map[y + k, x] = POSITION_BRIDGE_NEAR_EDGE; if (Map[y - k, x] == POSITION_ROAD) Map[y - k, x] = POSITION_BRIDGE_NEAR_EDGE; } } } const int INIT_INTERVAL = 30; void InitTimer() { Timer1.Interval = INIT_INTERVAL; Timer1.Tick += Timer1_Tick; Timer1.Start(); Timer2.Interval = (int)(Timer1.Interval * 1.5); Timer2.Tick += Timer2_Tick; Timer2.Start(); } } |
通路を描画する処理を示します。二次元配列 Mapに格納されている値を調べて正の数であれば通路です。そこで先に通路の輪郭になる部分に一回り大きな矩形を描画し、そのなかに通路として白く塗りつぶされた矩形を描画します。またプレイヤーが通過した部分は別の色で塗りつぶします。
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 |
public partial class Form1 : Form { SolidBrush BorderBrush = new SolidBrush(Color.Blue); SolidBrush WhiteBrush = new SolidBrush(Color.White); SolidBrush PassedBrush = new SolidBrush(Color.Lime); void DrawRoad(Graphics graphics) { for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if (Map[y, x] > 0) graphics.FillRectangle( BorderBrush, new Rectangle( x * ExpansionRate - BorderWidth + LEFT_MARGIN, y * ExpansionRate - BorderWidth + TOP_MARGIN, CharactorSize + BorderWidth * 2, CharactorSize + BorderWidth * 2)); } } for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if (Map[y, x] > 0) { graphics.FillRectangle( WhiteBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } } for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if (Map[y, x] > 0 && IsVisits[y, x]) { graphics.FillRectangle( PassedBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } } } } |
通路の一部は立体交差になっています。橋の部分を描画する処理を示します。
はじめのループで交差している点を求め、その部分に境界線を描画して通路のうえに橋がかかっているように見せかけます。そのあと橋の内部を描画します。まだ通過していない場所は白で、通過している部分は別の色で塗りつぶします。
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 |
public partial class Form1 : Form { void DrawBridgeNS(Graphics graphics) { // 橋の輪郭を描画 for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if (Map[y, x] == POSITION_BRIDGE_NS_CROSS) { graphics.FillRectangle( BorderBrush, new Rectangle(x * ExpansionRate - BorderWidth + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, BorderWidth, CharactorSize)); graphics.FillRectangle( BorderBrush, new Rectangle(x * ExpansionRate + CharactorSize + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, BorderWidth, CharactorSize)); } } } // 橋の内部を描画 for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if (Map[y, x] == POSITION_BRIDGE_NS) graphics.FillRectangle( WhiteBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } // 橋の内部ですでにプレイヤーが通過している部分を描画 for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if ((Map[y, x] == POSITION_BRIDGE_NS) && IsVisits[y, x]) { graphics.FillRectangle( PassedBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } } } void DrawBridgeWE(Graphics graphics) { for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if (Map[y, x] == POSITION_BRIDGE_WE_CROSS) { graphics.FillRectangle( BorderBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate - BorderWidth + TOP_MARGIN, CharactorSize, BorderWidth)); graphics.FillRectangle( BorderBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate + CharactorSize + TOP_MARGIN, CharactorSize, BorderWidth)); } } } for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if (Map[y, x] == POSITION_BRIDGE_WE) graphics.FillRectangle( WhiteBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } for (int y = 0; y < MapSourceHeight; y++) { for (int x = 0; x < MapSourceWidth; x++) { if ((Map[y, x] == POSITION_BRIDGE_WE) && IsVisits[y, x]) graphics.FillRectangle( PassedBrush, new Rectangle(x * ExpansionRate + LEFT_MARGIN, y * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } } } |
プレイヤーを描画する処理を示します。今回は仮なのでオレンジ色の矩形を描画するだけです。
1 2 3 4 5 6 7 8 9 10 |
public partial class Form1 : Form { SolidBrush PlayerBrush = new SolidBrush(Color.Orange); void DrawPlayer(Graphics graphics) { graphics.FillRectangle( PlayerBrush, new Rectangle(PlayerX * ExpansionRate + LEFT_MARGIN, PlayerY * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } |
描画処理の全体を示します。立体交差になっている部分があるので、先に下になる部分の描画を先におこないます。橋の下をくぐろうとしているプレイヤーを描画してはいけないのでプレイヤーが橋の上にいるかいないかで描画するタイミングを変えています。
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 partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { // 通路を描画する DrawRoad(e.Graphics); // 各キャラクタ(いまはプレイヤーのみ)を描画する DrawPlayer(e.Graphics); // 橋を描画する DrawBridgeNS(e.Graphics); DrawBridgeWE(e.Graphics); // 橋の始点ですでに通過した部分が橋を描画する過程で白く描画されている場合は塗りなおす ReDrawBridgeEdgeIfNeed(e.Graphics); // 橋の上(周辺も含む)を通過中のキャラクタは最後に描画する ReDrawPlayerIfNeed(e.Graphics); base.OnPaint(e); } void ReDrawBridgeEdgeIfNeed(Graphics graphics) { foreach (Position position in PositionsBridgeNearEdge) { if (IsVisits[position.Y, position.X]) { graphics.FillRectangle( PassedBrush, new Rectangle(position.X * ExpansionRate + LEFT_MARGIN, position.Y * ExpansionRate + TOP_MARGIN, CharactorSize, CharactorSize)); } } } void ReDrawPlayerIfNeed(Graphics graphics) { if (IsInMap(PlayerY, PlayerX) && (Map[PlayerY, PlayerX] == POSITION_BRIDGE_EDGE || Map[PlayerY, PlayerX] == POSITION_BRIDGE_NEAR_EDGE || Map[PlayerY, PlayerX] == POSITION_BRIDGE_NS || Map[PlayerY, PlayerX] == POSITION_BRIDGE_WE)) { DrawPlayer(graphics); } } } |
プレイヤーを操作する
プレイヤーを操作できるようにします。まず移動する方向を管理するための列挙体を作ります。
1 2 3 4 5 6 7 8 |
public enum Direct { Stop, 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 |
public partial class Form1 : Form { bool IsKeyUp = false; bool IsKeyDown = false; bool IsKeyLeft = false; bool IsKeyRight = false; protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Up) IsKeyUp = true; if (e.KeyCode == Keys.Down) IsKeyDown = true; if (e.KeyCode == Keys.Left) IsKeyLeft = true; if (e.KeyCode == Keys.Right) IsKeyRight = true; base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Up) IsKeyUp = false; if (e.KeyCode == Keys.Down) IsKeyDown = false; if (e.KeyCode == Keys.Left) IsKeyLeft = false; if (e.KeyCode == Keys.Right) IsKeyRight = false; base.OnKeyDown(e); } } |
方向転換できるタイミングで方向転換できる方向のキーがおされている場合、方向転換の処理がおこなわれ、PlayerDirectに移動方向が格納されます。以下のメソッドはキーが押されているかどうかでプレイヤーの移動方向を求めてPlayerDirectに格納する処理をおこなう処理をしています。
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 |
public partial class Form1 : Form { // プレイヤーが移動する方向 Direct PlayerDirect = Direct.Stop; // 引数の座標は配列の範囲内か? bool IsInMap(int y, int x) { if (y < 0) return false; if (y >= MapSourceHeight) return false; if (x < 0) return false; if (x >= MapSourceWidth) return false; return true; } void SetPlayerDirect() { // 橋の上または下では直進しかできない if (IsInMap(PlayerY, PlayerX) && Map[PlayerY, PlayerX] != POSITION_BRIDGE_NS_CROSS && Map[PlayerY, PlayerX] != POSITION_BRIDGE_WE_CROSS) { if (IsKeyUp) { if (Map[PlayerY - 1, PlayerX] > 0) PlayerDirect = Direct.Up; } if (IsKeyDown) { if (Map[PlayerY + 1, PlayerX] > 0) PlayerDirect = Direct.Down; } if (IsKeyLeft) { if (Map[PlayerY, PlayerX - 1] > 0) PlayerDirect = Direct.Left; } if (IsKeyRight) { if (Map[PlayerY, PlayerX + 1] > 0) PlayerDirect = Direct.Right; } } } } |
実際にプレイヤーを移動させる処理です。PlayerDirectに格納されている方向に移動処理がおこなわれます。
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 |
public partial class Form1 : Form { void MovePlayer() { // SetPlayerDirectメソッドによって移動方向がPlayerDirectに格納される SetPlayerDirect(); // 移動処理 if (PlayerDirect == Direct.Up) { if (PlayerY - 1 >= 0 && Map[PlayerY - 1, PlayerX] > 0) PlayerY--; // 画面端に来たらワープ処理 if (PlayerY - 1 == -1) { IsVisits[PlayerY, PlayerX] = true; PlayerY = MapSourceHeight - 1; } } if (PlayerDirect == Direct.Down) { if (Map[PlayerY + 1, PlayerX] > 0) PlayerY++; if (PlayerY + 1 == MapSourceHeight) { IsVisits[PlayerY, PlayerX] = true; PlayerY = 0; } } if (PlayerDirect == Direct.Left) { if (Map[PlayerY, PlayerX - 1] > 0) PlayerX--; if (PlayerX - 1 == -1) { IsVisits[PlayerY, PlayerX] = true; PlayerX = MapSourceWidth - 1; } } if (PlayerDirect == Direct.Right) { if (Map[PlayerY, PlayerX + 1] > 0) PlayerX++; if (PlayerX + 1 == MapSourceWidth) { IsVisits[PlayerY, PlayerX] = true; PlayerX = 0; } } if(PlayerX < MapSourceWidth && PlayerY < MapSourceHeight) IsVisits[PlayerY, PlayerX] = true; // 通路を塗りつぶしたら点を加算する(後述) AddPointsIfNeed(); } void AddPointsIfNeed() { // あとまわし } } |
タイマーイベントが発生したらMovePlayerメソッドを呼び出して再描画処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { private void Timer1_Tick(object sender, EventArgs e) { MovePlayer(); Invalidate(); } private void Timer2_Tick(object sender, EventArgs e) { // あとで使う } } |