Visual Basic ゲーム講座 |
この記事が対象とする製品・バージョン (バージョンの確認方法)
Visual Basic 2010 | ◎ | 対象です。 | |
Visual Basic 2008 | ○ | 対象ですが一部画面が異なる場合があります。 | |
Visual Basic 2005 | ○ | 対象ですが一部画面が異なる場合があります。 | |
Visual Basic.NET 2003 | × | 対象外です。 | |
Visual Basic.NET (2002) | × | 対象外です。 | |
Visual Basic 6.0 | × | 対象外です。 |
概要 ・複数のキャラクターを効率的に管理する方法 |
今回は効率的な方法でキャラクターを管理する方法を説明します。
地味な部分ですが非常に重要な仕組みでゲーム作りの最初の山と言えます。この部分を理解しているかしていないかで大きな差がでます。
前回までで簡単ながらも一応複数のキャラクターを動かすことができるようになりました。しかし、その結果に問題があることを理解していただけたかと思います。
その問題とは、ずばりキャラクターの種類や数が増えれば増えるほどプログラムが巨大化し、複雑になるということです。
この状況でプログラムを続けると、もっとゲームを面白くする部分のプログラムに取り組みたいと思っても、単純な部分のプログラムに時間の多くをとられてしまうことになりますし、複雑化すればするほどゲームの本質的なプログラムを作るのが難しくなってしまいます。
「普通のプログラムは作れるけどゲームをどう作っていいかわからない。」、「ゲームらしいゲームが作れない」という人は大勢いますが、そのほとんどはこの問題に対する解を持っていないのだと思います。
今回はこのような状況に対する解として、ゼロから新しいアプリケーションを作成します。この新しいアプリケーションでは敵の種類が増えようが、数が増えようがプログラムはシンプルです。なので、プログラマーは新しい敵の動きや、面白いゲームの仕掛けを比較的シンプルに追加していくことができるようになるでしょう。
この連載の今後のプログラムでも今回作る仕組みがベースになります。
今回は2種類の敵を出現させるところまでを実現させます。次回、主人公キャラクターの移動の仕組み、その次はいよいよあたり判定の説明ができればなと考えています。
さて、その新しい効率的なプログラムの仕組みの根幹は、すべてのキャラクターを表現する共通の基底クラスを作ることにあります。
この基底クラスをUnit(ユニット)という名前にします。 このUnitクラスの考え方はほとんどのジャンルのゲームを作るうえで、非常に非常に非常に重要です。
すべてのキャラクターはこのUnitクラスから派生させたクラスによって表現されます。
今回と次回つくるキャラクターは次の3つです。
キャラクター | クラス | |
---|---|---|
主人公 | PlayerUnit | 次回作ります。主人公です。 |
ザコ(敵) | ZakoUnit | 直進するだけの敵です。 |
ナナメ(敵) | NanameUnit | ななめに移動する敵です。 |
■表1
キャラクターには位置の更新と外見の描画という2つの重要な機能があり、この2つはどんなキャラクターでも必ず持っているものです。
このようにすべてのキャラクターが共通で持っている機能は基底クラスであるUnitクラスにプログラムします。
ただ、位置の更新自体はどのキャラクターでも行うことかもしれませんが、どのように位置を更新するかはキャラクターによって異なります。また外見の描画も当然キャラクターごとに異なります。
このようにキャラクターごとに異なる機能は、PlayerUnit、ZakoUnit、NanameUnitなどの各派生クラスにプログラムします。
そうすると、Form側ではどのようなキャラクターであれ、「位置を更新せよ」、「外見を描画せよ」と命令するだけで済みます。位置を更新する機能をMoveメソッド、外見を描画する機能をDrawメソッドとすると次のようなイメージです。
'これはイメージです。実際に動作するプログラムではありません。 Private Units As List(Of Unit) Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick For Each unit As Unit In Units unit.Move unit.Draw Next End Sub |
■リスト1
これで、Moveが呼ばれたときに実際にどう動くか、Drawが呼ばれたときに何を描画するかはキャラクターによって異なりますので、MoveメソッドやDrawメソッドの中身はPlayerUnitクラス、ZakoUnitクラス、NanameUnitクラスのそれぞれで変えます。
また、この仕組みではすべてのキャラクターはコレクションに登録します。上記のイメージではUnitsがそのコレクションです。Unitsに登録されているものはTimerが作動するたびに、(つまり、1フレームごとに)MoveとDrawが呼び出されます。
この作りにより、キャラクターが新しく出現するということはUnitクラス(の派生クラス)をUnitsに登録するということであり、キャラクターが消滅するということはUnitsの中から取り除かれるという意味になります。
ちょっとわかりにくいかもしれないので図にしてみます。
■画像1:Unitクラスの考え方
まず、Timerが短い時間に何度も何度も処理を繰り返します。この1回の処理のことをフレームというのでした。
1フレームの処理では、必ず前UnitのMoveとDrawを呼び出して、位置を更新し外見を描画します。
これだと、UnitクラスのMoveとDrawの中身がキャラクターの種類が多いほどごちゃごちゃになってしまいそうですが、実はそうではなく、オブジェクト指向の継承の考え方をつかって、それぞれのキャラクターごとに異なるUnitクラスの用意しておきます。
こうすることで、それぞれのキャラクターごとのUnitクラスを作って、位置の更新と外見の描画プログラムするだけで、あとはフレームごとの処理で全Unitの状態が自動的に更新されるようになるのです。
以上を踏まえて、実際に2種類の敵キャラクターをプログラムして動くようにしてみましょう。
今回は敵キャラクター2種類を作るところまでやります。これにベースとなるUnitクラスも含めて3つのUnitクラスを作ります。また、これらのUnitクラスを制御するフォームも前回同様にプログラムします。
Form1 | ゲームの画面。Timerを配置してフレームごとにユニットを処理する。 |
Unitクラス | すべての基本となるユニット。 |
ZakoUnitクラス | 敵キャラクター用のユニット。Unitクラスを継承。 |
NanameUnitクラス | 敵キャラクター用のユニット。Unitクラスを継承。 |
■表2:今回作るもの
MoveやDrawなどすべてのユニットで共通のメソッドはUnitクラスで定義します。メソッドの中身はキャラクターによってことなるので、それぞれの継承したクラスの中に記述します。
まずはプロジェクトを作成し、基本的な設定を先にやってしまいましょう。
Visual Basicを起動して新しいWindows フォーム アプリケーションを作成してください。名前はVBGameD03とします。
次にプロジェクトを右クリックして[追加] - [新しいフォルダー]の追加で、Imagesという名前のフォルダーとUnitsという名前のフォルダーを作成してください。
Imagesフォルダーにはこのプログラムで使用する画像ファイルを入れていきます。Unitsフォルダーにはいろいろな種類の敵キャラクターや主人公のプログラムを入れていきます。
まずはImagesフォルダーに前回までに作成した主人公と敵キャラクターの画像を入れましょう。
Imagesフォルダーを右クリックして、[追加] - [既存の項目]で前回までに作成した主人公の画像Player1.bmpとZako1.bmpを追加してください。今回から読んでいる人はこの名前で主人公の画像を敵キャラクターの画像をペイントでなどで作成して下さい。
一応私が用意した画像も載せておきますので、自分で用意するのが面倒な人はリンクを右クリックしてダウンロードしてください。(bmp形式の画像をダウンロードできるようにしておきました。)
Player1.bmp | Zako1.bmp |
この時点でソリューションエクスプローラーは次のようになります。
■画像2
画像をソリューションに追加したらそれぞれの画像ファイルのプロパティで「出力ディレクトリーにコピー」の項目を「新しい場合はコピー」にしておいてください。
では、いよいよ中身にとりかかります。
まずUnitsフォルダーにすべてのキャラクターの基になるUnitクラスを追加します。そのために、Unitsフォルダーを右クリックして、[追加] - [新しい項目]でクラスを選択し、名前はUnitと入力して追加をクリックしてください。
この時点でソリューションエクスプローラーは次のようになります。
■画像3
Unitクラスの内容はさきほどアプリケーションの仕組みのところで説明したことにをもとに考える以下のようになります。ゲームの設計によっては今後ここに機能を追加していくこともあり得ます。
これをプログラムレベルまで落とし込むと次のようになります。
プログラムする場所 | 名前 | 機能・プログラム内容 | |
---|---|---|---|
画像読み込み | コンストラクター | (New) | bmpファイルを読み込んで変数MainImageに保存する。透明色をセットする。 |
(クラスレベルの変数) | MainImage | 画像を保持するImage型の変数。 | |
(クラスレベルの変数) | imageSetting | 透明色の設定を保持するImaging.ImageAttributes型の変数。 | |
座標保持 | (クラスレベルの変数) | Location | Point型の変数。 |
座標更新 | メソッド | Move | Locationを変更する。 |
画像描画 | メソッド | Draw | Locationの位置にMainImageを描画する。 |
消滅判断 | プロパティ | IsAlive | ユニットが消滅すべきであればFalseを返す。 |
(技術的な理由) | プロパティ | Rectangle | ユニットの位置とサイズを保持する。 |
■表3:Unitクラスの設計
継承に慣れていない人は、ここで言っているUnitクラスは主人公(PlayerUnit)や敵キャラクター(ZakoUnit)の共通機能であるという点を常に意識するようにしてください。
画像を読み込んだり表示するところは主人公と敵キャラクターで同じプログラムになりそうなのでこのUnitクラスの中にプログラムしてしまうことにします。一方、座標の更新のさせ方や消滅の判断は主人公と敵キャラクターで異なりますのでここでは空のメソッドだけ作って中身はPlayerUnitクラスやZakoUnitクラスの中に書くようにします。
ここまで具体的に設計すれば、Unitクラスはできたも同然です。
まずは3つあるクラスレベルの変数をまとめて宣言します。
'このユニットの画像 Protected MainImage As Image 'MainImageの透明色の設定 Protected imageSetting As New Imaging.ImageAttributes '現在の座標 Public Location As Point |
スコープがProtectedになっているものとPublicになっているものがありますが、この違いは今後のためのもので現時点ではまったく意味はありません。全部PrivateでもOKです。
画像の読み込みは最初に一度だけ読み込んだ画像を使い回せばよいのでコンストラクターでやります。
Public Sub New(ByVal imageFileName
As String) '画像の読み込み MainImage = Image.FromFile(imageFileName) '紫を透明色に指定(この設定が使用されるのはDrawメソッド内) imageSetting.SetColorKey(Color.FromArgb(255, 0, 255), Color.FromArgb(255, 0, 255)) End Sub |
コンストラクターの引数に画像ファイルのフルパスを渡すようにします。中ではその画像を読み込んでMainImageに保存し、ついでに紫色を透明色に設定しておきます。透明色の設定は変数imageSettingにとっておきます。
透明色の指定は実際は描画担当のDrawメソッド内で行うのですが、上記のように単純に色を指定すればよいわけではなく、Drawメソッドが呼び出されるたびにこの処理を行うのは効率が悪いのであらかじめここで指定するようにしています。だから、意味的にはここに書くのではなくDrawメソッド内に書くほうが適切です。
Moveメソッドの内容はここでは空です。
''' <summary> ''' ユニットを移動させます。フレームごとに必ず呼び出してください。 ''' 移動の必要があるかないかも含めてすべてMoveメソッドの内部で処理します。 ''' つまり、Moveメソッドを呼び出してもまったく移動しない場合もあり得ます。 ''' </summary> Public Overridable Sub Move() ' End Sub |
■リスト2
キャラクターがどのように移動するかはキャラクターごとに異なるので、共通クラスであるUnitクラスの中には書きません。この中身は後で登場するZakoUnitクラスに書きます。ZakoUnitクラスなどの派生クラス側でこのメソッドの内容を上書きできるようにこのメソッドはOverridableで宣言する必要があります。
今回は採用しませんでしたOverridableではなくMustOverrideにするという考え方もありえます。
中身がないのならメソッド自体いらないのではないかと思う方がいるかもしれませんが、そうすると共通でMoveメソッドがあるという大前提が成り立たなくなります。詳しく知りたい方はオブジェクト指向を勉強してください。
Drawメソッドでは画像を指定された座標に書き込みます。この処理は多くのキャラクターで同じだと思うので、ここに中身まで書きます。
''' <summary> ''' ユニットを描画します。フレームごとに必ず呼び出してください。 ''' </summary> Public Overridable Sub Draw(ByVal g As Graphics) '現在の位置に画像を描画 g.DrawImage(MainImage, Rectangle, 0, 0, Rectangle.Width, Rectangle.Height, GraphicsUnit.Pixel, imageSetting) End Sub |
■リスト3
描画に必要なGraphicsクラスはこのメソッド内で作り出すことはせず、外部から受け取るようにします。こうしておくことで、呼び出し側は都合に応じてFormであれPictureBoxであれ、メモリ内であれ好きな場所にキャラクターを描画するように命令することができます。
ところで、ここでは画像(MainImage)と座標(Location)と透明色(imageSetting)の3つを指定するだけで十分なのですが、画像を描画するDrawImageメソッドではほかにも画像のサイズなどを指定しなければいけないので引数にはいろいろセットすることになってしまっています。
ここでユニットの位置とサイズを管理するRectangleプロパティがあったほうが呼び出しがスマートになるので、先に説明したように意味的には必要ないRectangleプロパティをプログラムするようにしています。
IsAliveプロパティはキャラクターが死んでいるなどの理由で消滅すべきであればFalseを返すようにします。
''' <summary> ''' ユニットの生死を判断します。Falseのときこのユニットは死んでいます。 ''' </summary> Public Overridable ReadOnly Property IsAlive() As Boolean Get 'この段階では常にTrueを返しておきます。(つまり、永遠に死なない。) Return True End Get End Property |
■リスト4
キャラクターの消滅条件はキャラクターごとに異なりますので、共通クラスであるUnitクラス側では常にTrueを返すようにして、必要に応じて派生クラス側で上書きするようにします。
注意したいのはIsAliveは消滅すべきかを判断するだけで、消滅させるものではありません。
キャラクターの生成と破棄はForm側で管理しているので、各ユニット内では生成や破棄までは行わないのです。
繰り返しになりますが念のために補足しておくと、ユニットが生成されるということはForm側のUnitsにユニットが追加されることを指します。ユニットが消滅するということはForm側のUnitsからユニットが取り除かれることを指します。
いくらIsAliveがFalseを返しても、Form側でIsAlive = FalseのユニットをUnitsから取り除く処理を実行しない限りいつまでもユニットは残ります。
ユニットの位置とサイズを返すプロパティです。位置はLocation、サイズはMainImageから取得できるので中身はこれらを合成するだけです。
''' <summary> ''' ユニットの現在の位置情報を返します。 ''' これはLocationが保持する値に幅と高さを加えた情報です。 ''' </summary> Public ReadOnly Property Rectangle As Rectangle Get Return New Rectangle(Location, MainImage.Size) End Get End Property |
■リスト5
以上でUnitクラスは完成です。全体は次のようになります。
Public
Class Unit 'このユニットの画像 Protected MainImage As Image 'MainImageの透明色の設定 Protected imageSetting As New Imaging.ImageAttributes '現在の座標 Public Location As Point |
Public Sub New(ByVal
imageFileName As String) '画像の読み込み MainImage = Image.FromFile(imageFileName) '紫を透明色に指定(この設定が使用されるのはDrawメソッド内) imageSetting.SetColorKey(Color.FromArgb(255, 0, 255), Color.FromArgb(255, 0, 255)) End Sub |
''' <summary> ''' ユニットを移動させます。フレームごとに必ず呼び出してください。 ''' 移動の必要があるかないかも含めてすべてMoveメソッドの内部で処理します。 ''' つまり、Moveメソッドを呼び出してもまったく移動しない場合もあり得ます。 ''' </summary> Public Overridable Sub Move() ' End Sub |
''' <summary> ''' ユニットの現在の位置情報を返します。 ''' これはLocationが保持する値に幅と高さを加えた情報です。 ''' </summary> Public ReadOnly Property Rectangle As Rectangle Get Return New Rectangle(Location, MainImage.Size) End Get End Property |
''' <summary> ''' ユニットを描画します。フレームごとに必ず呼び出してください。 ''' </summary> Public Overridable Sub Draw(ByVal g As Graphics) '現在の位置に画像を描画 g.DrawImage(MainImage, Rectangle, 0, 0, Rectangle.Width, Rectangle.Height, GraphicsUnit.Pixel, imageSetting) End Sub |
''' <summary> ''' ユニットの生死を判断します。Falseのときこのユニットは死んでいます。 ''' </summary> Public Overridable ReadOnly Property IsAlive() As Boolean Get 'この段階では常にTrueを返しておきます。(つまり、永遠に死なない。) Return True End Get End Property End Class |
■リスト5
次にいよいよ実際に敵キャラクターを表すZakoUnitクラスをプログラムします。
この敵は、前回プログラムしたのと同じで、ひたすら直進するだけのシンプルな敵にします。
ソリューションエクスプローラーでUnitsフォルダーを右クリックして、「ZakoUnit」という名前のクラスを追加してください。
初期状態では次のようなクラスが生成されます。
Public Class
ZakoUnit End Class |
■リスト6
これにUnitクラスから継承するようにInheritsを追加します。次の通りです。
Public Class
ZakoUnit Inherits Unit End Class |
■リスト7
この状態だと、コンストラクターの呼び出し方がわからないというエラーが表示されますがそれで正常です。
さて、画像の読み込みや表示は基底クラスであるUnitクラスの方でほとんどプログラムしているので、Unitクラスでは表現しきれていない部分だけをここにプログラムすることになります。
Unitクラスで表現しきれていない部分は、読み込む画像のファイル名と、だた直進するだけという動き方の2種類です。
読み込む画像のファイル名はコンストラクターで指定します。ただ直進するだけという動き方はMoveメソッドを上書き(オーバーライド)して表現しましょう。
いきなり完成版をお見せします。
Public Class
ZakoUnit Inherits Unit |
Public Sub New() MyBase.New(Application.StartupPath & "\Images\Zako1.bmp") End Sub |
''' <summary>ユニットを移動させます。</summary> Public Overrides Sub Move() '縦方向に2ドット移動 (=ちょっと下に移動) Me.Location.Y += 2 End Sub End Class |
■リスト8
必要な部分だけ書けばよく、シンプルなものですね。
Moveメソッドは基底クラスのMoveメソッドの内容を上書きするという意味のOverridesキーワードをつける必要があります。
では、フォーム側のプログラムも作ってとりあえず敵キャラクターが2体出現するようにしてみましょう。
フォームのサイズは640x480としDoubleBufferedプロパティをTrueにします。そしてTimerを1つ配置してIntervalを10にしてください。
フォームではキャラクターの画像や座標を管理する必要がないため、前回よりかなりシンプルなプログラムになります。
Public Class
Form1 Private mainGraphics As Graphics Private Units As New List(Of Unit) |
'64ビットOSでLoadイベント処理中の例外が捕捉されない問題があるのでShownを使います。 Private Sub Form1_Shown(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Shown Init() Timer1.Enabled = True End Sub |
Private Sub Init() '▼描画用のGraphicsクラスの確保(技術的な処理) If mainGraphics Is Nothing Then '初回のみ生成 Dim bmp As New Bitmap(Me.ClientRectangle.Width, Me.ClientRectangle.Height) Me.BackgroundImage = bmp mainGraphics = Graphics.FromImage(bmp) End If '▼敵キャラクター '1体目 Dim zako1 As New ZakoUnit zako1.Location = New Point(10, 0) '初期位置 Units.Add(zako1) '2体目 Dim zako2 As New ZakoUnit zako2.Location = New Point(200, -128) '初期位置 Units.Add(zako2) End Sub |
Private Sub Timer1_Tick(ByVal
sender As System.Object,
ByVal e As
System.EventArgs)
Handles Timer1.Tick 'いったん全体を黒で塗りつぶす mainGraphics.Clear(Color.Black) '登録されているすべてのUnitに対して、MoveとDrawを呼び出す。 For Each unit As Unit In Units If unit.IsAlive Then unit.Move() 'unitの座標更新 unit.Draw(mainGraphics) 'unitの描画(この時点では画面には反映されない) End If Next '最新の状態を画面に描画する。 Me.Invalidate() End Sub End Class |
■リスト9
注目すべきはTimer1_Tickの中です。ここが1フレームごとの処理を表している部分ですが、この中ではユニットに動け(Move)、描画せよ(Draw)を命じるだけで、具体的にどう動け、どう描画しろということは書いていません。
敵キャラクターの生成はInitメソッド内で行っています。敵キャラクターをどういう順番でどういう位置に修験させるかはゲームの作りの中でも大切な部分の1つですので、この部分の仕組みは回を改めてちゃんと説明したいと思います。今回はべたに2体の敵を追加しているだけです。敵を消滅させるプログラムも組み込んでいないので、この2体は画面から表示されなくなってもプログラム上は存在しつづけ、やがて座標がコンピューターが計算できる範囲を超えた時にオーバーフローエラーになります。
とはいえ、これで実行するとちゃんと2体の敵がでてきてくれます。
■画像4
3体にしたい場合、どうすればいいかはわかりますよね?しかも結構簡単に3体にできるようになっていると思いませんか?
もはや、キャラクターの数ごとに座標や画像を管理する必要はなくなったのです。すべてはUnitクラスが自分自身をうまく管理してくれます。
さて、最後にもう1種類敵を追加しましょう。やや速い速度で斜めに移動する敵です。この敵のためにNanameUnitクラスを追加してください。
画像を用意するのが面倒なので、画像はZako1.bmpをそのまま使うことにします。余裕がある人は画像も自分で作ってみてください。
さて、このNanameUnitクラスのプログラムを自分で書くことはできますか?今回説明してきた内容をよく理解していただければ自分でプログラムすることができるはずです。
斜めに移動すると横から画面をはみ出してしまうので、できれば画面の端っこまで来たら今度は反対側に飛ぶようにしたいのですが、今回はそこまで欲張るのはやめましょう。
すぐ下に正解を書きますが、できれば、自分で書いてみて、しかもこのななめの敵が表示されるようにForm側のプログラムも変えて実行まで挑戦してみてください。
では、NanameUnitクラスのプログラムです。
Public Class
NanameUnit Inherits Unit |
Public Sub New() MyBase.New(Application.StartupPath & "\Images\Zako1.bmp") End Sub |
Public Overrides
Sub Move() '斜めに移動させるための数字の組み合わせはご自由に Me.Location.X += 3 Me.Location.Y += 2 End Sub End Class |
■リスト10
フォーム側はInitメソッドに以下のプログラムを追加します。
'3体目 Dim naname1 As New NanameUnit naname1.Location = New Point(0, -100) Units.Add(naname1) |
■リスト11
今回作成したプログラムをダウンロードできるようにしておきます。
VBGameD03.zip 15KB (VB2010用)
ところで、この調子でいくとまだまだこの連載は長く続きそうなので、手っ取り早くゲームの完成品が見たい人は、こちらをダウンロードしてください。VBのソースコードがダウンロードできるので参考になります。
作りはこの連載で紹介しているものと若干違いがありますがほぼ同じです。この連載は要するにこのゲームをどうやって作っているかをゼロから説明しているだけとお考えてください。