簡単なシューティングゲームをつくります。ただし本当に鳩でも分かる内容にしたいので完成度はそんなに高くありません。この動画のようなものをつくります。
ソースコードはGitHubで公開しています。⇒ https://github.com/mi3w2a1/SimpleShooter
自機は左右にしか移動できず、ランダムに上方から下りてくる敵を撃退するだけの単純なゲームです。
まず自機と敵機を画像として描画します。そのためには画像を描画するテクニックを習得する必要があります。
画像をリソースに追加する
まずメニューのプロジェクト ⇒ WinFormsAppXのプロパティを選択します。そして[リソース]と書かれている部分をクリックします。すると「規定のリソースファイルが追加されていません」と表示されます。
このメッセージを読み進めてみると「ファイルを作成するにはここをクリック」と書かれているので、これをクリックします。
すると上のような表示になります。ここへ追加したい画像をドラッグアンドドロップしましょう。追加したい画像はこれを使います(ファイル名 sprite2.png)。
あとはこのなかから使いたい画像だけを切り取って使うことになります。
まずさきほどドラッグアンドドロップした画像のBitmapを取得するには
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { // フィールド変数 Bitmap CharactersBitmap = null; public Form1() { InitializeComponent(); CharactersBitmap = Properties.Resources.sprite2; } } |
これでOKです。ここから必要な部分を取得するのですが、必要なのは上の段にある青い戦闘機、敵キャラとして3種類のひよこ(やさぐれひよこ)、弾丸として戦闘機の下にある丸いものを使えばよいと思います。
まず画像のなかから切り取りたい部分を決めます。
戦闘機であれば左上の座標は(57,1)、サイズは幅44、高さ48です。
黄色いひよこであれば左上の座標は(4,61)、サイズは幅24、高さ28です。
ピンク色のひよこであれば左上の座標は(4,94)、上と同じです。
一番悪そうな青いひよこであれば左上の座標は(4,125)、上と同じです。
では元になる画像(sprite2.png)から必要な部分だけ取り出すにはどうすればいいのでしょうか?
まずsourceRectangeを作成します。コンストラクタの引数は切り取りたい部分の左上の座標とサイズです。次に切り取ったものをサイズをどうするか、幅と高さを指定します。変更しない場合も指定は必要です。ここではdestWidthとdestHeightという変数でこれを指定します。
取得したいBitmapのサイズは幅destWidth、高さdestHeightになるのでその大きさでBitmapを生成、Graphicsオブジェクトを生成してDrawImageメソッドで元のBitmapから出力先になるBitmapに書き込みます。最後にGraphicsオブジェクトを破棄します。
自機のBitmapを取得するのであれば以下のようになります。なんども取得すると時間がかかるのとメモリーが圧迫されるので、以下のメソッドは最初に1回だけ実行して、取得されたBitmapはフィールド変数に保存しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { Bitmap JikiBitmap = null; Bitmap GetJikiBitmap() { //戦闘機であれば左上の座標は(57,1)、幅44、高さ48 Rectangle sourceRectange = new Rectangle(new Point(57, 1), new Size(44, 48)); int destWidth = 44; int destHeight = 46; Bitmap bitmap1 = new Bitmap(destWidth, destHeight); Graphics graphics = Graphics.FromImage(bitmap1); graphics.DrawImage(CharactersBitmap, new Rectangle(0, 0, destWidth, destHeight), sourceRectange, GraphicsUnit.Pixel); graphics.Dispose(); return bitmap1; } } |
同じようにすれば敵キャラの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 |
public partial class Form1 : Form { Bitmap EnemyBitmap1 = null; Bitmap EnemyBitmap2 = null; Bitmap EnemyBitmap3 = null; Bitmap GetEnemyBitmap1() { // 黄色いひよこ。左上の座標は(4,61)、幅24、高さ28です。 Rectangle sourceRectange = new Rectangle(new Point(4, 61), new Size(24, 28)); int destWidth = 24; int destHeight = 28; Bitmap bitmap1 = new Bitmap(destWidth, destHeight); Graphics graphics = Graphics.FromImage(bitmap1); graphics.DrawImage(CharactersBitmap, new Rectangle(0, 0, destWidth, destHeight), sourceRectange, GraphicsUnit.Pixel); graphics.Dispose(); return bitmap1; } Bitmap GetEnemyBitmap2() { // ピンク色のひよこ。左上の座標は(4,94)、幅24、高さ28 Rectangle sourceRectange = new Rectangle(new Point(4, 94), new Size(24, 28)); int destWidth = 24; int destHeight = 28; Bitmap bitmap1 = new Bitmap(destWidth, destHeight); Graphics graphics = Graphics.FromImage(bitmap1); graphics.DrawImage(CharactersBitmap, new Rectangle(0, 0, destWidth, destHeight), sourceRectange, GraphicsUnit.Pixel); graphics.Dispose(); return bitmap1; } Bitmap GetEnemyBitmap3() { // 青色のひよこ。左上の座標は(4,125)、幅24、高さ28です。 Rectangle sourceRectange = new Rectangle(new Point(4, 125), new Size(24, 28)); int destWidth = 24; int destHeight = 28; Bitmap bitmap1 = new Bitmap(destWidth, destHeight); Graphics graphics = Graphics.FromImage(bitmap1); graphics.DrawImage(CharactersBitmap, new Rectangle(0, 0, destWidth, destHeight), sourceRectange, GraphicsUnit.Pixel); graphics.Dispose(); return bitmap1; } } |
敵をつくる
敵を作ったら動かさないといけません。そこで生成した敵を格納するリストを作ります。作成した敵はとりあえずこのなかにいれます。
それから敵をつくるペースは1秒に1回です。今回は本当に鳩でもわかる内容にしたいのであまり凝ったことはしません。敵は一番上に現れて、左右に移動しながら下りてくるだけです。
まずタイマーをつくります。コンストラクタ内でTickイベントを1秒間に60回発生させる設定にしてタイマーをスタートさせます。Tickイベントが発生するとイベントハンドラ Timer_Tickが実行されます。
あと、ちらつきをおさえるためにDoubleBuffered = trueにしています。それからゲームらしくするために背景色を黒にしています。
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 |
public partial class Form1 : Form { Timer timer = new Timer(); public Form1() { InitializeComponent(); DoubleBuffered = true; BackColor = Color.Black; // 自機、敵機を描画するために必要なBitmapを取得する GetBitmaps(); // タイマーを初期化 InitTimer(); } // 自機、敵機を描画するために必要なBitmapを取得する void GetBitmaps() { CharactersBitmap = Properties.Resources.sprite2; JikiBitmap = GetJikiBitmap(); EnemyBitmap1 = GetEnemyBitmap1(); EnemyBitmap2 = GetEnemyBitmap2(); EnemyBitmap3 = GetEnemyBitmap3(); } // タイマーを初期化 void InitTimer() { timer.Interval = 1000 / 60; timer.Tick += Timer_Tick; timer.Start(); } } |
Timer_Tickが実行されたらフィールド変数 TickCountをつかってTimer_Tickが何回実行されたかを数えます。そして60回に1回、つまり1秒ごとに1回、新しい敵をつくります。敵はEnemyクラス(後述)で管理します。Enemyクラス内にあるMoveメソッドで敵を動かします。そしてInvalidateメソッドを呼び出せば移動した敵を描画することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { int TickCount = 0; List<Enemy> Enemies = new List<Enemy>(); private void Timer_Tick(object sender, EventArgs e) { TickCount++; if (TickCount % 60 == 0) Enemies.Add(new Enemy(EnemyBitmap2, 0, 0)); foreach (Enemy enemy in Enemies) enemy.Move(); Invalidate(); } } |
敵を左右に移動させる場合、フォームの幅がわからないとどこで方向転換させればいいのかわかりません。そこでこれから作成するEnemyからでもフォームのサイズがわかるように静的フィールド変数 FormClientSizeを定義します。これでForm1.FormClientSizeとすればフォームのサイズがわかります。Form1のインスタンスはひとつしか作成しないので、これで問題はないはずです。
OnLoadはフォームが表示されたときに、OnResizeはサイズが変更されたときに実行されます。このときにフォームのサイズをFormClientSizeに格納することで、それ以外のクラスからでも Form1.FormClientSizeとすればフォームのサイズを取得することができるのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { public static Size FormClientSize = Size.Empty; protected override void OnLoad(EventArgs e) { FormClientSize = this.ClientSize; base.OnLoad(e); } protected override void OnResize(EventArgs e) { FormClientSize = this.ClientSize; base.OnResize(e); } } |
Enemyクラスを作成する
Enemyクラスを作成します。敵をつくるためには上記で作成したBitmapのどれを使うのか、最初に出現する座標はどこにするのかが必要です。そこでこれらはコンストラクタで渡すことにしました。Bitmapプロパティは敵のBitmap、XプロパティとYプロパティは敵が現在いる座標です。MoveRightプロパティは敵は右に動くかどうかです。
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 Enemy { public Enemy(Bitmap bitmap, int startX, int startY, bool moveRight) { Bitmap = bitmap; X = startX; Y = startY; MoveRight = moveRight; } public Bitmap Bitmap { get; set; } public int X { get; set; } public int Y { get; set; } public bool MoveRight { get; set; } } |
敵を動かすためにMoveメソッドを作成しました。敵はフォームの端までくると方向を転換します。このときフォームの幅がどうなっているかがわからないと処理ができません。
それから自機に攻撃された場合は消滅することになります。この場合、リストから取り除かないといけないので死亡フラグも必要です。それから自機に撃たれることなくフォームの下まで到達した場合も上から下にしか移動できない以上、描画されることはありません。これもリストから取り除かないといけないので死亡フラグをセットすることになります。
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 Enemy { public void Move() { if (Form1.FormClientSize.Width < X) MoveRight = false; if (0 > X) MoveRight = true; Y++; if(MoveRight) X += 3; else X -= 3; if (Form1.FormClientSize.Height < Y) IsDead = true; } public bool IsDead { get; set; } = false; } |
これは敵を描画するためのメソッドです。
1 2 3 4 5 6 7 8 |
public class Enemy { public void Draw(Graphics graphics) { if(!IsDead) graphics.DrawImage(Bitmap, new Point(X, Y)); } } |
自機を描画するためのクラスをつくる
自機を描画するためのJikiクラスを作成します。Bitmapは最初に作成したものを使います。自機が最初に出現する場所は中央のやや下寄りの場所です。最初は常に同じ場所に出現するため、Enemyクラスのようにコンストラクタに初期座標の引数をわたしていません。
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 Jiki { public Jiki(Bitmap bitmap) { Bitmap = bitmap; X = (Form1.FormClientSize.Width - Bitmap.Width) / 2; Y = (int)(Form1.FormClientSize.Height * 0.8); IsDead = false; } public Bitmap Bitmap { get; set; } public int X { get; set; } public int Y { get; set; } public bool IsDead { get; set; } } |
Enemyクラスと同様に移動させるためのメソッドを作成します。前後左右のキーが押されていたら移動させます。画面の外に出てしまわないように移動する前に本当に移動できるのか確認しています。
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 |
public class Jiki { public bool MoveLeft = false; public bool MoveRight = false; public bool MoveUp = false; public bool MoveDown = false; public void Move() { if (MoveRight && X + Bitmap.Width -3 < Form1.FormClientSize.Width) X += 3; if (MoveLeft && X > 3) X -= 3; if (MoveUp && Y > 3) Y -= 3; if (MoveDown && Y + Bitmap.Height - 3 < Form1.FormClientSize.Height) Y += 3; } public void Draw(Graphics graphics) { // 自機が死亡していないのであれば描画する if (!IsDead) graphics.DrawImage(Bitmap, new Point(X, Y)); } } |
作成したJikiクラスをForm1クラスに組み込むと以下のようになります。
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 |
public partial class Form1 : Form { Jiki Jiki = null; public Form1() { InitializeComponent(); DoubleBuffered = true; BackColor = Color.Black; GetBitmaps(); InitTimer(); Jiki = new Jiki(JikiBitmap); } private void Timer_Tick(object sender, EventArgs e) { TickCount++; Jiki.Move(); if (TickCount % 60 == 0) Enemies.Add(new Enemy(EnemyBitmap2, 0, 0)); foreach (Enemy enemy in Enemies) enemy.Move(); Invalidate(); } protected override void OnPaint(PaintEventArgs e) { Jiki.Draw(e.Graphics); foreach (Enemy enemy in Enemies) enemy.Draw(e.Graphics); base.OnPaint(e); } } |
これで自機と敵機が描画されるようになりました。しかしキーをおしても移動しません。また敵機を攻撃することもできません。キー操作にかんしてはまだ実装していないので当然といえば当然です。これは次回おこないます。
貴重なソースの公開ありがとうございます。
一応リストの通り入力したつもりですが①②でコンパイルエラーが出ます。入力ミスの可能性が大ですが、だとするとどの辺が怪しいでしょうか?
> Jiki = new Jiki(JikiBitmap);
Jk = new Jiki(JikiBitmap);
でコンパイルは通りました。でも真っ黒(笑)。
JikiクラスをForm1クラスの内部で定義していませんか?これだと「Form1 はJikiの定義を含んでいる」とか「Form1.JikiとForm1.Jiki間があいまい」というエラーが出ます。
これだと問題ないはず。
これだと「Form1 はJikiの定義を含んでいる」というエラーがでます。
次の質問では
とクラス名とインスタンス名を変えることでエラーを解決しています。これでもよいと思います。
次の質問で
>コンパイルは通りました。でも真っ黒(笑)。
予想される原因として
① 自機、敵機を描画するために必要なBitmapを取得するGetBitmaps()メソッドはうまく機能しているでしょうか?
タイマーをつかって自機と敵機、弾丸を移動させて再描画させているのですが、
② タイマーがStartしているか?
③ Timer_Tickの最後にInvalidate();を呼び出しているか?
④ OnPaintをオーバーライドしているか?
このあたりをチェックしてみてはいかがでしょうか?
丁寧な回答まことにありがとうございました。
> JikiクラスをForm1クラスの内部で定義していませんか?
してました。念のために確認したいのですが、EnemyクラスもForm1クラスの内部で定義してこちらはコンパイルは通りますが、EnemyクラスもForm1クラスの外に出すべきなのでしょうか?
無事動きました。
EnemyクラスはForm1クラスの内部でも外部でも正常に動きました。
Form1クラスの外部でクラスを定義するとき、Form1クラスの前にコーディングするとデザイナーに異変が起こることも確認しました。いろいろわからないことだらけです。
また、わからないことがあったらよろしくお願いいたします。
>Form1クラスの外に出すべきなのでしょうか?
Form1クラス以外でもインスタンスを生成する場合はForm1クラスの外で定義したほうがよいと思います。
絶対にForm1クラス以外では使わない場合は内部で定義したほうがよいと考えています。
クラスの内部で別のクラスを定義した場合、クラス名とインスタンス名が同じだと「Form1 はJikiの定義を含んでいる」のようなエラーがでてしまいます。
ちょっと思ったのですが、クラス名とインスタンス名が同じというのは避けたほうがいいのでしょうか?
Jiki Jiki = new Jiki();とかTimer Timer = new Timer();という書き方は読み手を混乱させるかもしれません。
Jiki Jiki1 = new Jiki();とかTimer Timer1 = new Timer();のようにクラス名とインスタンス名は変えたほうがいいかもしれませんね。
これならJikiと書いたとき、これってクラス名?インスタンス名?と惑わせることもないと思うので・・・
bitmapの画像の座標はどうやって求めることができるのでしょうか、、
また、敵の画像を下のロボットと鶏に変えたい場合座標の値はどうなりますか?
赤と青で囲まれた画像の座標を取得に関する質問でしょうか?
これはある方が誰でも再利用可ということで公開している画像です。各部分の座標は画像編集ソフトをつかって調べました。ロボットなら(4,160)、鶏なら(4,193)あたりでしょうか?
初めまして。C#初心者で現在勉強中がてら同じような簡易的な障害物を避けるようなゲームを作成中ですが、pictureboxにresourceから埋め込んだ画像を設定し背景を透過して自機や敵を作成したところ、画像の透過が親コントロール(フォーム)を元にした透過しかできず、他のpictureboxやコントロールと重なった場合に背景箇所がフォームの色に塗りつぶされた状態で表示され困っており、こちらのサイトにたどり着きました。
お見受けしたところ、こちらでは画像同士が背景透過された状態で重なっていたので
コレ通りに作成すればよいと思いBitmap画像を描画する処理を真似して記載してみましたが、どうもうまくいかず(理解も出来ておらず)旨い事描画されません。
こちらお手数おかけしますが、背景透過済の単体の画像をBitmapで透過素材として描画するまでのコードを教えていただけないでしょうか。
画像を切り抜いて~や他クラスを作成して~などは省き、フォーム上でただ表示させるだけでかまいません。お手数おかけしますが気が向いたらご返信いただければ幸いです。
どうも、素晴らしい記事をありがとうございます。
bitmapのDrawImageについてですが、描画する内容をbitmapのサイズに合わせる方法を教えてください!picturebox等に画像を設定して、サイズモードを「Zoom」に設定しているように、コントロール(キャンバス)に合わせて画像を拡大、縮小して画像のすべてをコントロール(キャンバス)内に収めて描画する方法を知りたいです。
よろしくお願いいたします。