第5回「ブロック崩し・前編」

目次

1.事前準備

プログラミング教室も終盤となりましたので,最後に2回分の時間をつかって「ブロック崩し」に挑戦してみたいと思います.最終的には以下のように仕上がります(注:現段階での完成品であるため,若干の変更が加わるかもしれません).

ブロック崩し

ただ,ブロック崩しのプログラムは今まで作ってきたものと比べて難度が格段にアップします.あくまでも個人的な見解ですが,何も見ずに自力で考えてこの手のプログラムが書けるようになれば,アマチュアの世界で「プログラマー」であることを名乗ることができるレベルです.

このように内容的にも濃く作業量の多い題材なので,1回分の時間だけでは完成させることができません.そこで,今回は「ブロックの無いブロック崩し(笑)」を完成させることを目指します.実際に上のようになるのは次回(1ヶ月後)です.一見殺風景でつまらなさそうに見えますが,実はこれでも意外に遊べるものなのです.

ブロックの無いブロック崩し

なお,今回の作業量は前回までのものに比べてかなり多めになっており,難易度も格段にアップしていますので,「解説」等の項目は後ほどご自宅で読まれることをおすすめします.作業の進捗を優先して下さい.

それでは,以下の手順で事前準備を行って下さい.

  1. プロジェクト名をBlockKuzushiとして,新しいプロジェクトを作成して下さい.プロジェクトの作成は,以下のように「ファイル」メニューから行います.
    事前準備
  2. Form1のTextプロパティを変更して,ウィンドウのタイトルを「ブロック崩し」に変更します.Textプロパティの値が「Form1」となっているので,ここに「ブロック崩し」と入力して下さい.
    事前準備
  3. ここまでできたら,念のためプロジェクトを保存しておきましょう.

2.各コントロールの配置

今回は1つのピクチャーボックスがゲームのメイン画面になるため,置くべきコントロールの数は(ゲームの規模に比べて)少なめです.

配置とプロパティ

まずは以下のようにコントロールの配置を行って下さい.タイマーコントロールに関しては,フォーム上に配置しようとすると自動的に画面下の方に配置されますので問題ありません.

各コントロールの配置

コントロールの配置が終わったら,以下のようにプロパティの設定を行って下さい.

コントロール名プロパティ備考
PictureBox1Size320, 320ゲーム画面のサイズは320x320ドット
Label1Textレベル
Label2Textスコア
Label3Text初期レベル
TextBox1TextAlignRightレベルの表示を右寄せに
TextBox2TextAlignRightスコアの表示を右寄せに
TextBox3TextAlignRight初期レベルの表示を右寄せに
TextBox3Text1初期レベルの初期値は1
Button1Textスタート「スタート/やめる」ボタン
CheckBox1Text一時停止ゲームの一時停止(ポーズ機能)用
Timer1EnabledTrueタイマーを有効化
Timer1Interval50タイマーの間隔を0.05秒に設定

コントロール名の変更

今回も名称が紛らわしいコントロールがいくつか存在するので,以下の表を参照して各コントロールの改名を行って下さい.

コントロール名プロパティ備考
PictureBox1(Name)PictureGamenゲームのメイン画面
TextBox1(Name)TextLevel現在のレベルの表示用
TextBox2(Name)TextScore現在のスコアの表示用
TextBox3(Name)TextSyokiLV「スタート」時のレベルの設定用
Button1(Name)ButtonStartゲームのスタートボタン
CheckBox1(Name)CheckPause一時停止(ポーズ)用チェックボックス
Timer1(Name)TimerGameゲームのメイン処理用タイマー

3.ゲーム起動時の初期化

まずは,ゲームを起動してタイトルロゴが表示されるところまでプログラムを組んでみます.「変数」という新しい概念が登場するので慎重に作業を進めてください.

プログラムの追加

では,フォーム上の何もない部分(コントロールが置かれていない部分)をダブルクリックし,プログラムを追加しましょう.Loadイベントの外側(上の方)にも追加すべきコードがあるので注意!

Public Class Form1

    '変数(システム関係)
    Dim g As Graphics   '画面に描くためのペン
    Dim playing As Boolean  'プレイ中かどうか(True,False)

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        '画面とペンの初期化
        PictureGamen.Image = New Bitmap(PictureGamen.Width, PictureGamen.Height)
        g = Graphics.FromImage(PictureGamen.Image)

        'その他の初期化
        Randomize() '乱数の初期化
        playing = False 'プレイ中ではない状態にする

        'タイトルロゴを表示
        g.Clear(Color.Black)    '画面をクリアする
        g.DrawString("ブロック崩し", New Font("MS 明朝", 32, GraphicsUnit.Pixel), Brushes.Yellow, 64, 144)
        PictureGamen.Invalidate()   '画面を更新する

        'キー入力を受け付けるようにする
        Me.KeyPreview = True
    End Sub
