なぜかファミコンのドラゴンクエストのようなゲームをつくってみたいと思ったので作ってみることにします。というか鳩でもわかるC#管理人はドラゴンクエストをしたことがありません。そこでつくるのは「ドラゴンクエストもどき」です。生暖かく見守りください。
Contents
Form1クラス
まずForm1クラスを示します。
ここでやっているのはマップを生成してキー操作をするとプレイヤーや上下左右に動くように描画処理をおこなうことです。マップはここではひとつだけつくります。後に城のなか、洞窟のなかのマップというように増やしていこうと考えています。もちろん今回はやりませんが、将来的には戦闘シーンも実装します。
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 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 { // 現在のマップ。ゲーム開始前はどこも選択されていない Map _map = null; public Form1() { InitializeComponent(); DoubleBuffered = true; this.FormBorderStyle = FormBorderStyle.FixedSingle; this.StartPosition = FormStartPosition.CenterScreen; // Map1を生成してフィールド変数 Mapに格納する // 同時にBGMを鳴らす Map1 map1 = new Map1(this); _map = map1; map1.BgmStart(); } protected override void OnKeyDown(KeyEventArgs e) { if (_map != null) { if (e.KeyCode == Keys.Left) _map.MovePlayer(Direct.Left); if (e.KeyCode == Keys.Up) _map.MovePlayer(Direct.Up); if (e.KeyCode == Keys.Right) _map.MovePlayer(Direct.Right); if (e.KeyCode == Keys.Down) _map.MovePlayer(Direct.Down); Invalidate(); } base.OnKeyDown(e); } protected override void OnPaint(PaintEventArgs e) { if(_map != null) _map.Draw(e.Graphics); base.OnPaint(e); } } |
これは移動する方向の列挙体です。
1 2 3 4 5 6 7 |
public enum Direct { Left, Up, Right, Down, } |
描画の単位になるブロックをつくる
次にマップを描画する単位になるブロックを作りますが、サイズは幅、高さともに32ピクセルとします。
1 2 3 4 5 6 7 8 9 10 11 |
public class Block { static public int Width { get { return 32; } } static public int Height { get { return 32; } } } |
ブロックの種類
ブロックの種類ですが、とりあえず以下のものを作ります。
1 2 3 4 5 6 7 8 9 10 11 |
public enum BlockType { None, // なにもない FlatGround, // 平原 32×32ピクセル 移動可能 SmallMountain, // 小さい山 32×32ピクセル 移動不可能 BigMountain, // 大きな山 64×64ピクセル 移動不可能 Sea, // 海 32×32ピクセル 移動不可能 BridgeEW, // 横方向の橋 32×32ピクセル 移動可能 Castle, // 城 96×96ピクセル 移動不可能 EnemyCastle, // 敵の城 96×96ピクセル 移動不可能 } |
描画するイメージを取得する
ブロックに描画するイメージを取得する処理をするクラスを作成します。
画像から入手した画像ファイルから必要な部分をきりとって使うのですが、この動画のなかでどうやって画像などの素材を入手したのかと質問したら丁寧にサイト名だけでなくurlも教えてもらえました。
ということでありがたく使わせていただきます。
マップチップ 32×32~ – ぴぽや倉庫からダウンロードしたファイルを解凍するとpipo-map001.pngというファイルがあるのでこれを使います。主人公はキャラクターチップのなかから選んだものを使いました。
画像には32×32のもの、64×64のもの、96×96のものがあります。右上の座標を引数にしてその部分を切り取ることができるメソッドをつくります。
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 |
public class BlockImage { static Bitmap GetBitmap(Bitmap resourceBitmap, int x, int y) { Bitmap bitmap = new Bitmap(32, 32); Graphics graphics = Graphics.FromImage(bitmap); graphics.DrawImage(resourceBitmap, new Rectangle(0, 0, 32, 32), new Rectangle(x, y, 32, 32), GraphicsUnit.Pixel); graphics.Dispose(); return bitmap; } static Bitmap GetBitmap2(Bitmap resourceBitmap, int x, int y) { Bitmap bitmap = new Bitmap(32 * 2, 32 * 2); Graphics graphics = Graphics.FromImage(bitmap); graphics.DrawImage(resourceBitmap, new Rectangle(0, 0, 32 * 2, 32 * 2), new Rectangle(x, y, 32 * 2, 32 * 2), GraphicsUnit.Pixel); graphics.Dispose(); return bitmap; } static Bitmap GetBitmap3(Bitmap resourceBitmap, int x, int y) { Bitmap bitmap = new Bitmap(32 * 3, 32 * 3); Graphics graphics = Graphics.FromImage(bitmap); graphics.DrawImage(resourceBitmap, new Rectangle(0, 0, 32 * 3, 32 * 3), new Rectangle(x, y, 32 * 3, 32 * 3), GraphicsUnit.Pixel); graphics.Dispose(); return bitmap; } } |
上記のメソッドをつかって主人公とマップに使うBitmapを取得します。
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 |
public class BlockImage { static Bitmap _ImageCharctor1 = null; public static Bitmap GetImageCharctor1() { if (_ImageCharctor1 != null) return _ImageCharctor1; _ImageCharctor1 = GetBitmap(Properties.Resources.pipo_charachip021e, 0, 0); return _ImageCharctor1; } static Bitmap _ImageFlatGround = null; public static Bitmap GetImageFlatGround() { if (_ImageFlatGround != null) return _ImageFlatGround; _ImageFlatGround = GetBitmap(Properties.Resources.pipo_map001, 0, 0); return _ImageFlatGround; } static Bitmap _ImageSmallMountain = null; public static Bitmap GetImageSmallMountain() { if (_ImageSmallMountain != null) return _ImageSmallMountain; _ImageSmallMountain = GetBitmap(Properties.Resources.pipo_map001, 128, 32); return _ImageSmallMountain; } static Bitmap _ImageBigMountain = null; public static Bitmap GetImageBigMountain() { if (_ImageBigMountain != null) return _ImageBigMountain; _ImageBigMountain = GetBitmap2(Properties.Resources.pipo_map001, 0, 128); return _ImageBigMountain; } static Bitmap _ImageBridgeEW = null; public static Bitmap GetImageBridgeEW() { if (_ImageBridgeEW != null) return _ImageBridgeEW; // 橋なので背景を海の色(青)にする Bitmap bitmap = new Bitmap(32, 32); Graphics graphics = Graphics.FromImage(bitmap); graphics.Clear(Color.Blue); graphics.DrawImage(Properties.Resources.pipo_map001, new Rectangle(0, 0, 32, 32), new Rectangle(0, 80, 32, 32), GraphicsUnit.Pixel); _ImageBridgeEW = bitmap; return _ImageBridgeEW; } static Bitmap _ImageCastle = null; public static Bitmap GetImageCastle() { if (_ImageCastle != null) return _ImageCastle; _ImageCastle = GetBitmap3(Properties.Resources.pipo_map001, 0, 32 * 11); return _ImageCastle; } static Bitmap _ImageEnemyCastle = null; public static Bitmap GetImageEnemyCastle() { if (_ImageEnemyCastle != null) return _ImageEnemyCastle; _ImageEnemyCastle = GetBitmap3(Properties.Resources.pipo_map001, 0, 32 * 14); return _ImageEnemyCastle; } } |
描画する矩形を管理するクラス
次にImageRectangleクラスをつくります。コンストラクタは2つ。マップのなかで何列目何行目になるのか、表示色は何色か?ブロックの種類は何かという情報をコンストラクタに渡します。RectangleプロパティはColumnとRowとブロックのサイズからこのブロックがはいる矩形を返します。またフィールド変数のCanMoveはこのブロックのなかにプレイヤーが移動できるかどうかを示すものです。
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 ImageRectangle { public ImageRectangle(int column, int row, Color color) { Column = column; Row = row; } public ImageRectangle(int column, int row, BlockType blockType) { Column = column; Row = row; BlockType = blockType; } public int Column { private set; get; } public int Row { private set; get; } public Color Color { private set; get; } public BlockType BlockType { private set; get; } public Rectangle Rectangle { get { return new Rectangle(Column * Block.Width, Row * Block.Height, Block.Width, Block.Height); } } public bool CanMove = false; } |
描画のための処理を示します。マップを描画するときは主人公が中央に表示されるため、そのぶんブロック全体を平行に移動させて描画しなければなりません。最初の1行でブロックを描画すべき位置を求めています。あとはその位置に描画するだけです。
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 |
public partial class ImageRectangle { public void DrawWithShift(Graphics graphics, int x, int y) { Rectangle rect = new Rectangle(Column * Block.Width + x, Row * Block.Height + y, Block.Width, Block.Height); if (BlockType == BlockType.FlatGround) graphics.DrawImage(BlockImage.GetImageFlatGround(), rect); if (BlockType == BlockType.BridgeEW) graphics.DrawImage(BlockImage.GetImageBridgeEW(), rect); if (BlockType == BlockType.Sea) graphics.FillRectangle(Brushes.Blue, rect); if (BlockType == BlockType.SmallMountain) graphics.DrawImage(BlockImage.GetImageSmallMountain(), rect); if (BlockType == BlockType.BigMountain) { Rectangle rect2 = new Rectangle(rect.X, rect.Y, rect.Width * 2, rect.Height * 2); graphics.DrawImage(BlockImage.GetImageBigMountain(), rect2); } if (BlockType == BlockType.Castle) { Rectangle rect3 = new Rectangle(rect.X, rect.Y, rect.Width * 3, rect.Height * 3); graphics.DrawImage(BlockImage.GetImageCastle(), rect3); } if (BlockType == BlockType.EnemyCastle) { Rectangle rect3 = new Rectangle(rect.X, rect.Y, rect.Width * 3, rect.Height * 3); graphics.DrawImage(BlockImage.GetImageEnemyCastle(), rect3); } } } |
小さい石の場合、はしのほうはなにも描画されていません。そこで最初に陸地の部分には草原のイメージを書き込んでそれから山や城を書き込むことにします。
1 2 3 4 5 6 7 8 9 |
public partial class ImageRectangle { public void DrawFlatGroundWithShift(Graphics graphics, int x, int y) { Rectangle rect = new Rectangle(Column * Block.Width + x, Row * Block.Height + y, Block.Width, Block.Height); graphics.DrawImage(BlockImage.GetImageFlatGround(), rect); } } |
マップをつくる
それではマップをつくります。今回つくるマップはひとつだけですが、複数作成して切り替えて使うことを考えています。
基底クラスのMapクラス
これはマップの基底クラスです。フィールド変数はImageRectangleオブジェクトを格納するリストとプレイヤーの位置を格納するためのものです。
1 2 3 4 5 6 7 8 9 |
public class Map { protected Point PlayerPoint = Point.Empty; protected List<ImageRectangle> ImageRectangles = new List<ImageRectangle>(); public Map() { } } |
CanMoveメソッドはプレイヤーが新しい座標に移動できるかどうかを調べます。プレイヤーを矩形とみなして4つの点がすべてImageRectanglesに格納されているオブジェクトでそのフィールド変数CanMoveがtrueのもののなかに入っている場合は移動可能と判断します。また橋や狭いところをとおるとき難しくなるので2ピクセルの余裕を持たせています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Map { protected virtual bool CanMove(int newLeft, int newTop) { int newRight = newLeft + Block.Width; int newBottom = newTop + Block.Height; List<Rectangle> rects = ImageRectangles.Where(x => x.CanMove).Select(rect => rect.Rectangle).ToList(); if (!rects.Any(rect => rect.Left - 2 <= newLeft && newLeft <= rect.Right + 2 && rect.Top - 2 <= newTop && newTop <= rect.Bottom + 2)) return false; if (!rects.Any(rect => rect.Left - 2 <= newRight && newRight <= rect.Right + 2 && rect.Top - 2 <= newTop && newTop <= rect.Bottom + 2)) return false; if (!rects.Any(rect => rect.Left - 2 <= newLeft && newLeft <= rect.Right + 2 && rect.Top - 2 <= newBottom && newBottom <= rect.Bottom + 2)) return false; if (!rects.Any(rect => rect.Left - 2 <= newRight && newRight <= rect.Right + 2 && rect.Top - 2 <= newBottom && newBottom <= rect.Bottom + 2)) return false; return true; } } |
MovePlayerメソッドはプレイヤーを移動させます。
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 |
public class Map { public virtual void MovePlayer(Direct direct) { if (direct == Direct.Left) { if (CanMove(PlayerPoint.X - 4, PlayerPoint.Y)) PlayerPoint.X += -4; } if (direct == Direct.Up) { if (CanMove(PlayerPoint.X, PlayerPoint.Y - 4)) PlayerPoint.Y += -4; } if (direct == Direct.Right) { if (CanMove(PlayerPoint.X + 4, PlayerPoint.Y)) PlayerPoint.X += 4; } if (direct == Direct.Down) { if (CanMove(PlayerPoint.X, PlayerPoint.Y + 4)) PlayerPoint.Y += 4; } } public virtual void Draw(Graphics graphics) { } } |
Map1クラスを作成する
最初にこのようなマップをつくる文字列を用意しておきます。
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 class Map1 : Map { string MapText = "" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~MMMMMM~~~~~~~~~~\n" + "~~~~~~MMMMMM・~~~~~~~~MMM・・・・・MMMM~~~~~~~\n" + "~~~~~MMMM・・・・~~~~~~~~・・・・ MMMM~~~~~~~\n" + "~~~~MMMMM・ 橋橋橋橋橋橋橋橋 MMMMM~~~~~~\n" + "~~~MMMMM・・ ~~~~~~~~MMMM・ MMMMMM~~~~~\n" + "~~~MMMM・・ ~~~~~~~~MMMM・ ・MMMMMM~~~~\n" + "~~~MMMM・ ~~~~~~~~MMMMM・ MMMMMMM~~~\n" + "~~~MM・・・ MMMMMMMMMMMMM・ MMMMMMM~~~\n" + "~~~MM・ ・MMMMMMMMMMMM・ MMMMMMM~~~\n" + "~~~MM・ MMMMMMMMMM・・・ MMMMMMM~~~\n" + "~~~MM・ MMMMMMMMMMMMM・・・・ MMMMMMMM~~~\n" + "~~~MM・ MMMMMMMMMMMM・ MMMMMMMM~~~\n" + "~~~MM・ MMMMMMM・・・・M・ MMMMMMMM~~~~\n" + "~~~MM・ MMMMMMM・魔○○M・ MMMMMMMMMMMM~~~~\n" + "~~~MM・ ・M・・・・M・○○○M・ ・・MMMMMMMMM~~~~~\n" + "~~~MM・ M・城○○M・○○○M・ MMMMMMMMM~~~~~\n" + "~~~MM・ M・○○○MM・ M・ MMMMMMMM~~~~~~\n" + "~~~MM・ M・○○○MM・ MMM・ MMMMMMMM~~~~~~~\n" + "~~~MM・ M・ MM・ ・・・・ MMM山MMM~~~~~~~~\n" + "~~~MM・ M・ MMM・ MMMMM~~~~~~~~~~\n" + "~~~MM・ ・・ MMM・ MMMM~~~~~~~~~~~\n" + "~~~MMM・ MMMMMMMMMMMMMM~~~~~~~~~~~~~\n" + "~~~MMM・ MMMMMMMMMMMM~~~~~~~~~~~~~~~\n" + "~~~MMMMMMMMMMMMMMMMMMMMM~~~~~~~~~~~~~~~~\n" + "~~~~MMMMMMMMMMMMMMMMMMM~~~~~~~~~~~~~~~~~\n" + "~~~~~MMMMMMMMMMMMMMMMM~~~~~~~~~~~~~~~~~~\n" + "~~~~~~・・・・・・・・・・・・・・・・~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"; } |
コンストラクタのなかでInitメソッドを実行します。Initメソッドが実行されるとそのなかで文字をもとにImageRectangleオブジェクトが生成されます。これをリストのなかに格納します。
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 Map1 : Map { protected Form1 _form1 = null; public Map1(Form1 form1) { _form1 = form1; Init(); } 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] == 'm') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.SmallMountain)); else if (vs1[x] == 'M') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.BigMountain)); else if (vs1[x] == '・' || vs1[x] == '○') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.None)); else if (vs1[x] == ' ') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.FlatGround)); else if (vs1[x] == '橋') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.BridgeEW)); else if (vs1[x] == '城') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.Castle)); else if (vs1[x] == '魔') ImageRectangles.Add(new ImageRectangle(x, y, BlockType.EnemyCastle)); if (vs1[x] == ' ' || vs1[x] == '橋' || vs1[x] == '城') ImageRectangles.Last().CanMove = true; } } PlayerPoint.X = 12 * Block.Width; PlayerPoint.Y = 19 * Block.Height; } } |
これはBGMを鳴らすためのメソッドです。
1 2 3 4 5 6 7 8 |
public class Map1 : Map { WMPLib.WindowsMediaPlayer player = new WMPLib.WindowsMediaPlayer(); public void BgmStart() { player.URL = Application.StartupPath + "\\bgm-field.mp3"; } } |
マップを描画する
マップを描画する処理を示します。
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 Map1 : Map { public override void Draw(Graphics graphics) { _form1.BackColor = Color.Blue; foreach (ImageRectangle rect in ImageRectangles) rect.DrawFlatGroundWithShift(graphics, -PlayerPoint.X + _form1.ClientSize.Width / 2, -PlayerPoint.Y + _form1.ClientSize.Height / 2); List<ImageRectangle> seas = ImageRectangles.Where(x => x.BlockType == BlockType.Sea).ToList(); foreach (ImageRectangle rect in seas) rect.DrawWithShift(graphics, -PlayerPoint.X + _form1.ClientSize.Width / 2, -PlayerPoint.Y + _form1.ClientSize.Height / 2); List<ImageRectangle> others = ImageRectangles.Where(x => x.BlockType != BlockType.Sea).ToList(); foreach (ImageRectangle rect in others) 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); } } |
実際に動かしてみるとこんな感じです。城から城へ移動するだけで敵もでてきません。
初めまして
描画のイベントをどこで起こしているのかわかりません。最初のbitmapを描画した後は、どのように再描画を行っているのか教えていただきたいです。
描画はForm1.OnPaintでおこなっています。そしてOnPaintはInvalidate()を呼び出したときに呼び出されます。
Form1.OnKeyDownでInvalidate()を呼び出しているのでキーを押したときに再描画がおこなわれています。
分かりやすい説明ありがとうございます!