4-7 CBmsProクラス

ここでは今まで説明したBMS解析処理をクラス化することにします。
クラス化しておけば他のゲームにも流用可能だし、
もしバグがあった場合は1つの修正で他のゲームも修正したことに出来ます。

ちなみにこういった使い方をここではライブラリと言っています。

■仕様

このクラスでは以下の要素に対応するように仕様を決めます。

①1つのクラスはBMSファイル1つを管理する
②1小節は9600カウントで処理する
③ヘッダ部と実データ部の読み込みを分ける
 
※曲選択時はヘッダ部のロードのみを使うと高速
④256以上のBPM、また小数値に対応
⑤256チャンネル分の情報を管理
⑥途中BPM変更可能
⑦小節ラインの管理
⑧ゲーム中に使用するフラグの追加

これにさらにゲーム中で使用する時間からBMSカウントを算出する処理を実装しますが、
ここではこの関数の詳細はまだ説明しません。

■CBmsProのダウンロード

CBmsProライブラリ v3.05 CBmsPro_v3.05.zip

サンプルプログラムなどで使用しているライブラリはこれより古い可能性があるため、
最新版は常にこちらからダウンロードするようにしてください。

■ヘッダファイル

ここからはヘッダファイルについて説明します。

まずBMS仕様に基づいた値を名前で参照出来るように、以下の用に定数として定義しています。

#define BMS_RESOLUTION      9600            // 1小節のカウント値
#define BMS_MAXBUFFER       (16*16)         // 00~FFまでのバッファ数

// BMSチャンネル定義
#define BMS_BACKMUSIC       0x01            // その位置にきたら、自動的に再生されるWAVを指定します
#define BMS_STRETCH         0x02            // その小節の長さを定義したデータ倍します(10進数、小数ともに使用可)
#define BMS_TEMPO           0x03            // 再生テンポ(BPM / 1分間の四分音符数)の途中変更(16進数)
#define BMS_BACKANIME       0x04            // バックグラウンドアニメーション機能
#define BMS_EXTENEDOBJ      0x05            // 落下してくるオブジェを別のキャラクターナンバーのものにすり替える機能
#define BMS_CHGPOORANI      0x06            // POORを出したときに表示される画像を変更
#define BMS_LAYERANIME      0x07            // Ch.04で指定したBGAの上にかぶせるBMPを指定できます
#define BMS_BPMINDEX        0x08            // BPMのインデックス指定(新)


ここで一番重要なのはBMS_RESOLUTION定数ですが、
これは1小節のカウント数で全ての演算はこれをベースに行われます。

もし解像度を上げたいとか下げたい場合、この値を変更するだけで対応出来るようにしたいので、
全てのプログラムは直接9600と記述するのではなく、必ずこの定数を使って計算を行うようにします。

なお、このサイト上でのBMSカウント値はLONG(Windowsでは32bit整数)型で定義しているため、
解像度をむやみに上げてしまうと計算結果がLONG型の範囲を超えてしまうことがあります。
この場合は64bit型を使うなどの対策が必要です。
※一般的なCPUは32bit整数の方が計算が速いので、なるべく32bitに収まるようにした方が効率がよくなります

次のBMS_MAXBUFFER定数は、単純にプログラムが扱える最大チャンネル数の定義です。
ここでは0xFFが最大値となるため16*16となっていますが、ZZ対応にする場合は36*36に変更して、
これに合わせてさらにプログラム側も修正します。

次のチャンネル定義とは、BMSのシステムコマンドに該当するチャンネル番号の定義です。
今後いきなり変更されることは無いと思いますが、一応定数として設定しておけばあとで値が変わった時に、
この定義を変更するだけで修正可能です。




次は構造体の定義です。

// BMSヘッダ情報
typedef struct _BMSHEADER {
    long        lPlayer;                    // プレイモード
    char        mGenre[256];                // データのジャンル
    char        mTitle[256];                // データのタイトル
    char        mArtist[256];               // データの製作者
    float       fBpm;                       // 初期テンポ(初期値は130)
    char        mMidifile[MAX_PATH];        // バックグラウンドで流すMIDIファイル
    long        lPlaylevel;                 // ゲームの難易度
    long        lRank;                      // 判定ランク
    long        lWavVol;                    // 音量を元の何%にするか
    long        lTotal;                     // ゲージの増量
    char        mStagePic[MAX_PATH];        // 曲開始字に表示する画像
    float       fBpmIndex[BMS_MAXBUFFER];   // テンポインデックス(初期値は120)

    long        lEndBar;                    // 終了小節
    long        lMaxCount;                  // 最大のカウント数
} BMSHEADER,*LPBMSHEADER;