End Class

実行

プログラムを追加したら,まずは実行してみましょう.以下のように,黒い背景の中に「ブロック崩し」という黄色いタイトルロゴが出てきたら成功です.

タイトルロゴ

「変数」=コード自体に持たせる「プロパティ」

今回追加したプログラムの一番上の方を見て下さい.「g」や「playing」という見慣れない文字がDimと共に現れています.まず,「g」の方はお絵かきソフトの回で出てきた「pen」と同一のものですから特に心配はありません.Dim pen as Graphicsという形が,Dim g as Graphicsという形に変わっただけです.

もうひとつのplayingの方は「変数」という新しい概念です.変数とは,プログラムのコード自体に持たせる「プロパティ」のことだと思っていただければ分かりやすいでしょう.

ちなみに,この変数は「Boolean」という種類の変数です(他の種類については後述します).Booleanという種類の変数には,「True」または「False」の値を書き込むことができます.つまり,「ブロック崩しというゲームにはplayingという名前のプロパティ(=変数)があって,playingプロパティにはTrueまたはFalseのどちらかをセットできる」と解釈していただければOKです.なお,このplayingという変数は「ゲームをプレイ中かどうか」を記憶しておくために用意した変数です.

その他の解説

4.ゲームの開始

この章(4.ゲームの開始)では,実際に見た目として分かる「結果」が出るまでに追加すべきプログラムが他の章に比べて多めになっています.そのため,作業手順をちょっとでも誤るとゲームが動かなくなる可能性がありますので,より慎重に作業を進めて下さい.

特に,プログラムの追加に関しては「赤くなっている部分のみ」を追加するようにして下さい.それ以外の箇所(黒くなっている部分)はVB.NETによって自動的に追加されるか,または以前に追加した部分です.そのため,黒い箇所を貼り付けてしまうと動かなくなる可能性が大ですので特に注意して下さい.

タイトルロゴの画面が出来上がったので,今度は「スタート」ボタンを押して弾やバーが表示されるところまでプログラムを組んでみます.「変数」に続いて「定数」という似たような概念も新たに登場しますので油断は禁物です.

「スタート」と「やめる」の切り替え

まずはスタートボタンを押すたびに,「スタート」と「やめる」が切り替えられるようにします.スタートボタン(ButtonStart)をダブルクリックし,以下のコードを追加して下さい.

    Private Sub ButtonStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonStart.Click
        If playing Then
            'プレイ中だったら,ゲームを停止する
            playing = False
            ButtonStart.Text = "スタート"
        Else
            'プレイ中でなかったら,初期化の上プレイ状態にする
            playing = True
            ButtonStart.Text = "やめる"
            CheckPause.Focus()  '「一時停止」にフォーカスをあてる
        End If
    End Sub

実行

プログラムを追加したら,実行して,以下のことができているかどうかを確認します.

解説

「スタート」ボタンを押すたびに,先ほど追加した変数であるplayingの値の切り替えを行っています.この後追加して行く処理では,このplayingの値がTrueなのかFalseなのかを見たうえで,実際にブロック崩しとしての処理を行うかどうかを決定します.

また,スタート時にはCheckPause.Focus()によって「一時停止」チェックボックスにフォーカスをあてる(カーソルをあてる)ようにしています.カーソルがチェックボックスにいくことによって,(わざわざクリックしなくても)スペースキーを押すことによってポーズのON/OFFを切り替えることができるようになるわけです.

「定数」と「変数」の追加

ゲームを進めるうえで必要になる定数と変数を追加します.変数については前に説明したとおりですが,「定数」については後ほど説明します.

それでは,コード画面を開き,プログラムの一番上の方に以下の赤い部分のコードを追加して下さい.「定数」は「変数(システム関係)」よりも上に,「変数(ゲーム関係)」は「変数(システム関係)」よりも下に配置することに注意して下さい.

Public Class Form1
    '定数
    Const BAR_HABA As Integer = 60   'バーの幅を指定(整数)
    Const BAR_SPEED As Integer = 8   'バーのスピードを指定(整数)
    Const TAMA_SPEED As Integer = 5  '弾の初期スピード(整数)
    Const TAMA_MAXSP As Integer = 32 '弾の最大スピード(整数)
    Const TEN_HANSYA As Integer = 1 '弾が壁に1回反射したときの得点(整数)
    Const TEN_UKETORI As Integer = 10   '弾をバーで1回受け止めたときの得点(整数)
    Const TEN_HIT As Integer = 100    'ブロックに1回あたったときの点数(整数)
    Const TEN_SPEED As Integer = 20 '何点ごとに弾のスピードが1段階上がるか(整数)

    '変数(システム関係)
    Dim g As Graphics   '画面に描くためのペン
    Dim playing As Boolean  'プレイ中かどうか(True,False)

    '変数(ゲーム関係)
    Dim level As Integer '現在のレベル(整数)
    Dim score As Integer '現在のスコア(整数)
    Dim tamaX As Double '弾の中心位置X(数値)
    Dim tamaY As Double '弾の中心位置Y(数値)
    Dim kakudo As Double    '弾の角度・計算用(数値)
    Dim tamaMukiX As Double    '弾の向きX(数値)
    Dim tamaMukiY As Double    '弾の向きY(数値)
    Dim tamaYosokuX As Double  '弾の予測位置X・当たり判定計算用(数値)
    Dim tamaYosokuY As Double  '弾の予測位置Y・当たり判定計算用(数値)
    Dim bar As Double  'バーの左隅位置(数値)

