低所得の青色申告個人事業主のブログ

開業から経験で得た情報をお知らせしていきたいと思います。

= グローバル ナビゲーション =

【第3回】VBA で単純な倉庫番ゲームにチャレンジ


[ スポンサー リンク ]

Amazonのアソシエイトとして、当メディアは適格販売により収入を得ています

その昔 倉庫番 というゲームがありました。

Excelで完全に再現することは不可能ですが、できるだけ再現してみたいと思います。

ゲームプログラミングを勉強したい方には、あまり参考にならないと思います。

しかし、VBAの学習に多少寄与できるよう無駄なことをしながら挑戦してみます。

途中で挫折するかもしれませんので、ご承知ください。

 

ゲーム開始時でのおおまかなアルゴリズム

前回記事の続きです。

テンプレートとなる画像(シェイプ)はすでに用意されている前提です。

画像ファイルを使用しない場合は、色を塗ったり値をセットしたりなどで読み替えてください。

ゲーム開始時の動きは、ギブアップして最初からやり直す動きと同じです。

初期表示を構築するには

  • 初期表示(設計図)情報を生成
  • それに基づき、マップ上に画像生成

となります。

ただ、やり直し時にはその時点の状態の画像が残っているので、画像(再)表示前に画像を消去する必要があります。

ゲーム開始時の状態も画像有無はわからないので、画像消去処理はゲーム開始時も共通です。

壁は移動することはないので消去しないという方法もありますが、ここでは壁も含めてすべて消去し、再構築することとします。

そこで、

  • 既存画像消去
  • 初期表示(設計図)情報生成(作成済)
  • マップ上に画像生成

という構成で作成します。

なお、コードは標準モジュールに記述します。

 

 

定数の用意

それぞれのコードの前に、用意している定数を紹介します。

'定数定義

Const C_INIT_PRSN_ROW   As Integer = 3      '初期作業者位置(行)
Const C_INIT_PRSN_COL   As Integer = 5      '初期作業者位置(列)

Const C_PIC_TYP_WALL    As String = "壁"    'テンプレートシェイプ「壁」
Const C_PIC_TYP_GOAL    As String = "的"    'テンプレートシェイプ「目的地」
Const C_PIC_TYP_CRATE   As String = "荷"    'テンプレートシェイプ「荷物(未)」
Const C_PIC_TYP_DONE    As String = "済"    'テンプレートシェイプ「荷物(済)」

Const C_SHAPE_NAME_PRFX As String = "Pict"  'マップ内シェイプ名接頭語
Const C_SHAPE_WHS_PRSN  As String = "WH-WORKER"    '作業者のシェイプ名

シェイプ名の接頭語」はのちほど説明します。

テンプレートとなる画像(シェイプ)は、ほかに作業者用も用意しています。

しかし、作業者テンプレートシェイプの定数を用意していません

「上」「右」「下」「左」はテンプレートのシェイプ名であると同時に、移動方向を示す引数(パラメータ)にも今後利用する予定です。

2つの意味をもつこれらの文言は、定数にせず直接コードに記述していきます。

 

 

マップ上に初期状態の画像を生成

画像を利用する方だけですが、64bitマシーンの場合モジュールレベル領域にこのコードを Copy&Paste してください。

32bitマシーンの場合少しコードが違うので、調べてください。

Option Explicit

'64bit PC
Private Declare PtrSafe _
    Sub Sleep Lib "kernel32.dll" (ByVal ms As LongPtr)
    

構造体情報を読み込んで、マップと同一シート上にあるテンプレートシェイプをコピーして該当のセルに貼り付けるという作業です。

生成するシェイプは、セルと同様特定できるように一意な名前を持たせます。

ネーミングルールとして「 シェイプ名の接頭語 + 2桁行番号 + 2桁列番号 」とします。

シェイプ名を生成するユーザー定義関数グローバルレベルで用意します。

'----------------------------------------------------------------------
'   g_fnCrtShapeName
'       行番号を2桁に列番号を2桁にして、一意なシェイプ名を作る
'       引数 = in_intRow    : 行番号
'       引数 = in_intCol    : 列番号
'       戻り値 = シェイプ名
'----------------------------------------------------------------------
Public Function g_fnCrtShapeName _
                (in_intRow As Integer, in_intCol As Integer) As String

    g_fnCrtShapeName = C_SHAPE_NAME_PRFX & _
                    Format(in_intRow, "00") & Format(in_intCol, "00")

End Function

"g_"とすることで、グローバルレベルであることを意味します。

引数(パラメータ)であることを意味する"p_"を使ってもいいのですが、参照渡しByRef)を利用して引数を戻り値にする場合と区別して"in_"としています。

