【プログラミング初心者向け】ExcelVBAでスネークゲームを自作してみよう!基礎編

VBA-gif-eyecatch PC

Excelで使えるVBAはプログラミングの基礎を学びやすい簡単な言語です。
Excelのワークシート上で動作するゲームを作って、プログラミングの仕組みを理解してみましょう。

初心者でもわかるように細かく解説していきます。

スネークゲームを自作しよう

スネークゲームとはランダムで出現する餌を回収して蛇の体を伸ばしていくゲームです。
自分の体や壁にぶつかるとゲームオーバーになります。

ヘビゲームって言われることの方が多いかもしれないです。

スネーク

上の画像が僕が作成したスネークゲームです。
難易度選択やランキング機能が付いていますが、まずはゲームとしてプレイできることを目標に組み立てていきます。

室井
このゲームは僕が高校生の時に作ったゲームです。
授業中の暇つぶしを目的に作成しました。

ファイルの保存

プログラミングを始める前に、ファイルを保存しておきましょう。
ファイルを保存する際に、ファイルの種類をExcel マクロ有効ブックに変更してください。

通常の拡張子のまま保存するとVBAのデータが吹っ飛びます。
注意しましょう。

ファイルの保存

外部設計

まずはゲームのプレイ画面を作成します。

外部設計

ワークシートのセルを活用するので、すべてのセルを正方形にしてください。

15×15になるように罫線を引きます。
C3からQ17までの範囲で作成してください。
セル番地がずれるとコードを書き換える必要が出てきて混乱が生じます。

方向を入力するコマンドボタンを4つ作成します。
このコマンドボタンをクリックすると蛇(青いブロック)を操作できるようにします。

コマンドボタン

コマンドボタンは開発タブの挿入から選択できます。

開発タブが表示されていない場合はオプションのリボンのユーザー設定から開発にチェックを入れてください。

オプション

いちいちマウスでクリックするのは面倒なので、キーボードでも操作できるように後で設定します。

スタートボタンがないとゲームが始まりませんね。
方向ボタンの下にでも同様の手順で作っておきましょう。

スタートボタン

コマンドボタンの文字はプロパティから変更できます。
デザインモードに変更してから右クリックで表示できます。

「Caption」が実際に表示される文字列です。
プログラムに記述する際の名称は(オブジェクト名)で指定します。
「Caption」とは異なる点に注意しましょう。

プロパティ

プログラミング

次に、ゲームを動かすためのコードを書いていきます。
開発タブから「Visual Basic」を選択します。

変数の宣言を強制する

ほとんどのプログラミング言語では変数を利用する際に宣言する必要があります。
しかし、VBAは宣言しなくてもエラーを吐きません。

ですが、変数を宣言なしで使える状態だとバグが発生する原因になります。
スペルミスをしていても気がつかないなんてことになりかねません。

オプションから「変数の宣言を強制する」にチェックをいれておきましょう。
1行目に「Option Explicit」という記述が自動で追加されるようになります。

変数の宣言を強制する

また、僕は「自動構文チェック」はうっとうしいので外しています。
プログラミングに慣れてきたら外すといいでしょう。

標準モジュールの追加

プログラムを記述する標準モジュールというものを追加します。
挿入タブから標準モジュールを選択しましょう。

標準モジュール

ほとんどのコードは標準モジュール(Module1)に記述していきますが、外部設計で追加したコマンドボタンに関する記述はSheet1に記述します。

初期化処理の作成

初期化処理に関するプロシージャをModule1に作成します。

プロシージャとは命令の固まりのようなものです。
プロシージャごとに呼び出すことができます。

Public Sub 初期化()からEnd Subまでが初期化という名前のプロシージャです。

Option Explicit

Public cnt As Integer
Dim i As Integer, j As Integer

Public Sub 初期化()
    cnt = 1 '取得したブロック数
'ゲーム画面を初期化
    For i = 1 To 20
        For j = 1 To 20
            Cells(i, j).Interior.ColorIndex = xlNone
        Next j
    Next i
'ブロック寿命を初期化
    For i = 19 To 35
        For j = 2 To 18
            Cells(i, j) = 0
        Next j
    Next i
End Sub

3行目で宣言したcntは取得した赤いブロックの数を記憶する変数です。
Sheet1からも使えるようにPublicで宣言しています。

室井
パブリック変数で宣言すると他のモジュールからも参照できるよ。
グローバル変数と呼ぶこともあるんだ。

3行目で宣言したijはループ処理で使う変数です。

通常はその場でしか使わないこれらの変数はプロシージャ内で宣言します。
無駄に変数を参照できる範囲(スコープ)を広げるとバグの原因になるためです。