// BMSデータ情報
typedef struct _BMSDATA {
    LONG        lTime;                      // このデータの開始位置(BMSカウント値)
    LONG        lData;                      // 鳴らすデータ(0x01~0xFF)
    float       fData;                      // 小数値データ(テンポ用)
    BOOL        bFlag;                      // アプリが使用出来る任意の変数(ここでは判定に利用)
} BMSDATA,*LPBMSDATA;


// 小節情報
typedef struct _BMSBAR {
    float       fScale;                     // この小節の長さ倍率
    LONG        lTime;                      // この小節の開始位置(BMSカウント値)
    LONG        lLength;                    // この小節の長さ(BMSカウント値)
} BMSBAR,*LPBMSBAR;

これは前に説明したものがそのまま記載されているので、内容については前の記事を参照してください。

なお構造体は普通はstructのみで定義しますが、ここではtypedefを使って定義を行っています。
これは構造体の定義と同時に、その構造体のポインタ型も一緒に定義する書き方です。
※マイクロソフトのサンプルでよく使われます

例えば以下のようにBMSHEADER構造体の実体を定義していたとします。

BMSHEADER mHead;

実体はどこかのメモリアドレスに確保されていることになりますが、この実体のメモリアドレスは以下のように参照出来ます。

LPBMSHEADER pHead = &mHead;

実体に'&'を付けるとそのアドレスを指すわけですが、これをその構造体のポインタ型として参照することが出来ます。
※ポインタがよく分からない場合は先に理解しておいてください

ちなみにポインタ型は、構造体の名前の先頭にLP(Long Pointerの略)が付いています。
※別にポインタ型を定義しなくともBMSHEADER *pHead;というように、アスタリスクを付けて記述することが出来ますが、
 オブジェクト指向的に言えば「BMSHEADER *」と記述が2つに分かれているより、
 「LPBMSHEADER」と1つの型で宣言している方がスマートに見えるという意味もあります。
 ただし、逆に'*'があればポインタだと分かりやすいという人もいるので、
 このあたりは自分で好きなほうを選べばいいと思います。



次は実際のクラス定義となります。

////////////////////////////////////////////////////////////////////////////////////
// BMSクラス
////////////////////////////////////////////////////////////////////////////////////
class CBmsPro {
protected:
    BMSHEADER   mBH;                                        // BMSヘッダ情報

    LPBMSDATA   pBmsData[BMS_MAXBUFFER];                    // 実データ
    int         iBmsData[BMS_MAXBUFFER];                    // 実データのそれぞれの数

    char        mWavFile[BMS_MAXBUFFER][MAX_PATH];          // WAVのファイル名
    char        mBmpFile[BMS_MAXBUFFER][MAX_PATH];          // BMPのファイル名

    BMSBAR      mBmsBar[1000+1];                            // 小節データ(999小節時に1000番目も小節バーとして使うため+1しておく)

    char        mLastError[1024];                           // エラーが発生した場合の文字列

public:
    CBmsPro();                                              // コンストラクタ
    virtual ~CBmsPro();                                     // デストラクタ

    // 基本メソッド
    BOOL Clear( void );                                     // データの初期化
    BOOL LoadHeader( const char *file );                    // BMSヘッダ情報だけを取り出す
    BOOL Load( const char *file );                          // BMSファイルのロード
    BOOL Save( const char *file );                          // BMSファイルにセーブ
    BOOL Sort( int ch );                                    // 指定チャンネルのデータを昇順に並び替える

    BOOL Restart( void );                                   // リスタート用にオブジェのフラグを元に戻す
    LONG GetCountFromTime( double sec );                    // 時間からBMSカウント値を計算

public:
    // 内部データのアクセス
    inline const char*          GetLastError( void )        { return mLastError; }              // 最後のエラー文字列