ここからは画像を使わない方は適宜置き換えてください。

テンプレートをコピーしてセルの上に貼り付けるという作業は、初期表示だけでなくプレー中の動きでも利用するので、共通プロシージャとして用意します。

まず、いったんコードを作成します。

'----------------------------------------------------------------------
'   sbCreatePic
'       シェイプをコピーして生成する
'       引数 = in_objSheet      : シートオブジェクト
'       引数 = in_obgRange      : ピクチャを生成したいセル
'       引数 = in_strFromName   : コピー元シェイプ名
'       引数 = in_strToName     : 生成するシェイプにつける名前
'----------------------------------------------------------------------
Sub sbCreatePic(in_objSheet As Worksheet, in_objRange As Range, _
                    in_strFromName As String, in_strToName As String)
    
    in_objSheet.Shapes(in_strFromName).Copy
    in_objRange.PasteSpecial
    Application.CutCopyMode = False
    Selection.Name = in_strToName
    in_objRange.Select
    
    'コピー時や貼り付け時のエラー回避
    Sleep 100
    DoEvents
       
End Sub

引数に Range オブジェクトがありますが、こちらは範囲でなく1つのセルが入ります。

 Cells はオブジェクトがないため Range を使用します。

引数(パラメータ)に、セルとシェイプ名でなく行番号列番号にして、サブルーチン内でシェイプ名を生成する形でもいいです。

途中 SleepDoEvents とありますが、オブジェクトを Copy&Paste する際のエラー回避のおまじないのようなものなので、画像を使わない方は不要です。

私はあまり詳しくないのですが、クリップボードに保持する処理がExcel側でなくWindows側のために高速で処理が走るとエラーになるみたいです。

なお Sleep はVBA標準でないため、 API を呼び出すコードをモジュールレベル領域に記述しました。

ただ、残念ながら私の環境では ABnormal-END が頻発してしまい、エラーハンドリングしています。

最終的なコードはこちらです。

'----------------------------------------------------------------------
'   sbCreatePic
'       シェイプをコピーして生成する
'       引数 = in_objSheet      : シートオブジェクト
'       引数 = in_obgRange      : ピクチャを生成したいセル
'       引数 = in_strFromName   : コピー元シェイプ名
'       引数 = in_strToName     : 生成するシェイプにつける名前
'----------------------------------------------------------------------
Sub sbCreatePic(in_objSheet As Worksheet, in_objRange As Range, _
                    in_strFromName As String, in_strToName As String)
    
On Error GoTo WorkAround

    in_objSheet.Shapes(in_strFromName).Copy
    in_objRange.PasteSpecial
    Application.CutCopyMode = False
    Selection.Name = in_strToName
    in_objRange.Select
    
    'コピー時や貼り付け時のエラー回避
    Sleep 100
    DoEvents
    
    Exit Sub

WorkAround:

    MsgBox Err.Number & vbCrLf & Err.Description
    
    Resume
       
End Sub

単純にエラーを無視するだけというのは怖いので、念のためメッセージ表示はしています。

下準備ができたので、作業者以外のマップ画像を生成する sbReGene を作ります。

'----------------------------------------------------------------------
'   sbReGene
'       目的地、荷物を再生成する
'       引数 = in_objSheet : シートオブジェクト
'----------------------------------------------------------------------
Sub sbReGene(in_objSheet As Worksheet)

    Dim intRow As Integer
    Dim intCol As Integer
    
    Dim objRange As Range
    Dim strShapeID As String        'シェイプ名
    
    For intRow = C_MIN_ROW To C_MAX_ROW
        For intCol = C_MIN_COL To C_MAX_COL
            
            Set objRange = in_objSheet.Cells(intRow, intCol)
            strShapeID = g_fnCrtShapeName(intRow, intCol)
            
            '壁描画
            If m_typCellInfo(intRow, intCol).HasWALL Then
                Call sbCreatePic(in_objSheet, _
                                objRange, C_PIC_TYP_WALL, strShapeID)
            End If

            '目的地描画
            If m_typCellInfo(intRow, intCol).HasGOAL And _
                    Not m_typCellInfo(intRow, intCol).HasCRATE Then
                Call sbCreatePic(in_objSheet, _
                                objRange, C_PIC_TYP_GOAL, strShapeID)
            End If

            '荷物(未到)描画
            If m_typCellInfo(intRow, intCol).HasCRATE And _
                    Not m_typCellInfo(intRow, intCol).HasGOAL Then
                Call sbCreatePic(in_objSheet, _
                                objRange, C_PIC_TYP_CRATE, strShapeID)
            End If