しかし、他のプロシージャでも何度も2重ループを使うのでプロシージャ外に書きました。

7行目でcnt1を代入します。
この変数は後程、ブロックを消すために利用します。

9~13行目はC3からQ17までのセルを透明の背景色で塗りつぶす処理です。
ゲーム開始前にこの処理を入れることで、前回のゲームプレイで残ったブロックを消去します。

15~20行目はブロック寿命を初期化する処理です。

デバッグ作業時にデータを目で確認できるように、ブロック寿命をB20~R35に保存しています。

ブロック寿命とは僕が作った造語です。
ブロックが1マス動くのを1ターンと考え、ブロックが生成されてからの経過ターン数をブロック寿命と呼んでいます。

ゲーム画面が1枚目の画像のようになっているとき、ブロック寿命は2枚目のようになります。

ゲーム画面
ブロック寿命

ブロック寿命とcntを組み合わせることで移動後の不要なブロックを消す処理を後で作ります。
そうしないとこうなってしまうからです。

失敗例
室井
詳しい仕組みは後で解説するから、今はなんとなくでも大丈夫だよ。

ゲームを制御する処理の作成

ブロックの移動や赤いブロックの取得、ゲームオーバーといった処理をModule1に記述していきます。

変数の宣言

まずはInteger型の変数を3つ、Boolean型の変数を2つ作成します。

Integer型の変数はXYmukiとします。
Boolean型の変数はplayfinとします。

Boolean型とは「真 = true」と「偽 = false」のどちらかの値を入れる変数です。

室井
Boolean型がピンとこない人はInteger型で代用してもいいよ。

これらの変数をModule1にパブリック変数として宣言します。

Public X As Integer, Y As Integer, muki As Integer
Public play as Boolean, fin as Boolean

API宣言

処理を指定時間停止できるsleep関数を使用できるようにします。

コンピューターの命令はとても速いので、普通に実行したのではブロックは目にも止まらぬ速さで移動してしまいます。
目にも止まらぬというか、そもそも描写されないです。速すぎて。

なので、ブロックが1マス移動した後処理をループさせる前に指定時間停止させてあげます。

そのために使うのがsleep関数です。
sleep関数を使うことでミリ秒単位で処理を停止できます。

ですが、sleep関数はVBAの関数ではありません。
Windows APIの関数なので、使用するためにはその旨をコンピューターに伝えなければいけません。

Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr) 'sleep関数を使用可能に

モジュールの最初にこの記述を追加してください。

Office2010以降のバージョンを想定した記述方法です。
Office2007以前のバージョンで動作させるには書き換える必要があります。

プロシージャの作成

ゲーム中にループさせる処理をプロシージャに書き込みます。
プロシージャの名前はブロックとしています。
各自でわかりやすい名前にしてください。

プロシージャの中にはこのように記述します。

Public Sub ブロック()
    
    Do While fin = False
    
    'ブロックが表示されてから移動が発生するごとに1ずつ増える数値をブロック寿命とする
        For i = 19 To 35
            For j = 2 To 18
                If Cells(i, j) >= 1 Then
                    Cells(i, j) = Cells(i, j) + 1
                End If
            Next j
        Next i
        
    '"cnt"とブロック寿命を比較しブロックを消去
        For i = 19 To 35
            For j = 2 To 18
                If cnt < Cells(i, j) Then
                    Cells(i - 17, j).Interior.ColorIndex = xlNone
                    Cells(i, j) = 0
                End If
            Next j
        Next i
            
    'ブロックの移動方向を判定
            Select Case muki
                Case 2
                    Y = Y + 1
                Case 4
                    X = X - 1
                Case 6
                    X = X + 1
                Case 8
                    Y = Y - 1
            End Select
            
    'ゲームオーバーの判定
        If Y < 3 Or Y > 17 Or X < 3 Or X > 17 Or Cells(Y, X).Interior.ColorIndex = 5 Then
            fin = True
            play = False
            cnt = cnt - 1
        End If
            
    '赤いブロックを取得した際の判定
        If Cells(Y, X).Interior.ColorIndex = 3 Then
            cnt = cnt + 1
            
    '空白にランダムで赤いブロックを表示
            Dim flag As Boolean
            flag = False
            
            Do Until flag = True
            
                Randomize
            
                i = Int(Rnd * 15 + 1) + 2
                j = Int(Rnd * 15 + 1) + 2
                
                If Cells(i, j).Interior.ColorIndex = xlNone Then
                    Cells(i, j).Interior.ColorIndex = 3
                    flag = True
                End If
            Loop
        End If
            
        Cells(Y, X).Interior.ColorIndex = 5
        Cells(Y + 17, X) = 1 'ブロックの寿命
            
        DoEvents
        
        Sleep 90'待ち時間の調節  
    Loop