定数について

「変数」に加えて「定数」という新しいものが登場しました.しかし,「定数」は「変数」とほとんど一緒です.

「定数」とは,値を変更することができない変数のことだと思って下さい.つまり,値を読み込むことができても設定することができないプロパティであることを意味します.

Boolean以外の「種類」について

今回新しく追加された変数や定数には,Booleanに加えて「Integer」や「Double」という新しい名前の種類が出てきました.これらの「種類」は,厳密には「型」と呼ばれます.

今回追加した定数や変数の意味

各定数や各変数の右側にコメントをつけておきましたので分かるとは思いますが,今回追加した定数や変数は「ブロック崩し」の処理を実現するための鍵となるものです.

「定数」の方には,弾を跳ね返すためのものである「バー」の動くスピードや,ブロックを破壊するためのものである「弾」の初期スピードなどが設定されています.後ほどゲームバランスの調整を行うときに「定数」の形になっていた方が修正しやすいからです(定数化しないでプログラム中にそのまま数値などを書いてしまうと,修正箇所を探すときに非常に苦労してしまうものです).

一方,「変数」の方には,ゲーム中に刻一刻と変化する値を記憶するためのものが入っています.プレイヤーのキー操作によって動く「バー」の位置や,ブロックを破壊するために飛んで行く「弾」の位置などが代表例です.もちろん「スコア」も刻一刻と変化しますから変数化されています.

また,気づいた方もおられるかもしれませんが,「定数」の名前がすべてアルファベットの大文字で表現されています.これは定数であることを分かりやすくするために,プログラマーの間で「昔からの慣習」として行われているものです.

弾やバーなどの初期配置を行うサブルーチン

追加した変数の中には,tamaXやtamaY,barなどのように弾やバーの位置を記憶するための変数が含まれています.しかし,ゲームを実際にスタートするためには,あらかじめ弾やバーなどの初期配置を決定しておかなければなりません.そこで,弾やバーなど,代表的な変数を初期化するためのサブルーチンを用意することにします.

また,サブルーチンを追加することになるので,ButtonStartに対してはサブルーチンを呼び出すためのコードを追加することになります.具体的には,以下のプログラムの赤くなっている部分が新規追加分となります.

    Private Sub ButtonStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonStart.Click
        If playing Then
            'プレイ中だったら,ゲームを停止する
            playing = False
            ButtonStart.Text = "スタート"
        Else
            'プレイ中でなかったら,初期化の上プレイ状態にする
            playing = True
            ButtonStart.Text = "やめる"
            SyokiHaichi()   '初期配置を行う
            CheckPause.Focus()  '「一時停止」にフォーカスをあてる
        End If
    End Sub

    '初期配置を行うサブルーチン
    Private Sub SyokiHaichi()
        '弾の最初の角度をランダムに決定(200度〜340度の範囲内で)
        kakudo = (Rnd() * 140 + 200) * Math.PI / 180
        tamaMukiX = Math.Cos(kakudo)    '「コサイン」で横方向の強さを算出
        tamaMukiY = Math.Sin(kakudo)    '「サイン」で縦方向の強さを算出
        tamaX = 160 '弾の初期位置を設定(X座標)
        tamaY = 295 '弾の初期位置を設定(Y座標)
        bar = (320 - BAR_HABA) / 2 'バーを初期位置(中央)に戻す
    End Sub

End Class

解説

初期配置を行うサブルーチンの中では,以下の3点について初期化を行っています.

このうち,弾の向きを決定するところが特に難しいところですから詳しく説明します.特に,「サイン」と「コサイン」に関しては高校の数学で習う範囲ですが,ここで一応簡単に説明しておきます.

まず,角度の取り扱いについては,以下のように右側を0度とした時計回りのものとして扱われます.

角度について

サインは「距離1に対して縦方向に進んだ距離」を意味し,コサインは「距離1に対して横方向に進んだ距離」を意味します.例えば,以下の画像のように弾が30度の方向に進んだとします.すると,実際に進んだ距離は1ですが,サイン30度を計算すると0.5に,コサイン30度を計算すると約0.866になります(計算自体はコンピュータが自動的にやってくれます).

サインとコサイン

