前回はフィールド上(フィールド変数と紛らわしい)を歩くだけのプログラムでしたが、城からフィールド、フィールドから城へと移動できるようにします。城はマップの上に描画されているので、それと重なる位置に移動したら城のなかに入ることができるようにします。
Contents
Mapクラスにメソッドを追加
すべてのマップの基底クラスになる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 |
public class Map { protected void GetRowColumnFromChar(string mapText, char c, ref int column, ref int row) { string[] vs1 = mapText.Split('\n'); for (row = 0; row < vs1.Length; row++) { if (vs1[row].IndexOf(c) == -1) continue; char[] vs2 = vs1[row].ToCharArray(); for (column = 0; column < vs2.Length; column++) { if (vs2[column] == c) { return; } } } // 見つからない場合は -1とする column = -1; row = -1; } } |
主人公の現在位置はダンジョンがある位置か?
これは引数として渡されたブロック(Rectangle)のリストのなかに点(x, y)が存在するのかどうかを調べるメソッドです。主人公の現在位置が城やダンジョンのある位置の内部なのかどうかを調べるときに使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Map { protected bool IsPositionBlocks(int positionX, int positionY, List<Rectangle> rectangles) { int right = positionX + Block.Width; int bottom = positionY + Block.Height; if (rectangles.Any(rect => rect.Left <= positionX && positionX <= rect.Right && rect.Top <= positionY && positionY <= rect.Bottom)) return true; if (rectangles.Any(rect => rect.Left <= right && right <= rect.Right && rect.Top <= positionY && positionY <= rect.Bottom)) return true; if (rectangles.Any(rect => rect.Left <= positionX && positionX <= rect.Right && rect.Top <= bottom && bottom <= rect.Bottom)) return true; if (rectangles.Any(rect => rect.Left <= right && right <= rect.Right && rect.Top <= bottom && bottom <= rect.Bottom)) return true; return false; } } |
Map1クラスにメソッドを追加
Map1クラスに以下を追加します。
まず城のなかに入ったときは城の内部を描画できるようにしなければなりません。ふたつの城は両方とも下から城に入るようになっています。そこで上に移動したときに城の内部に入っているかどうかを調べるIsCastleメソッドとIsEnenyCastleメソッドを作成します。
プレイヤーが城に入ったかどうか?
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 |
public class Map1 : Map { bool IsCastle(int newX, int newY) { int column = 0; int row = 0; GetRowColumnFromChar(MapText, '城', ref column, ref row); List<Rectangle> rects = new List<Rectangle>(); for (int y = 0; y < 3; y++) { for (int x = 0; x < 3; x++) { int column1 = column + x; int row1 = row + y; rects.Add(new Rectangle(new Point(column1 * Block.Width, row1 * Block.Width), new Size(Block.Width, Block.Height))); } } return IsPointBlocks(newX, newY, rects); } bool IsEnenyCastle(int newX, int newY) { int column = 0; int row = 0; GetRowColumnFromChar(MapText, '魔', ref column, ref row); List<Rectangle> rects = new List<Rectangle>(); for (int y = 0; y < 3; y++) { for (int x = 0; x < 3; x++) { int column1 = column + x; int row1 = row + y; rects.Add(new Rectangle(new Point(column1 * Block.Width, row1 * Block.Width), new Size(Block.Width, Block.Height))); } } return IsPointBlocks(newX, newY, rects); } } |
城のなかに移動した場合のイベントを定義
城がある位置に移動した場合はフィールドではなく城の内部を描画させたいのでEnterCastleイベントとEnterEnemyCastleイベントを定義します。
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 Map1 : Map { public event EventHandler EnterCastle; public event EventHandler EnterEnemyCastle; public override void MovePlayer(Direct direct) { if (direct == Direct.Up) { if (IsCastle(PlayerPoint.X, PlayerPoint.Y - 4)) { EnterCastle?.Invoke(this, new EventArgs()); BgmStop(); return; } if (IsEnenyCastle(PlayerPoint.X, PlayerPoint.Y)) { EnterEnemyCastle?.Invoke(this, new EventArgs()); BgmStop(); return; } } base.MovePlayer(direct); } } |
BGMを再生したり停止できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Map1 : Map { WMPLib.WindowsMediaPlayer player = new WMPLib.WindowsMediaPlayer(); public void BgmStart() { player.URL = Application.StartupPath + "\\bgm-field.mp3"; } public void BgmStop() { player.controls.stop(); } } |
Form1クラスの変更
Form1クラスを少し変更します。
コンストラクタが呼び出されたら自作メソッド InitDungeonsを実行します。ここでMap1クラス、MapCastleクラスのインスタンスを生成します。
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 |
public partial class Form1 : Form { Map _map = null; Map1 map1 = null; MapCastle mapCastle1 = null; public Form1() { InitializeComponent(); DoubleBuffered = true; this.FormBorderStyle = FormBorderStyle.FixedSingle; this.StartPosition = FormStartPosition.CenterScreen; InitDungeons(); _map = map1; map1.BgmStart(); } void InitDungeons() { map1 = new Map1(this); map1.EnterCastle += Map1_EnterCastle; map1.EnterEnemyCastle += Map1_EnterEnemyCastle; mapCastle = new MapCastle(this, 0); // MapCastleクラスは後述 mapCastle.ExitCastle += MapCastle1_ExitCastle; mapEnemyCastle = new MapCastle(this, 1); mapEnemyCastle.ExitCastle += MapCastle1_ExitCastle; } } |
城に入るとき、城から出るときのイベントが発生したら表示を切り替えます。
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 { // フィールドから城に入る private void Map1_EnterCastle(object sender, EventArgs e) { Task.Run(() => { // 一瞬画面全体を黒くする _map = null; System.Threading.Thread.Sleep(500); _map = mapCastle; Invalidate(); }); } // フィールドから敵の城に入る private void Map1_EnterEnemyCastle(object sender, EventArgs e) { Task.Run(() => { _map = null; System.Threading.Thread.Sleep(500); _map = mapEnemyCastle; Invalidate(); }); } // 城(敵味方関係なく)からフィールドに出る private void MapCastle1_ExitCastle(object sender, EventArgs e) { Task.Run(() => { _map = null; System.Threading.Thread.Sleep(500); _map = map1; map1.BgmStart(); Invalidate(); }); } } |
描画処理にかんする部分を示します。_map == nullの場合は全面を黒にします。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { if (_map != null) _map.Draw(e.Graphics); else this.BackColor = Color.Black; base.OnPaint(e); } } |
城の内部を描画するクラス
城の内部を描画するクラスを作成します。
城はとりあえず2つだけ作ります。コンストラクタの引数で自分の城と敵の城を区別します。第二引数が0なら自分の城、1なら敵の城です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class MapCastle : Map { protected Form1 _form1 = null; WMPLib.WindowsMediaPlayer player = new WMPLib.WindowsMediaPlayer(); public MapCastle(Form1 form1, int id) { _form1 = form1; if (id == 1) MapText = MapText.Replace("王", "魔"); Init(); // 城に入ったときに最初に表示される場所 PlayerPoint.X = 16 * Block.Width; PlayerPoint.Y = 14 * Block.Height; } } |
城の初期化
初期化の処理を示します。敵の城の場合は「王」の位置に魔王を表示させます。
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 |
class MapCastle : Map { string MapText = "" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 王 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁 壁~~~~~~~\n" + "~~~~~~~壁壁壁壁壁壁壁壁 壁壁壁壁壁壁壁~~~~~~~\n" + "~~~~~~~壁壁壁壁壁壁壁壁段段段壁壁壁壁壁壁壁~~~~~~~\n" + "~~~~~~~~~~~~~~壁段段段壁~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~壁段段段壁~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~壁段段段壁~~~~~~~~~~~~~\n"; public void Init() { string[] vs = MapText.Split('\n'); for (int y = 0; y < vs.Length; y++) { string str = vs[y]; char[] vs1 = str.ToCharArray(); for (int x = 0; x < vs1.Length; x++) { if (vs1[x] == '~') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.Sea)); else if (vs1[x] == '壁') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.Wall1)); else if (vs1[x] == '段' || vs1[x] == '1') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.Stairs1)); else if (vs1[x] == ' ') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.Floor1)); else if (vs1[x] == '王') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.Queen1)); else if (vs1[x] == '魔') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.Devil1)); if (vs1[x] == ' ' || vs1[x] == '段') ImageRectangles.Last().CanMove = true; } } } } |
城内の描画処理
ImageRectangle.DrawWithShiftメソッドに以下を追加してBlockTypeが壁、床、階段、王女、魔王のとき適切な描画ができるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class ImageRectangle { public void DrawWithShift(Graphics graphics, int x, int y) { // 以前から存在したものは省略 // BlockTypeが壁、床、階段、王女、魔王のとき適切な描画ができるようにする if (BlockType == BlockType.Wall1) graphics.DrawImage(BlockImage.GetImageWall1(), rect); if (BlockType == BlockType.Floor1) graphics.DrawImage(BlockImage.GetImageFloor1(), rect); if (BlockType == BlockType.Stairs1) graphics.DrawImage(BlockImage.GetImageStairs1(), rect); if (BlockType == BlockType.Queen1) graphics.DrawImage(BlockImage.GetImageQueen1(), rect); if (BlockType == BlockType.Devil1) graphics.DrawImage(BlockImage.GetImageDevil1(), rect); } } |
BlockTypeとBlockImageクラスにメソッドを追加して、壁、床、王、魔王のイメージを取得できるようにします。
1 2 3 4 5 6 7 8 9 10 11 |
public enum BlockType { // 以前からあったものは省略 // 追加 Wall1, Floor1, Stairs1, Queen1, Devil1, } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class BlockImage { // とりあえず壁のイメージだけ。他も適当に画像を拾ってきてBitmapを返せるようにする static Bitmap _ImageWall1 = null; public static Bitmap GetImageWall1() { if (_ImageWall1 != null) return _ImageWall1; _ImageWall1 = GetBitmap(Properties.Resources.wall, 0, 0); return _ImageWall1; } } |
これで城の内部が描画されるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class MapCastle : Map { public override void Draw(Graphics graphics) { _form1.BackColor = Color.Blue; foreach (ImageRectangle rect in ImageRectangles) rect.DrawWithShift(graphics, -PlayerPoint.X + _form1.ClientSize.Width / 2, -PlayerPoint.Y + _form1.ClientSize.Height / 2); int centerX = _form1.ClientSize.Width / 2; int centerY = _form1.ClientSize.Height / 2; graphics.DrawImage(BlockImage.GetImageCharctor1(), centerX, centerY, Block.Width, Block.Height); } } |
BGMの開始と終了
BGMを開始したり終了できるようなメソッドを作成します。BGMはフィールド上と同じものを使用しています(サンプルなので手抜きです)。
1 2 3 4 5 6 7 8 9 10 11 12 |
class MapCastle : Map { public void BgmStart() { player.URL = Application.StartupPath + "\\bgm-field.mp3"; } public void BgmStop() { player.controls.stop(); } } |
城の外に移動する処理
ExitCastleイベントを定義して出口に移動したら城の外に出る処理がおこなえるようにしておきます。
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 |
class MapCastle : Map { public event EventHandler ExitCastle; public override void MovePlayer(Direct direct) { if (direct == Direct.Down) { if (IsExit(PlayerPoint.X, PlayerPoint.Y)) { BgmStop(); ExitCastle?.Invoke(this, new EventArgs()); return; } } if (direct == Direct.Up && IsQueen(PlayerPoint.X, PlayerPoint.Y - 4)) { // 王女に接触すると会話ができるようにするが次回にする return; } if (direct == Direct.Down && IsQueen(PlayerPoint.X, PlayerPoint.Y + 4)) { return; } if (direct == Direct.Left && IsQueen(PlayerPoint.X - 4, PlayerPoint.Y)) { return; } if (direct == Direct.Right && IsQueen(PlayerPoint.X + 4, PlayerPoint.Y)) { return; } if (direct == Direct.Up && IsDevil(PlayerPoint.X, PlayerPoint.Y - 4)) { // 魔王に接触すると戦闘が発生するが次回以降にする return; } if (direct == Direct.Down && IsDevil(PlayerPoint.X, PlayerPoint.Y + 4)) { return; } if (direct == Direct.Left && IsDevil(PlayerPoint.X - 4, PlayerPoint.Y)) { return; } if (direct == Direct.Right && IsDevil(PlayerPoint.X + 4, PlayerPoint.Y)) { return; } // if文にかからなかった場合は通常の移動をする base.MovePlayer(direct); } } |
以下は移動しようとしている場所に出口や人がいるかどうかを調べるためのメソッドです。
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 |
class MapCastle : Map { bool IsExit(int x, int y) { if (15 * Block.Width <= x && x <= 18 * Block.Width && 15 * Block.Height <= y && y <= 17 * Block.Height) return true; else return false; } bool IsQueen(int x, int y) { int column = 0; int row = 0; this.GetRowColumnFromChar(MapText, '王', ref column, ref row); List<Rectangle> rects = new List<Rectangle>(); rects.Add(new Rectangle(column * Block.Width, row * Block.Height, Block.Width, Block.Height)); return IsPositionBlocks(x, y, rects); } bool IsDevil(int x, int y) { int column = 0; int row = 0; this.GetRowColumnFromChar(MapText, '魔', ref column, ref row); List<Rectangle> rects = new List<Rectangle>(); rects.Add(new Rectangle(column * Block.Width, row * Block.Height, Block.Width, Block.Height)); return IsPositionBlocks(x, y, rects); } } |
これでフィールドから城へ、城からフィールド上への移動ができるようになりました。