End Sub

コメントを読めば大体わかるようにはしたつもりですが、各処理ごとに個別に解説していきます。

まずは5~12行目の処理について解説します。

    'ブロックが表示されてから移動が発生するごとに1ずつ増える数値をブロック寿命とする
        For i = 19 To 35
            For j = 2 To 18
                If Cells(i, j) >= 1 Then
                    Cells(i, j) = Cells(i, j) + 1
                End If
            Next j
        Next i

初期化処理を作成した際に、ブロック寿命をB20~R35に保存することにしましたよね。

2重ループを使ってB20~R35の全てのマスに対してIf文が実行されるようにしています。

64~65行目の処理も合わせて見てください。

        Cells(Y, X).Interior.ColorIndex = 5
        Cells(Y + 17, X) = 1 'ブロックの寿命

青で塗りつぶしたセルに対応したセルに1を代入しています。

ループして5~12行目が実行された際に、1以上の数値が格納されたセルは+1されます。
この処理によって青いブロックが表示されてから何回処理がループしたかがわかるようになります。

処理がループした回数がわかると、任意のタイミングでブロックを消去できるようになります。

14~22行目の処理を見てください。

    '"cnt"とブロック寿命を比較しブロックを消去
        For i = 19 To 35
            For j = 2 To 18
                If cnt < Cells(i, j) Then
                    Cells(i - 17, j).Interior.ColorIndex = xlNone
                    Cells(i, j) = 0
                End If
            Next j
        Next i

ここでcntという変数が出てきます。
初期化処理を作成した際に宣言しましたね。

cntは赤いブロックを取得した数を記録する変数です。
初期値が0ではなく1に設定したことに注意してください。

If cnt < Cells(i, j) Then が実行される条件を考えてみましょう。

ゲーム開始直後の状態ならcntには1が格納されています。

64~65行目で青くなったセルに対応したセルにも1が格納されます。
このセルがC3、対応したセルはC20、つまりCells(20, 3)だったと仮定します。

ループして5~12行目が実行されるとCells(20, 3)の値は+1されて2になります。

cnt < Cells(20, 3)の条件式を満たすので、Cells(20, 3)の値は0になります。
また、Cells(20 – 17, 3)であるC3のセルは透明になります。

これが赤いブロックを取得する前の状態です。

では、赤いブロックを3個取得した状態も考えてみましょう。
青いブロックの長さは4マス分にならないといけません。

cntに赤いブロックを取得した数を足していく処理を作りましょう。

42~44行目を見てください。

    '赤いブロックを取得した際の判定
        If Cells(Y, X).Interior.ColorIndex = 3 Then
            cnt = cnt + 1

これが赤いブロックを取得した際の処理です。
移動後の座標のセルが赤い場合にcntに+1されます。

なお、赤いセルは64行目の処理で青に上書きされます。

ブロックを3個取得した状態なら、cntには4が格納されているはずです。

ブロックが消去されるのは cnt < Cells(20, 3) の条件式を満たす必要がありますから、ブロック寿命が5になったタイミングです。
それまではブロックは消えずに残り続けます。

ブロックを消去する処理が実行されるのを3回後に遅らせることができますね。

みゅう
何言ってるのか全然わかんないんだけど( ;∀;)
室井
とりあえず全体をザックリ読んで、実際に書いてみたらいいと思うよ。
読んだだけで100%理解できなくても大丈夫!

次に、ブロックを操作するための命令を作りましょう。
24~34行目を見てください。

    'ブロックの移動方向を判定
            Select Case muki
                Case 2
                    Y = Y + 1
                Case 4
                    X = X - 1
                Case 6
                    X = X + 1
                Case 8
                    Y = Y - 1
            End Select

mukiという変数に応じてXYの値を増減させています。
これによって処理をするセルの座標を移動させます。

条件を2・4・6・8にしたのは、テンキーと対応させると自分が覚えやすかったからです。
8で上、2で下、4で左、6で右に移動させます。

Y = Y + 1 が下、Y = Y 1が上に進む点が勘違いしやすいので注意してください。

室井
我流でコードを書いてるので、わかりづらい部分は臨機応変に手直ししてね。

mukiに変数を代入するための処理も作りましょう。

外部設計

外部設計で4つのボタンを作りましたよね。
デザインモードに切り替えて、ダブルクリックしてください。

すると、Sheet1というコードを入力する画面が開くはずです。

Private Sub Button右_Click()
    If muki <> 4 Then
        muki = 6
    End If
End Sub