VB.NETには,「30度の方向に弾を飛ばせ」ということを指示するための命令がないので,このように「縦方向」と「横方向」に分けて計算してあげなければならないのです.しかし,この2つはサインとコサインをコンピュータに計算させるだけで求めてくれるので,それほど多くの行数をとらずに済む訳です(コンピュータの内部では「マクローリン展開」と呼ばれる特別な手法で計算が行われています).

また,プログラム中に書かれているRnd()は,0以上1未満のランダムな数値を返してくれるものです.したがって,これを140倍することによって,0以上140未満のランダムな数値になります.

発射される弾の向きは200度から340度の間でランダムに決定することになるので,このランダムな数値に200を足すことによって,ランダムな数の範囲が200〜340になる訳です.

発射される弾の向き

ただし,コンピュータは角度を「度」で表しても理解してくれません.コンピュータの内部では,角度は「度」としてではなく「ラジアン」として表さなければなりません.

普段使っている角度の単位である「度」は,0度から始まって180度で半周する単位ですが,「ラジアン」は0ラジアンから始まって約3.14ラジアン(円周率と同じ値)で半周する単位です.つまり,「度」として求めた角度を「ラジアン」に変換するためには,「度」に円周率(Math.PI)を掛け算したものを180で割ってあげる必要があるのです.

このように,「角度」を扱うプログラムでは算数や数学が重要になってくるため,慣れないうちは理解できないかもしれません.将来プログラマーを目指す方は今のうちに算数や数学に慣れておいた方が良いでしょう.

弾やバーを表示する

さて,初期配置が終わったので,いよいよ弾とバーを表示するプログラムを書くことになります.弾の位置やバーの位置は刻一刻と変化するものですから,処理自体はタイマーの中に書くことになります.デザイナ画面でTimerGameをダブルクリックして以下のコードを追加します.

    Private Sub TimerGame_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerGame.Tick
        'プレイ中でないときは何もしない
        If Not playing Then
            Exit Sub
        End If

        '画面のクリア
        g.Clear(Color.Black)

        '画面への描画
        g.FillEllipse(Brushes.White, CInt(tamaX) - 5, CInt(tamaY) - 5, 10, 10) '弾
        g.FillRectangle(Brushes.Orange, CInt(bar), 300, BAR_HABA, 10)   'バー

        PictureGamen.Invalidate()   '画面を更新する
    End Sub

実行

ここまでのプログラムが無事追加できていれば,弾とバーが画面上に表示されるようになります.実際にプログラムを実行し,「スタート」ボタンを押してみて下さい.すると,「ブロック崩し」の文字が消え,画面下部にバーと弾が表示されるはずです.

ゲームの開始

ただし,まだ「実際に弾やバーを動かすプログラム」を組んでいないので,どちらも静止したままです.

解説

初期配置を行うサブルーチンの中でtamaXやtamaY,barなどの値を設定しました.今回追加したプログラムでは,それらの変数の中に設定されている値(位置情報)をもとに,FillRectangleやFillEllipseを使って四角形や円として描いています(これらの命令の使い方は第3回の「お絵かきソフト」のときに説明した通りです).

ちなみに,変数名についているXやYという名称は,それぞれ「X座標」,「Y座標」の意味です.

また,座標は以下の例のように(X座標, Y座標)の形で記述されます.例えば画面の中心の場合は(160,160)になります.

座標

5.弾とバーを動かしてみよう

さて,まだまだ追加すべきプログラムは多いですが,完成はまだまだ先です.これから,「変数」を駆使して弾やバーを動かすことができるようにしてみます.

バーを操作できるようにする

では,実際にバーを操作できるようにするために,いくつかコードを加えていきましょう.まずは「キーが押されているかどうか」を管理するための変数を追加します.コード画面で,「変数(システム関係)」のところに以下の2つの変数を追加して下さい.

    '変数(システム関係)
    Dim g As Graphics   '画面に描くためのペン
    Dim playing As Boolean  'プレイ中かどうか(True,False)
    Dim keyHidari As Boolean    '左が押されているかどうか(True,False)
    Dim keyMigi As Boolean  '右が押されているかどうか(True,False)

keyHidariが左ボタンを,keyMigiが右ボタンを管理する変数です(どちらも「押されているかどうか」を表現するためのものなので,Boolean型にしてあります).

追加したら,一旦デザイナ画面に戻り,フォーム上の何も無いところをダブルクリックして下さい.すると,最初に追加したLoadイベントのところにカーソルがジャンプし,Form1のイベントを選択できる状態になります.

ここで,以下の画面を参考に「KeyDown」イベントと「KeyUp」イベントを追加して下さい.

バーを操作できるようにする