    // ゲーム必須メソッド
    inline LONG                 GetMaxCount( void )         { return mBH.lMaxCount; }           // ゲーム内の最大のカウント値
    inline int                  GetBarNum( void )           { return mBH.lEndBar+1; }           // 小節バーの個数(最後の小節も含むため+1する)
    inline const LPBMSBAR       GetBar( int num )           { return &mBmsBar[num]; }           // 小節バーのデータ
    inline int                  GetObjeNum( int ch )        { return iBmsData[ch]; }            // 指定チャネルのデータ数を返す
    inline const LPBMSDATA      GetObje( int ch,int num )   { return &pBmsData[ch][num]; }      // チャネルと配列番号でデータを取得する
    inline const LPBMSHEADER    GetHeader( void )           { return &mBH; }                    // ヘッダ情報を返す
    inline const char*          GetBmpFile( int num )       { return mBmpFile[num]; }           // 使用しているBMPファイル名
    inline const char*          GetWavFile( int num )       { return mWavFile[num]; }           // 使用しているWAVファイル名

private:
    // 非公開関数
    int  atoi1610( const char *s );                         // 16進数文字列を数値に変換
    BOOL itoa1036( int num,char *dst,int keta=-1 );         // 10進数を桁付きの36進数文字へ変換
    BOOL AddData( int ch,LONG cnt,LONG data );              // 1つのデータを追加(ソートはされない)
    int  GetCommand( const char *s );                       // コマンド番号を返す
    BOOL GetCommandString( const char *src,char *dst );     // パラメータ文字列を取得
    BOOL LoadBmsData( const char *file );                   // BMSデータの読み込み
    BOOL LineCompact( const char *src,char *dst );          // データを最適化して返す
};

まずは変数の定義になりますが、これは今までの説明にあった変数がそのままクラス内に移動したものです。

なおmLastErrorに関してはこのクラスに新たに追加したもので、BMSの解析に失敗した場合にその時のエラー文字列が入ります。
ただしこの変数は外部には公開されていないため、GetLastError()関数を使って間接的に呼び出します。


あとはそれぞれメソッドの定義になります。

この中で実際にゲームで使用するのはLoad()GetCountFromTime()関数のみです。
GetCountFromTime()関数はメインゲームのルーチンで説明するので省略しますが、
Load()関数は今までの説明にあった関数そのもので、引数にBMSファイル名を指定すればそれを解析し、
結果を内部変数に設定してから返ります。

そして各inline関数により、解析後の情報を外部から取り出して利用することが出来るようになっています。
※内部のメモリバッファを返す関数は、基本的に外部から書き換えられないようにconst指定となっています

最後にprivate関数ですが、これも基本的に以前説明した関数そのままです。
これらの関数はクラス内でしか使用しないため、間違って外部から呼び出して不正な処理とならないように非公開としています。


ちなみにこのクラスを使って曲選択処理がしたいなら、Load()関数ではなくLoadHeader()関数を使用すると良いでしょう。
以前の説明にもあるとおり、LoadHeader()はヘッダ部しか解析しないため処理が軽くなります。
ヘッダの解析が終わったらGetHeader()にて解析したデータを取得し、自前の曲選択リストなどに登録します。

CBmsProを複数作ることも可能ですが、このクラス内のサウンドや画像ファイル名を保存する変数の容量が意外と多いのと、
曲選択時にはそもそも不要な情報なので、最低でもBMSHEADERのリストだけあれば事足りるのではと思われます。
※C++ならstd::vector<BMSHEADER> head_list;みたいにするなど



■ソースファイル

ここではクラスのソースコード部分の説明をしています。

ただし、今まで説明した関数などは単にクラス化しただけなのでここでは特に解説は行いません。
ここに無い関数は前回までのページを確認してください。

まずはこのクラス内で使用している定義です。

#define SAFE_FREE(x)        { if(x) { free(x); x=NULL; } }

既に知っているかもしれませんが、これはxというポインタにメモリが確保されていた場合、
開放してそのポインタをNULLにするというマクロです。

例えばこれをあるポインタ変数に対して2回呼び出したとしたら、最初の1回目で開放されNULLになり、
2回目では既にNULLなので何もしないプログラムとなります。
これで間違って2回開放してしまうようなバグを回避することが出来ます。


次にコンストラクタとデストラクタ、それと初期化用の関数の中身です。

