本当です。
エクセルでゲームを作った変態すごい人達が世の中にはいます。
私も2018年に変態の仲間入りその偉人達の仲間入りすることが出来ました。
そもそも、世の中にはScratchやらUnityなどのゲームをつくるプログラムがあるのに、なぜエクセルでゲームを作ってしまうのか。。。
というかエクセルでインベーダを作る場合、勉強も含めて結構な時間を投資しましたが、Scratchでインベーダを作るのは勉強を含めても1日でできます。(今ではScratchなら10分あれば簡単なインベーダを作れます)
それでもエクセルでゲームを作りたい方は、お進みください。
ゲーム画面
キー入力取得という概念は分かりやすいですね。
キーボードの右・左・上・下・スペースキーが入力されているかどうかの判定ですね。
エクセルvbaでキーボード入力を検知できるようにするには、VBAの一番最初にこう書きます。
Declare Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Long
そしてコード本文にて、このように書きます
If GetAsyncKeyState(39) <> 0 Then
(処理1)
End If
ここで39というのは右キーのことを指しています。
これで、右キーが押されていたら(処理1)が行われます。
(参考)
左キー:37
上キー:38
右キー:39
下キー:40
スペースキー:32
Scratchをやっている人なら、上のコードが
「もし」ブロックに「右キーが押されていたら」を入れている状態と同じだと思えばOKです。
(処理1)に、「自機のX方向を1動かす」という命令を入れておけば、自機を右方向に動かすことが出来ます。
私は、インベーダゲームの自機を動かすのに、次のコードを使いました。
(注)JikiCというのはJiki(自機)のColumn(列)の意味で作ったint変数。
If GetAsyncKeyState(39) <> 0 Then
If JikiC < 10 Then
JikiC = JikiC + 1
End If
End If
If GetAsyncKeyState(37) <> 0 Then
If JikiC > 1 Then
JikiC = JikiC - 1
End If
End If
ちなみにJikiCを1~10の範囲に限定しているのは、画面外に飛び出てしまわないようにするためです。
恒常ループというのは、無限ループのことです。
Do while文を作って無限ループを作ります。
エクセルVBAを普段使っている人には違和感MAXだと思いますが、ゲームの世界では無限ループは当たり前に用いられます。
というよりも、人間の操作やセンサーの信号を待っているプログラムは、基本的に無限ループしています。
Dim GameFlag as integer: GameFlag = 0
Do while GameFlag = 0
(処理2)
Loop
このようにすると、GameFlagがゼロのままなので処理2が永遠に繰り返されます。
よって、ここを次のようなフローのプログラムを作ると、シューティング系のゲームができます。
Do while GameFlag = 0
①キー入力判定(左右キー)
もし入力されていれば自機を移動
②敵移動
③キー入力判定(Shiftキー)
もし入力されていれば弾を発射
④弾移動
⑤あたり判定(当たったらGameFlag=1にする)
⑥エクセルに描写
⑦同期wait
Loop
⑧クリア描写
ここで、⑦に同期wait処理という単語が出ましたが、次の章を参照ください。
パソコンの性能によってエクセルの処理速度は違います。
なので、Aさんのパソコンだと敵が早く、Bさんのパソコンだと敵が遅い、なんてことがありえます。
それはゲームとして良くないので、同じ時間の「フレーム」を作ります。
スト2(ストリートファイター2)等でフレームという単語を聞いたことがあると思いますが、アレです。
やり方は簡単で、こう書きます。
StartTime =GetTickCount //初期値設定
WaitTime=10 //1フレームの時間(ミリ秒)
//(中略:ゲームのコーディング)
Do While GetTickCount - StartTime < WaitTime
Sleep 1
Loop
StartTime = GetTickCount
DoEvents
GetTickCountというのはパソコンの中の時計です(単位はミリ秒)。
一度、StartTimeに時間を覚えさせて、1フレーム時間が過ぎるまで永遠とsleepさせるのです。
そしてループを抜けたら、再びStartTimeに現在の時間を覚え直させます。
これで1フレーム毎に無限ループを1回実行することが繰り返されます。
ちなみに上記のように「DoEvent」と一言書いておくと、エクセルマクロ以外にもメモリが解放されるようになります。これでエクセル以外がフリーズしなくて済みます。
なお、GetTickCountとSleepを使うには、VBAの一番上に次の命令を書き込んでおく必要があります。
Declare Function GetTickCount Lib "kernel32" () As Long
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
セルを描写する前に、各処理にwait時間を設定する必要があります。
先ほどまでの例では、10ミリ秒を1フレームにしました。
なので、各処理にwait時間を設けておかないと、1秒間に100回処理されてしまいます。
例えば、1秒間 右キーを押していると、100回も自機が右に行く動作を行ってしまいます。
よって、「1回キー入力した後は、100ミリ秒後にキー再入力を許可する」というルールを設ける必要があります。
ということで、キー入力判定&自機移動のプログラムはこちらで完成になります。
JikiSpeed=100 //初期設定:100ミリ秒後にキー再入力を許可
If GetTickCount - JikiTime > JikiSpeed Then
If GetAsyncKeyState(39) <> 0 Then
If JikiC < 10 Then
JikiC = JikiC + 1
End If
End If
If GetAsyncKeyState(37) <> 0 Then
If JikiC > 1 Then
JikiC = JikiC - 1
End If
End If
JikiTime = GetTickCount
End If
ちなみに、今回はJikiSpeedを100ミリ秒にしましたが、ここを小さい値にすると短い間隔で移動許可が出るので、自機の移動スピードが上がることになります。
このように各処理にwait時間の設定が必要ですが、この値を調整することで自機・敵機・弾のスピードをコントロールできます。
自機・敵機・弾の移動を処理したら、プログラム上では自機・敵機・弾の行/列データが揃っています。
これをエクセルのセルに描写します。
エクエルは描写処理が苦手なため、ここを1セル1セル・・・と描写させるとうまく動きません。
よって、仮想的な10×10のセルに相当する変数MyF(行,列)を作り、リセット(すべて白紙に変換)した後、自機のシンボル「凸」、敵のシンボル「只」、弾のシンボル「^」を書き込みます。
そしてMyRange = MyFとしてゲーム範囲に一気にバーーーっと描写させます。
Dim MyRange As Range
Set MyRange = Range("B2:K12") //初期設定。MyRange=ゲームの描写範囲だと覚えさせる
'描写
For R = 1 To 10
For C = 1 To 10
'現記載をリセット
MyF(R, C) = ""
'自機描写
If C = JikiC Then
MyF(10, C) = ChrM(1, 1) //ChrM(1,1)には「凸」が入ってる
End If
'敵描写
If R = TekiPR Then
If C = TekiPC Then
MyF(R, C) = ChrM(2, 1) //ChrM(1,1)には「只」が入ってる
End If
End If
'弾描写
For i = 1 To TamaMax //Tama Maxは球の制限値(何発まで同時に打てるか)のデータが入ってる
If R = TamaR(i) Then
If C = TamaC(i) Then
MyF(R, C) = ChrM(3, 1) //ChrM(1,1)には「^」が入ってる
End If
End If
Next i
Next C
Next R
MyRange = MyF
長文お付き合い頂きありがとうございました。
以下が私が作ったサンプルコードです。
本件は「インプレスジャパン出版 Excel VBA アクションゲーム作成入門」の4章まで勉強した上で、私が作ったオリジナルコードです。
イマイチな部分もあろうと思いますが、そこは皆さまにて色々推敲くださいませ。
メイン画面です
B2~K12セルは以下のようにセットしています。
・セルの塗りつぶし:黒
・文字の色:白
・文字の配置:中央揃え
ボタンもつけてマクロを起動できるようにしていますが、なんかボタンはいまいちです。(やってみればわかりますが、なんか動きがイマイチで、VBAの再生ボタンのほうが上手く動きます)
DATAシートです
ここで自機の文字(凸)、敵の文字(只)、弾の文字(^)を簡単に変更できるようにしています。
また、WaitTime等もこのシートの値を参照させているので、VBAコードをいじることなく、敵の速さなどをチューニングできるようにしています。
Declare Function GetTickCount Lib "kernel32" () As Long
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Declare Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Long
Option Explicit
Sub Invedar1()
'フィールドをMyRangeとして登録
Dim MyRange As Range
Set MyRange = Range("B2:K12")
Dim MyF As Variant
MyF = MyRange
'DATAシートからデータ読み込み
Dim ChrM As Variant
ChrM = Worksheets("DATA").Range("C2:C4")
Dim WaitTime As Long
WaitTime = Worksheets("DATA").Range("C6").Value
Dim TekiSpeed As Long
TekiSpeed = Worksheets("DATA").Range("C7").Value
Dim TamaSpeed As Long
TamaSpeed = Worksheets("DATA").Range("C8").Value
Dim JikiSpeed As Long
JikiSpeed = Worksheets("DATA").Range("C9").Value
Dim TamaMax As Long
TamaMax = Worksheets("DATA").Range("C10").Value
'時間系
Dim StartTime As Long
StartTime = GetTickCount
Dim TekiTime As Long
TekiTime = GetTickCount
Dim TamaTime As Long
TekiTime = GetTickCount
Dim JikiTime As Long
JikiTime = GetTickCount
'ユーティリティ
Dim i As Long
i = 1
Dim j As Long
j = 1
Dim R As Long
R = 1
Dim C As Long
C = 1
Dim GameFlag As Long
GameFlag = 0 '0:ゲーム中、1:クリア、2:ゲームオーバ
'各種定数宣言
'自機系
Dim JikiC As Long
JikiC = 1 '自機の列位置
Dim JikiCMax As Long
JikiCMax = 10 '自機の右端限界(フィールドを調整できるようにするなら、ここをいじる)
Dim JikiCMin As Long
JikiCMin = 1
'敵系(敵はポジションとステータスと向きがあるので、TekiPとTekiSとTekiDを使う)
Dim TekiPR As Long
TekiPR = 2
Dim TekiPC As Long
TekiPC = 5
Dim TekiS As Long
TekiS = 0
Dim TekiD As Long
TekiD = 1 '敵の移動向き。1:左、2:右
'弾系
Dim TamaR(1 To 3) As Long
Dim TamaC(1 To 3) As Long
Dim TamaS(1 To 3) As Long
Do While GameFlag = 0
'自機移動
If GetTickCount - JikiTime > JikiSpeed Then
If GetAsyncKeyState(39) <> 0 Then
If JikiC < 10 Then
JikiC = JikiC + 1
End If
End If
If GetAsyncKeyState(37) <> 0 Then
If JikiC > 1 Then
JikiC = JikiC - 1
End If
End If
JikiTime = GetTickCount
End If
'敵移動
If GetTickCount - TekiTime > TekiSpeed Then
If TekiD = 1 Then
If TekiPC <= 1 Then
TekiD = 2
TekiPR = TekiPR + 1
Else
TekiPC = TekiPC - 1
End If
Else
If TekiPC >= 10 Then
TekiD = 1
TekiPR = TekiPR + 1
Else
TekiPC = TekiPC + 1
End If
End If
'ゲームフラグ(ゲームオーバ)
If TekiPR >= 10 Then
GameFlag = 2
End If
TekiTime = GetTickCount
End If
'弾系
'弾発射
If GetAsyncKeyState(32) <> 0 Then
TamaS(j) = 1
TamaR(j) = 10
TamaC(j) = JikiC
j = j + 1
If j >= TamaMax Then
j = 1
End If
End If
'弾移動
If GetTickCount - TamaTime > TamaSpeed Then
For i = 1 To TamaMax
If TamaS(i) = 1 Then
TamaR(i) = TamaR(i) - 1
'上端まで行ったら弾消滅
If TamaR(i) <= 0 Then
TamaS(i) = 0
End If
End If
Next i
TamaTime = GetTickCount
End If
'当たり判定
For i = 1 To TamaMax
If TamaS(i) = 1 Then
If TamaR(i) = TekiPR Then
If TamaC(i) = TekiPC Then
'当たった時
TekiS = 1 '敵消滅フラグ
TamaS(i) = 0 '弾消滅
'音を入れるならここ
'スコアを足すならここ
'ゲームフラグ(クリア)
If TekiS = 1 Then
GameFlag = 1
End If
End If
End If
End If
Next i
'描写
For R = 1 To 10
For C = 1 To 10
'現記載をリセット
MyF(R, C) = ""
'自機描写
If C = JikiC Then
MyF(10, C) = ChrM(1, 1)
End If
'敵描写
If R = TekiPR Then
If C = TekiPC Then
MyF(R, C) = ChrM(2, 1)
End If
End If
'弾描写
For i = 1 To TamaMax
If R = TamaR(i) Then
If C = TamaC(i) Then
MyF(R, C) = ChrM(3, 1)
End If
End If
Next i
Next C
Next R
MyRange = MyF
'恒常ループ
Do While GetTickCount - StartTime < WaitTime
Sleep 1
Loop
StartTime = GetTickCount
DoEvents
Loop
If GameFlag = 1 Then
MsgBox "Clear"
Else
MsgBox "GameOver"
End If
End Sub