追加したら,それぞれに対して以下のコードを貼り付けて下さい.

    Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown
        If e.KeyCode = Keys.F Then
            keyHidari = True  '左が押され始めた!(左キーをON)
        End If
        If e.KeyCode = Keys.J Then
            keyMigi = True  '右が押され始めた!(右キーをON)
        End If
    End Sub

    Private Sub Form1_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyUp
        If e.KeyCode = Keys.F Then
            keyHidari = False  '左が離された!(左キーをOFF)
        End If
        If e.KeyCode = Keys.J Then
            keyMigi = False  '右が離された!(右キーをOFF)
        End If
    End Sub

これで,左右それぞれのキーが押されているときはTrueに,離されているときにはFalseになるような機構が完成しました.プログラムを見ても分かるように,Fキーが左,Jキーが右となっています.

キーの押下情報をもとにバーを動かす

あとは既に作成済みのタイマーイベントに対して以下のプログラムを追加すれば,実際にバーが動くようになります.「画面のクリア」と「画面への描画」の間に追加すべきものであることに注意.

    Private Sub TimerGame_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerGame.Tick
        'プレイ中でないときは何もしない
        If Not playing Then
            Exit Sub
        End If

        '画面のクリア
        g.Clear(Color.Black)

        'キー入力の受付・バーの移動
        If keyMigi Then
            bar = bar + BAR_SPEED    'バーを右へ移動
            If bar + BAR_HABA > 320 Then
                bar = 320 - BAR_HABA 'バーが画面右端を越えないようにする
            End If
        End If
        If keyHidari Then
            bar = bar - BAR_SPEED    'バーを左へ移動
            If bar < 0 Then
                bar = 0 'バーが画面左端を越えないようにする
            End If
        End If

        '画面への描画
        g.FillEllipse(Brushes.White, CInt(tamaX) - 5, CInt(tamaY) - 5, 10, 10) '弾
        g.FillRectangle(Brushes.Orange, CInt(bar), 300, BAR_HABA, 10)   'バー

        PictureGamen.Invalidate()   '画面を更新する
    End Sub

実行

「スタート」ボタンを押した後,実際にキー操作でバーが動くかどうかを確認します.Fキーで左方向へ,Jキーで右方向に移動できます.

また,バーが左端や右端を超えて移動できないことも確かめて下さい.

バーを操作できるようにする

解説

左右それぞれのキーが押されたとき,定数BAR_SPEEDに設定されているスピードの分だけバーを動かしています.X座標は右方向へ向かうほど大きな値をとるので,右方向への移動はBAR_SPEEDの分だけを加算,左方向への移動はBAR_SPEEDの分だけを減算するかたちになります.

ただし,バーが左右の端を越えて移動できてしまうのはよくないので,ifを使って現在のバーの位置を判定しています.変数barには,バーの左隅の座標が入っているので,barがゼロよりも小さくならないように監視することで左端を越えてしまうことを防いでいます.一方,右端を判定するときには変数barの値に定数BAR_HABA(バーの幅を表す定数)を加えて右端の座標を算出し,それが320を超えてしまわないように監視しています.

レベルとスコアを設定できるようにする

ブロック崩しでは,通常,ゲームが進行してスコア(得点)が増えていくごとに弾のスピードが上昇していきます.今回作成するブロック崩しでもそのようなシステムを採用することになるため,「スコア」を設定することができるようにします.また,次回はスコアに加えて「レベル」を使用することになりますので,ついでに「レベル」も設定することができるようにします.

サブルーチンの追加

設定処理を使いまわしがきくようにするため,サブルーチン化しておきたいと思います.また,現在のスコアから弾のスピードを計算するためのサブルーチンも作成しておきます.以下の3つのサブルーチンをタイマーイベントの下のあたりに追加して下さい.

    'レベルをセットするサブルーチン
    Private Sub SetLevel(ByVal atai As Integer)
        level = atai
        TextLevel.Text = atai
    End Sub

    'スコアをセットするサブルーチン
    Private Sub SetScore(ByVal atai As Integer)
        score = atai
        TextScore.Text = atai
    End Sub

    '現在のスコアから弾のスピードを計算するサブルーチン(関数)
    Private Function GetSpeed() As Integer
        Dim result As Integer
        result = CInt(score / TEN_SPEED) + TAMA_SPEED
        If result > TAMA_MAXSP Then
            result = TAMA_MAXSP '弾が最大スピードを超えないようにする
        End If
        Return result
    End Function

弾のスピードを計算するサブルーチンは,値を返すことのできる「関数」と呼ばれる形をしています.関数とは,これまでに登場したMath.Sin()やMath.Cos(),およびランダムな数を返してくれるRnd()のように,「内部で処理を行う」のと同時にその結果を「値として返却する」機能を持った特別なサブルーチンのことです.

今回作成したGetSpeed()では,内部でresultという変数を作成したうえで,その変数の中にスピードの計算結果を設定し,Return命令を発行することによって値を確定しています.これにより,外部からGetSpeed()のような書式で呼び出してあげることによって,GetSpeed()の部分が「数値」としてみなされます.関数の名前の後ろにAs Integerと書かれていますので,GetSpeed()の値は「Integer型」であることを意味します.