Private Sub Button下_Click()
    If muki <> 8 Then
        muki = 2
    End If
End Sub

Private Sub Button左_Click()
    If muki <> 6 Then
        muki = 4
    End If
End Sub

Private Sub Button上_Click()
    If muki <> 2 Then
        muki = 8
    End If
End Sub

それぞれのボタンにこのようなコードを追加します。
進行方向と逆に移動してしまうと、自身とぶつかりゲームオーバーしてしまうので防ぎます。

僕はオブジェクト名をわかりやすく変更しています。
コピペでは動かないので注意が必要です。

デフォルトではCommandButton○になっているので、変更しておきましょう。

方向ボタンはこれで完成です。
しかし、このままではゲーム中に押しても何も反応しません。

プログラムをループさせてる間、こちらの処理は実行されません。
ゲームオーバーして処理を抜けた後にようやく反映されます。

なので、DoEvents関数を使って処理に割りこめるようにします。
67行目にDoEventsと書いておきましょう。

次はゲームオーバーの処理を作っていきます。
36~40行目を見てください。

    'ゲームオーバーの判定
        If Y < 3 Or Y > 17 Or X < 3 Or X > 17 Or Cells(Y, X).Interior.ColorIndex = 5 Then
            fin = True
            play = False
        End If

指定した範囲からはみ出して青く塗りつぶしたら、finをTrueに切り替えてループから抜けます。
ゲームをプレイ中かどうかを判別する変数、playFalseにします。

みゅう

finとplay、1つにまとめられない?

室井

僕もそう思います。

みゅう

じゃあ直そうよ(‘Д’)

赤いブロックをランダムに表示させる処理を作ります。
46~62行目を見てください。

    '空白にランダムで赤いブロックを表示
            Dim flag As Boolean
            flag = False
            
            Do Until flag = True
            
                Randomize
            
                i = Int(Rnd * 15 + 1) + 2
                j = Int(Rnd * 15 + 1) + 2
                
                If Cells(i, j).Interior.ColorIndex = xlNone Then
                    Cells(i, j).Interior.ColorIndex = 3
                    flag = True
                End If
            Loop
        End If

透明なセルの座標を取得できるまで50~61行目を繰り返します。

Rndは0以上1未満の乱数を発生させる関数です。

例えば1~10の整数をランダムに取得したい場合、Int(Rnd * 10 + 1) となります。
これは公式のようなものなので覚えてください。

今回は3~17の整数を取得できるようにしています。

Randomizeはランダムにシード値を指定する処理です。
シード値とはRndで乱数を発生させる際に使用する数値です。

Randomizeを入れないと、Excel起動時にシード値が同じになってしまいます。
忘れずに書きましょう。

最後にSleep関数を記述します。
69行目の部分ですね。

Sleep○ と記述することで処理を指定時間停止できます。

ここの数値を変更することでゲームの難易度を調節できます。

室井
ここまでくればゲームはほとんど完成だ!
最後にゲームを開始するスタートボタンを作ろう。

スタートボタンの作成

スタートボタンに以下のようなコードを記述します。

Private Sub CommandButton1_Click()
    If play = False Then
        Call 初期化
        Call ランダム配置
        fin = False
        play = True
        X = 10
        Y = 10
        muki = 2
        Call ブロック
    End If
End Sub

最初にCall文を使って「初期化」のプロシージャを呼び出します。
1つ目の赤いブロックを配置する必要があるので、ランダム配置というプロシージャを新たに作りました。
Public Sub ランダム配置()
'空白にランダムで赤いブロックを表示
    Dim flag As Boolean
    flag = False
    
    Do Until flag = True
    
        Randomize
    
        i = Int(Rnd * 15 + 1) + 2
        j = Int(Rnd * 15 + 1) + 2
        
        If Cells(i, j).Interior.ColorIndex = xlNone Then
            Cells(i, j).Interior.ColorIndex = 3
            flag = True
        End If
    
    Loop
End Sub

先ほど作った「ブロック」のプロシージャに書いたものと全く同じです。
本来は同じコードを複数回書くことは美しくないのですが、DoEvents関数を使用する都合上こうなりました。

後はゲーム開始時の座標と進行方向を格納して、「ブロック」のプロシージャを呼び出します。

これでゲームとしてプレイできるようになりました。

あとがき

無事にゲームを動作させることはできましたか?

今回は基礎編ということで、プレイ可能になるまでのプログラムを組みました。
次回はキーボードからの操作に対応させる方法を解説します。

また、難易度選択やランキング機能も追加していく予定です。

難易度変更

【プログラミング中級者向け】ExcelVBAでスネークゲームを自作してみよう!応用編