////////////////////////////////////////////
// コンストラクタ
////////////////////////////////////////////
CBmsPro::CBmsPro( void )
{
    ZeroMemory( &mBH,sizeof(mBH) );
    ZeroMemory( &pBmsData,sizeof(pBmsData) );
    ZeroMemory( &iBmsData,sizeof(iBmsData) );
    ZeroMemory( &mWavFile,sizeof(mWavFile) );
    ZeroMemory( &mBmpFile,sizeof(mBmpFile) );
    ZeroMemory( &mBmsBar,sizeof(mBmsBar) );
    ZeroMemory( &mLastError,sizeof(mLastError) );

    // デフォルト値セット
    Clear();
}

////////////////////////////////////////////
// デストラクタ
////////////////////////////////////////////
CBmsPro::~CBmsPro()
{
    Clear();
}

////////////////////////////////////////////
// データの初期化
////////////////////////////////////////////
BOOL CBmsPro::Clear( void )
{
    int i;

    // ヘッダ初期化
    ZeroMemory( &mBH,sizeof(mBH) );
    mBH.lPlayer = 1;
    mBH.fBpm    = 130;
    for( i=0;i<BMS_MAXBUFFER;i++ ) {
        mBH.fBpmIndex[i] = 120.0f;
    }

    // 実データ初期化
    for( i=0;i<BMS_MAXBUFFER;i++ ) {
        SAFE_FREE( pBmsData[i] );       // BMSデータ領域をクリア
        iBmsData[i] = 0;                // データの数をクリア
    }

    // ファイル名
    ZeroMemory( &mWavFile,sizeof(mWavFile) );
    ZeroMemory( &mBmpFile,sizeof(mBmpFile) );

    // 小節の長さを1.0で初期化
    ZeroMemory( &mBmsBar,sizeof(mBmsBar) );
    for( int i=0;i<1001;i++ ) {
        mBmsBar[i].fScale = 1.0f;
    }

    return TRUE;
}

コンストラクタではクラス内の全ての変数を初期化しており、その後Clear()関数を呼び出しています。

Clear()関数では先ほど定義したSAFE_FREE()マクロを使ってメモリの開放を行っていますが、
一度コンストラクタで0クリアを行っているため、この時のSAFE_FREE()は実質何も行われません。

Clear()関数はプログラム側で明示的に呼び出すことも想定しているため、
どのような時でも確実に内部の変数を初期化できるようになっています。

同じようにデストラクタでClear()関数を呼び出していますが、
これでクラスの実体が無くなる時も確実に内部のメモリを開放出来ます。



次はBMSのロード関数です。

////////////////////////////////////////////
// データロード
////////////////////////////////////////////
BOOL CBmsPro::Load( const char *file )
{
    // ヘッダ&小節倍率の読み込み
    if( !LoadHeader(file) ) {
        DEBUG( "ヘッダ読み取りエラー\n" );
        return FALSE;
    }
    DEBUG( "HEADER OK\n" );

    // 実データの読み込み
    if( !LoadBmsData(file) ) {
        DEBUG( "データ読み込みエラー\n" );
        return FALSE;
    }
    DEBUG( "DATA OK\n" );

    return TRUE;
}

ここでは単純にヘッダと実データ両方を解析する処理になっています。
なんとこのメソッド1つを実行するだけで、BMSのロードが完了してしまうという手軽さです。



それ以外の関数は以下のような処理を行っています。

Save() 解析後のデータをBMSファイルとして書き出します。
※暫定的な機能なので全ての情報が正しく出力出来るとは限りません
Restart() ゲーム用に用意した各オブジェの変数(bFlag)をTRUEに再セットします。
これはゲームをもう一度始めから遊べるようにするためのものです。
GetCountFromTime() 時間からBMSカウント値を計算します。
これはメインゲームルーチンで詳しく説明しています。
LineCompact() 書き出し時に実データ部を最適化して返します。
例えば「0100000002000000」だった場合は「0102」と無駄な部分が省略されます。
※アルゴリズムはソースコードを参照



これでBMSファイルを扱う準備が整いました。

あとはこのクラスを使ってゲームを作りますが、その前に今はまだBMSファイルを解析しただけであって、
当然これだけではゲームは出来ないので、次は一番大事な画面の描画方法について説明します。