また,サブルーチンの場合はPrivate Subのような形をしていましたが,関数の場合はPrivate Functionのような形になります.Functionとは,英語で「関数」を意味する単語です.

ゲーム開始時にレベルとスコアを初期化する

必要なサブルーチンの追加が完了したので,ゲーム開始時にレベルとスコアを初期化するようなプログラムを組み込んでみましょう.ゲームの開始は「スタート」ボタンで行うものですから,ButtonStartに対して以下の2行を追加することになります.

    Private Sub ButtonStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonStart.Click
        If playing Then
            'プレイ中だったら,ゲームを停止する
            playing = False
            ButtonStart.Text = "スタート"
        Else
            'プレイ中でなかったら,初期化の上プレイ状態にする
            playing = True
            ButtonStart.Text = "やめる"
            SetLevel(TextSyokiLV.Text)   'レベルを「初期レベル」に設定
            SetScore(0)   'スコアを0に設定
            SyokiHaichi()   '初期配置を行う
            CheckPause.Focus()  '「一時停止」にフォーカスをあてる
        End If
    End Sub

先ほど追加したSetLevel()とSetScore()を呼び出してレベルとスコアを設定しているだけなので特に問題はないでしょう.

実行

「スタート」ボタンを押した際に,レベルの値が「初期レベル」の値に,スコアの値が「ゼロ」になることを確認して下さい.

レベルとスコアの初期化

弾が動くようにする

弾を動かすための準備が整いましたので,実際に弾が動くようにプログラムを追加してみましょう.後々当たり判定を追加することになるため,ちょっと特別なコードを追加することになります.

まずは,弾を動かすために必要な変数を「変数(システム関係)」のところに2つ追加します(これらの変数の意味については後述します).

    '変数(システム関係)
    Dim g As Graphics   '画面に描くためのペン
    Dim playing As Boolean  'プレイ中かどうか(True,False)
    Dim keyHidari As Boolean    '左が押されているかどうか(True,False)
    Dim keyMigi As Boolean  '右が押されているかどうか(True,False)
    Dim i As Integer   '繰り返し計算用として使用
    Dim j As Integer    '繰り返し計算用として使用

次に,実際に弾を動かすためのコード(+当たり判定を行うための予備のコード)を追加します.追加する場所はタイマーイベント内の「画面のクリア」処理の直後です.

    Private Sub TimerGame_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerGame.Tick
        'プレイ中でないときは何もしない
        If Not playing Then
            Exit Sub
        End If

        '画面のクリア
        g.Clear(Color.Black)

        '弾の移動(1ドット単位)および当たり判定
        For i = 1 To GetSpeed()
            '現在の弾の位置と向き,スピードにより次の弾の位置をはじき出す
            tamaYosokuX = tamaX + tamaMukiX
            tamaYosokuY = tamaY + tamaMukiY

            '当たり判定終了後,弾を実際に移動させ,軌跡を描画する
            tamaX = tamaX + tamaMukiX
            tamaY = tamaY + tamaMukiY
            g.FillEllipse(Brushes.Gray, CInt(tamaX) - 5, CInt(tamaY) - 5, 10, 10)
        Next

        'キー入力の受付・バーの移動

実行

「スタート」ボタンを押すと,弾がランダムな方向(200度〜340度の範囲内)に飛び出します.ただし,まだ「当たり判定」のプログラムを書いていないため,隅に当たっても反射することはなく,そのまま場外に飛び出してしまいます.

ここでは,何度も「スタート」,「やめる」を繰り返して,毎回弾が異なる方向に飛び出すことを確認して下さい.

弾が動くようにする

解説

For〜Nextという新しい形の命令が出てきました.これは,ForとNextで囲まれた部分を繰り返し実行する命令です.For文を実行するためには,繰り返し回数を制御するための変数が必要で,一般的にiやjなどという名前の1文字の変数が使われることが多いです.

Forの後ろには,繰り返しの「初期値」と「終了値」を書きます.例えば,For i = 1 to 10と書けば,変数iの値が1,2,3,...と増えていって,iが10になるまで繰り返しが行われることになります.

今回のプログラムの場合では,「終了値」の部分にGetSpeed()が指定されています.つまり,現在の弾のスピードの分だけ繰り返し実行し,弾を移動させるという意味になります(スピードが上がって行けば,繰り返し回数が増えるため,タイマーイベントが1回発生するごとの弾の移動量が大きくなり,結果的に弾がはやく動くようになる,といった仕組みです).

なお,Forの中では,当たり判定を行う関係上弾の予測位置の算出を行っていますが,現在は当たり判定が行われていないため,「予測位置」と「実際に移動する先の位置」が同じになっています.また,移動していることを分かりやすく表現するために,弾本体(白色)よりも若干暗めの灰色で弾の軌跡を描くようにしています.