'
'            '荷物(済)描画
'            If m_typCellInfo(intRow, intCol).HasCRATE And _
'                        m_typCellInfo(intRow, intCol).HasGOAL Then
'                 Call sbCreatePic(in_objSheet, _
'                                objRange, C_PIC_TYP_DONE, strShapeID)
'            End If
            
        Next intCol
    Next intRow
        
End Sub

作業者は1つだけなので、シェイプ名はセル位置と無関係に固定値にします。

ゲーム開始地点でもあるため、シェイプを生成するとともにそのセル位置を返します。(左向きにしていることには特に意味はありません。)

'----------------------------------------------------------------------
'   fnCreateWorker
'       ゲーム開始時の位置に作業者を生成する
'       引数 = in_objSheet : シートオブジェクト
'       戻り値 = 開始時点の作業者セル
'----------------------------------------------------------------------
Function fnCreateWorker(in_objSheet As Worksheet) As Range

    Dim objRange As Range
    
    '作業者描画
    Set objRange = in_objSheet.Cells(C_INIT_PRSN_ROW, C_INIT_PRSN_COL)
    Call sbCreatePic(in_objSheet, objRange, "左", C_SHAPE_WHS_PRSN)
    
    '戻り値セット
    Set fnCreateWorker = objRange
    
End Function

 

 

マップ内の既存画像を消去

前述のとおり、壁も含めたシェイプすべてを削除します。

シート上のすべてのシェイプを対象とする場合は For Each ループ を使います。

一般的にもブック内のすべてのシートに対する処理など、オブジェクトを対象とするループ処理でよく利用されます。

あまり使う機会はありませんが、Variant変数に対しても使えます。

シート上のシェイプの中でも、テンプレートは削除したくありません。

また、説明書きをテキストボックスで作成している場合も削除されては困ります。

そのため、シェイプ名の接頭語がついているものと作業者のシェイプのみを削除対象とします。

シェイプを削除する sbClearShapes のコードです。

'----------------------------------------------------------------------
'   sbClearShapes
'       マップ内の壁、目的地、荷物、作業者を消す
'       引数 = in_objSheet : シートオブジェクト
'----------------------------------------------------------------------
Sub sbClearShapes(in_objSheet As Worksheet)

    Dim objPic As Shape
    Dim intLen As Integer
    
    intLen = Len(C_SHAPE_NAME_PRFX)
    
    For Each objPic In in_objSheet.Shapes
        
        If objPic.Name = C_SHAPE_WHS_PRSN Or _
                    Left(objPic.Name, intLen) = C_SHAPE_NAME_PRFX Then

            objPic.Delete
        
        End If
        
    Next objPic
    
End Sub

 

 

ゲーム開始時とやり直し時の処理

材料はできあがりましたので、いったん Macro1 としてゲーム開始時とやり直し時の共通処理コードを作成します。

'----------------------------------------------------------------------
'   fnRefresh
'       構造体リセットや画面初期表示を行う
'       引数 = in_objSheet : シートオブジェクト
'       戻り値 = 作業者初期配置のセル(オブジェクト)
'----------------------------------------------------------------------
Sub Macro1()
        
    Dim objRange As Range
        
    '画面反映停止
'    Application.ScreenUpdating = False
        
    '画像クリア
    Call sbClearShapes(ActiveSheet)
    
    '構造体初期化
    Erase m_typCellInfo
    
    'ステップ数初期化
    in_objSheet.Cells(C_CNT_STEP_ROW, C_CNT_STEP_COL) = 0

    '各セルの情報を持つ構造体を生成
    Call sbStatusInfo
    
    '画像再描画
    Call sbReGene(ActiveSheet)
    
    '初期位置で倉庫作業者セルを定義
    Set objRange = fnCreateWorker(ActiveSheet)
    '作業者セルを選択
    objRange.Select
    
    '画面反映再開
'    Application.ScreenUpdating = True
    
End Sub

 

 

共通処理なので、あとで引数と戻り値をもつ ユーザー定義関数 に書き換えます。

Application.ScreenUpdatingコメントアウトしていますが、VBAではよく使います。

プログラム処理内容をエクセル画面に反映させないことより、処理時間を短縮できます

次回は、ギブアップによるやり直し機能やゲームの途中終了機能を実装していきます。

 

 


ご質問は下の 「コメントを書く」 からお願いします。

ExcelVBA全般に関わる質問で、比較的簡単にお答えできるものはできる限り回答したいと思います。

回答を公開でなくメールでやり取りしたいという場合は、その旨記載していただければ非公開で回答することも可能です。

有償での作業依頼は非公開にしますので、条件等をお知らせください。