ポーズができるようにする

ゲームをプレイしていると,目が疲れてきたりトイレに行きたくなったりすることがあります.そのためには一時的にゲームを停止(ポーズ)する必要が生じます.そこで,「一時停止」チェックボックス(CheckPause)に対してタイマーのON/OFFを切り替えるコードを追加しておくことにしましょう.デザイナ画面上で「一時停止」チェックボックスをダブルクリックし,以下のコードを追加します.

    Private Sub CheckPause_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CheckPause.CheckedChanged
        TimerGame.Enabled = Not CheckPause.Checked
    End Sub

TimerGameのEnabledプロパティの値を,チェックボックスのチェックとは逆の値(NotをつけることによってTrueとFalseが逆転します)を代入して,タイマーのON/OFFを制御する訳です.

実行

「スタート」ボタンを押して弾が飛んでいく状態で,チェックボックスをクリックしてチェックを入れることによって弾の動きが一時的に停止します(もちろん,一時停止中はバーを移動することもできなくなります).そして,チェックを外せば再び弾が動き出します.

なお,「スタート」ボタンを押したあとチェックボックスに対してフォーカスが行くようにプログラムしておいたので,スペースキーを押すことによってもチェックの切り替えができるようになっています.

一時停止

6.当たり判定を追加しよう

弾とバーの移動ができるようになりました.あとは弾が壁やバーに当たったときの「当たり判定」を組み込みさえすれば,「ブロックの無いブロック崩し」が完成します.

壁に当たって反射するようにする

まずは上または左右の壁,すなわち隅にぶつかって弾が反射するような当たり判定を組んでみましょう.以下のプログラムを,タイマーイベント中のFor文の中(弾の予測位置をはじき出した直後)に追加します.

        '弾の移動(1ドット単位)および当たり判定
        For i = 1 To GetSpeed()
            '現在の弾の位置と向き,スピードにより次の弾の位置をはじき出す
            tamaYosokuX = tamaX + tamaMukiX
            tamaYosokuY = tamaY + tamaMukiY

            '隅との当たり判定
            If tamaYosokuX < 0 Or tamaYosokuX >= 320 Then
                tamaMukiX = -tamaMukiX    '左右の隅とぶつかった場合
                SetScore(score + TEN_HANSYA) 'スコアに反射分の点数を加算
            End If
            If tamaYosokuY < 0 Then
                tamaMukiY = -tamaMukiY    '上隅とぶつかった場合
                SetScore(score + TEN_HANSYA) 'スコアに反射分の点数を加算
            End If

            '当たり判定終了後,弾を実際に移動させ,軌跡を描画する
            tamaX = tamaX + tamaMukiX
            tamaY = tamaY + tamaMukiY
            g.FillEllipse(Brushes.Gray, CInt(tamaX) - 5, CInt(tamaY) - 5, 10, 10)
        Next

実行

このプログラムを組み込むことによって,上または左右の隅に弾がぶつかったときに反射をおこすようになります.実際に実行して確認してみて下さい.また,1回反射したときに定数TEN_HANSYAの分だけスコアが加算されます.ゲームオーバー判定を行っていないため,弾が一番下に達してもゲームが終了しません.

当たり判定

解説

弾の予測位置の座標(tamaYosokuX,tamaYosokuYの2つの変数)が隅に達したとき,「反射」処理を行ったうえで得点を加算するようなプログラムになっています.

「弾が隅に達する」ということは,X座標が0未満になる(左隅)か,X座標が320以上になる(右隅)か,もしくはY座標が0未満になる(上隅)ことを意味します.したがって,弾の座標がこの条件にあるときに反射処理を行えば良いのです.

また,反射する,ということは,「別の方向に飛んで行く」という意味になります.つまり左隅もしくは右隅にぶつかったときは弾の横方向の向きを逆向きにすれば良く,上隅にぶつかったときは弾の縦方向の向きを逆向きにすれば良い訳です.

ゲームオーバーになるようにする

いつまでたっても「ゲームオーバー」にならなければゲームとしての意味がありません.そこで,弾が下隅に達したときに「ゲームオーバー」とし,変数playingの値をFalseにするようなプログラムを組むことにします.以下のコードを,「画面のクリア」の直前に追記して下さい.

    Private Sub TimerGame_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimerGame.Tick
        'プレイ中でないときは何もしない
        If Not playing Then
            Exit Sub
        End If

        '弾がとれなかったらゲームオーバー
        If tamaY > 325 Then
            playing = False 'プレイ中状態を解除する
            ButtonStart.Text = "スタート"
            g.FillRectangle(Brushes.White, 0, 128, 320, 64) '背景を白色に
            g.DrawString("ゲームオーバー!", New Font("MS 明朝", 32, GraphicsUnit.Pixel), Brushes.Red, 32, 144)
            PictureGamen.Invalidate()   '画面を更新する
            Exit Sub    '当たり判定を終了する
        End If

        '画面のクリア
        g.Clear(Color.Black)

実行

実際に実行してみて下さい.これで弾が下端に達した時に「ゲームオーバー」と表示されるようになったはずです.なお,「スタート」ボタンをもう一度押せば再びプレイできるようになります.

当たり判定

弾をバーで受け取れるようにする

後は弾をバーで受け取れるようにさえすればゲームになります.この処理を追加すれば今日の範囲は完了です!

なお,先ほど追加した「隅との当たり判定」の直後にコードを追加するようにして下さい.

            '隅との当たり判定
            If tamaYosokuX < 0 Or tamaYosokuX >= 320 Then
                tamaMukiX = -tamaMukiX    '左右の隅とぶつかった場合
                SetScore(score + TEN_HANSYA) 'スコアに反射分の点数を加算
            End If
            If tamaYosokuY < 0 Then
                tamaMukiY = -tamaMukiY    '上隅とぶつかった場合
                SetScore(score + TEN_HANSYA) 'スコアに反射分の点数を加算
            End If

            'バーとの当たり判定を計算
            If tamaYosokuY >= 300 And tamaYosokuY < 310 Then
                '弾がバーの幅の位置にあるかどうか
                If tamaYosokuX >= bar And tamaYosokuX < bar + BAR_HABA Then
                    'バーで受け取った場所に応じて弾を飛ばす向きを変える
                    kakudo = ( _
                        (BAR_HABA - (bar + BAR_HABA - tamaYosokuX)) _
                        * 140 / BAR_HABA + 200) _
                        * Math.PI / 180 '真ん中に近いほど反射が直角になる
                    tamaMukiX = Math.Cos(kakudo)    '「コサイン」で横方向の強さを算出
                    tamaMukiY = Math.Sin(kakudo)    '「サイン」で縦方向の強さを算出

                    'スコアを加算して音を鳴らす
                    SetScore(score + TEN_UKETORI)   '受け取った分の点数を加算
                End If
            End If

実行

実際に弾をバーで受け止めて跳ね返すことができることを確認してみましょう.なお,跳ね返る角度は弾がバーに当たった位置によって変わります(バーの中央に当たればほぼ垂直に跳ね返り,隅の方にあたればかなりキツい角度で跳ね返ります).これはほとんどのブロック崩しで共通ですので,できるだけバーの中央で跳ね返すことがハイスコアを狙うコツの1つであったりもします.

ちなみに,バーで受け止めたときの点数は定数TEN_UKETORIにて設定されています.なお,定数が初期設定の状態では弾のスピードがどんどん上がっていくので,500点を超えることができればかなりすごい方だと思います.

当たり判定

解説

先ほども述べたように,バーに対して当たった場所に応じて跳ね返す角度を変えることになるので,ここでもやはりサインとコサインが活躍します.左隅が200度,真ん中が270度(上向きに直角),右隅が340度という設定になっています.

計算式がかなり複雑ですが,よく見てみると理解できるかもしれません.

定数をいろいろいじってみよう

とりあえず,これにて「ブロックの無いブロック崩し」が完成しました.ブロックが無くてもバランスの調整次第で面白くなるものであることが分かっていただけたと思います.

ここまで出来てしまった方は,プログラムの先頭の方に書かれている定数群をいろいろいじってみてゲームバランスを調整してみて下さい.デフォルトの定数は以下に示すとおりです.

    Const BAR_HABA As Integer = 60   'バーの幅を指定(整数)
    Const BAR_SPEED As Integer = 8   'バーのスピードを指定(整数)
    Const TAMA_SPEED As Integer = 5  '弾の初期スピード(整数)
    Const TAMA_MAXSP As Integer = 32 '弾の最大スピード(整数)
    Const TEN_HANSYA As Integer = 1 '弾が壁に1回反射したときの得点(整数)
    Const TEN_UKETORI As Integer = 10   '弾をバーで1回受け止めたときの得点(整数)
    Const TEN_HIT As Integer = 100    'ブロックに1回あたったときの点数(整数)
    Const TEN_SPEED As Integer = 20 '何点ごとに弾のスピードが1段階上がるか(整数)

例えば,一番上のBAR_HABAは最初は60ドットになっていますが,これを120ドットにしてみたらどうでしょう? 受け止めることができる範囲が大きくなるので,ゲームがかなり楽になるはずです.

当たり判定

他にも上に示したとおりいろいろな定数がありますから,自分好みにいろいろいじってみて下さい.

課題

今回やった内容はかなり難しいものであったため,おそらくほとんどの方が十分に理解できなかっただろうと思います.

そこで,次回までの間に是非とももう1度このプログラムをご自宅で組んでみて下さい.時間があると思いますから,解説も熟読されることをおすすめします.