13.BaseHelper共有ライブラリ

今回は「DxLib」にあります、「BaseHelper.h/cpp」についての話題です。
前回の後半に書いたように、このファイルのは汎用的な、そしてよく使われるクラスや関数群を記述しています。テンプレート関数になってるものも多々あり、そのあたりも含め説明したいと思います。

この記事は、
コミット「Dx12版のウインドウ作成」
から、
コミット「Dx11版ウインドウ作成と共有ファイルの追加」
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。

前項にも述べたように、この記事の時点の関数クラス等は該当コミット(後ろのほう)を調べていただくのがベストですが、今回紹介する内容は最新のコミットでも大きな変更はないのでそちらでも参照可能です。

BaseHelper.hの冒頭にある

    struct handle_closer { void operator()(HANDLE h) { if (h) CloseHandle(h); } };
    typedef public std::unique_ptr<void, handle_closer> ScopedHandle;
    inline HANDLE safe_handle(HANDLE h) { return (h == INVALID_HANDLE_VALUE) ? 0 : h; }

という3つの記述はBinaryReaderが使用する関数群です。WindowsのHANDLEやunique_ptrの解放に使います。

続いて、「BaseExceptionクラス」ですが、これはBaseCross独自の例外クラスです。コンストラクタに3つの文字列を引数に取り、エラーの原因をゲームプログラマに伝える役割があります。
ゲームプログラムにおけるエラー処理は、いろいろ議論の分かれるところです。abort()やexit()を使って強制終了させるのは論外としても、assertを使う方法、_wassertを使って例外処理風にする方法。いろいろありますが、BaseCrossは例外を送出する形にしています。ライブラリからthrowされた例外は、WinMain.cppでキャッチされメッセージボックスを出します。こうすることでいくらかきれいな終了ができますし、エラー原因も特定しやすいと思います。
ライブラリ中で送出する例外は以下のような形です。後ほど述べる「ObjectInterfaceクラス」の「GetThis()テンプレート関数」ですが

    template<typename T>
    std::shared_ptr<T> GetThis() {
        auto Ptr = dynamic_pointer_cast<T>(shared_from_this());
        if (Ptr) {
            return Ptr;
        }
        else {
            wstring str(L"thisを");
            str += Util::GetWSTypeName<T>();
            str += L"型にキャストできません";
            throw BaseException(
                str,
                L"if( ! dynamic_pointer_cast<T>(shared_from_this()) )",
                L"ObjectInterface::GetThis()"
                );
        }
        return nullptr;
    }

といった記述になってます。ここでは、そのオブジェクトのクラス階層に属さない型のGetThisは例外が送出されます。その際、取得しようとして失敗した型の型名を、Util::GetWSTypeName()関数で取得して、例外文字列に渡します。
このような形にすることで、ゲームプログラマが、いったいどのコードで発生したエラーかを特定しやすくなります。
バグのないコードはみんなの願いです。でも人間が書いてる以上、バグはつきものです、例外クラスによって、できるだけバグを特定しやすくしています。

「BaseExceptionクラス」の仲間に「BaseMBExceptionクラス」があります。同じ例外クラスですが、扱う文字列の型がこちらはstring型になります。
BaseCrossは、文字列の扱いとして、UNICODE(つまりwstringやwchar_t*)とMBString(stringやchar*)を完全に「別タイプ」として考えます。アプリケーション自体はUNICODE環境なので、例えばファイルのパスなどはwstringで行います。ですから、おおむねwstringで問題ないのですが、STLのエラーなどはstring型なので、両方使えるようにしておく必要があります。そのため例外のcatchはmbstringもメッセージボックス表示できるようにしています。
それで、この「BaseMBExceptionクラス」も、もし、何かmbstringでエラー表示する必要があったときのために、一応、用意しています。ですので、サービス的ではありますがstringのメッセージを送出できる例外クラスとお考えください。

続いて「ThrowIfFailed関数」です。この関数は、第一引数に「HRESULTを返す関数」を記述することで、失敗した場合、第2引数以降のメッセージをthrowすることができます。
XmlDoc.cppにはXML関連の関数が並んでいますが、「XmlDoc::SetText関数」で以下のような記述があります。

    void XmlDoc::SetText(const IXMLDOMNodePtr& Node, const CComBSTR& text){
        CheckXmlDoc();
        ThrowIfFailed(
            Node->put_text(text),
            L"テキストの設定に失敗しました",
            L"Node->put_text()",
            L"XmlDoc::SetText()"
            );
    }

ここで

            Node->put_text(text)

というのが「HRESULTを返す関数」です。ですので

    void XmlDoc::SetText(const IXMLDOMNodePtr& Node, const CComBSTR& text){
        CheckXmlDoc();
        HRESULT hr = Node->put_text(text);
        if (FAILED(hr)){
            throw BaseException(
            L"テキストの設定に失敗しました",
            L"Node->put_text()",
            L"XmlDoc::SetText()"
            );
        }
    }

と書くのと同じことになります。この違いを理解しましょう。細かいようですが、上のほうが、若干ですが助長的ではありません。
次の「BinaryReaderクラス」はDirectXのサンプルからBaseCrossに合わせる形で流用しています。
単純ではありますが、強力なバイナリファイル(メモリにも対応)なリーダーです。ポイントは、

    template<typename T> T const* 
    ReadArray(size_t elementCount)

です。これはT型のデータの配列を取得し、読み込んだ後、その分次のデータの先頭までシーク位置を移動させます。
このことは以下のように書けることになります。

    BinaryReader reader(L"test.dat");
    //test.datはabc型のデータ10個とそのあとxyz型のデータが5個並んでいるとする
    const abc* a = reader.ReadArray<abc>(10);
    const xtz* x = reader.ReadArray<xyz>(5);
    abc temp_a = a[3];
    xyz temp_x = x[2];

この処理でtemp_aはデータ内のabc型のデータの3番目、temp_xには、データ内のxyz型のデータの2番目が代入されます。

なお、この「BinaryReaderクラス」と次の「CsvFileクラス」そしてXmlDoc.h/cppに収められている「XmlDocReader」「XmlDocクラス」については、どこかで個別に別記事を立てて説明します(もう少しゲーム環境が整ってからですね)。

別記事を立てる予定ですが、「BinaryReaderクラス」の次は「CsvFileクラス」です。一応紹介はしておきます。
このクラスはcsvファイルを読み込んで、データを細かく抜き出したりできるクラスです。
csvファイルは大きく2つのデータ検証方法があります。まず、「CsvFile::GetCsvVec関数」は、1行1行の文字列の配列として扱う場合です。セルマップなどをcsvに準備した場合はこの関数が有効です。
もう一つは、「CsvFile::GetSelect関数(2つの多重定義があります)」と「CsvFile::GetSelect2関数」です。これらはcsvの中から条件に合った行をselectします。ちょうどデータベースSQL文の「SELECT句」に似た抽出を行います。
「CsvFile::GetSelect関数(1つ目)」は、csvの各行のどこかのフィールドにキーになる文字列を入れておいて、そのキーを検証して抽出します。
「CsvFile::GetSelect関数(2つ目)」は抽出条件を「関数へのポインタ」を使って行います。「CsvFile::GetSelect2関数」は、抽出条件を「ラムダ式」を使って行います。
「GetSelect系関数」は複数のタイプのオブジェクトの初期データを一つのcsvファイルで管理するのに役だちます。

次からの「MakeRangeErr関数」「SafeDelete関数」「SafeDeleteArr関数」「SafeRelease関数」「SefeDeletePointerList関数」「SafeDeletePointerVector関数」「SefeReleasePointerList関数」「SafeReleasePointerVector関数」「SharedToVoid関数」「VoidToShared関数」について、関数のコメントにある通りなので、ここでは触れません。各コメントを参照ください。
一つだけ触れますと、「SafeDelete関数」に代表されるポインタ削除は、スマートポインタを使うことでほとんど用なしとなります。BaseCrossの前身の「DxBase2015、DxBase2016」には、もっと前身があってそのライブラリでは生ポインタを使ってました。ですのでこれらの関数は非常に重要かつ重宝したのですが、現在ではスマートポインタは自動的に削除されるのであまり必要ありません。
ただ、1つ断っておきたいのは「C/C++そのものはガーベージコレクションは言語自体は持ってない」ということです。
スマートポインタはSTLの一部であり、本来は自分でnewしたポインタは自分でdeleteすべきです。ただその部分をSTLが代用してるってだけなので、知識的にはnewとdeleteは必要だと思います。このことはスマートポインタを使う上でも、その動きはどうなっているのか(どのタイミングでdeleteするのかなど)知っておいたほうが良い、ということです。

次の「Util」構造体です。この構造体はstatic呼び出しをされることを前提としたヘルパー系関数が入ってます。
主に「文字列変換」「エンコード変換」「intから文字列」「floatから文字列」などの文字列操作や、デバッグ文字列作成に使用できる関数群があります。
それと乱数作成「DivProbability関数」「RandZeroToOne関数」があります。「DivProbability関数」は「何分の1の確率」でtrueを返す関数です。「RandZeroToOne関数」は0から1.0fまでのfloat型を作り出す乱数です。発生したものに0が含まれるかどうかを選択できます。乱数についてはこの2つ関数があればある程度は応用がきくと思います。それ以外の乱数が必要な場合は自作する形になります。

「Util」構造体に含まれる関数とグローバル(といってもbasecrossネームスペース内ですが)の関数の違いは、単純に使用頻度の違いです。「SafeDelete関数」などは、以前は至るとこで使用されていました。ただその使用頻度も変わりつつあります。将来は少し変更が必要かもしれません。

次の「StepTimerクラス」はタイマーです。DirectXサンプルをBaseCrossに合わせる形で実装してます。よく使うのは、「前のターンからの経過時間を調べる、GetElapsedSeconds()関数」です。この関数は後ほど出てくる「Appクラス」でラッピングされ、「App::GetElapsedTime()関数」として多用されます。あとFPSを計算する「GetFramesPerSecond関数」も多用されます。

次に出てくる、「Point2Dクラス」「Rect2Dクラス」は単純な2次元ポイントと2次元矩形を表現します。テンプレートになっているのでintやflaot、doubleでも使用できます。
この二つのクラスの組み合わせで面白いのは、

void  Rect2D<T>::operator+=(Point2D<T> point)関数

でしょう。この関数は、例えば以下のように使います

    Rect2D<float> BaseRect(0,0,128.0f,128.0f);
    Point2D<float> MovePoint(320.0f,320.0f);
    BaseRect += MovePoint;

この処理により、(0,0,128.0f,128.0f)のサイズの矩形を、(320.0f,320.0f)に移動することが可能です。矩形のおおもとの大きさと、位置情報で、どこにでも矩形を移動できるのでアニメーションさせるのに便利に使えます。このほかにも

void  Rect2D<T>::PtInRect(Point2D<T> point)関数

も有用でしょう。あるポイントが矩形の中に含まれるかどうかを調べられます。

そして「ObjectInterfaceクラス」です。前項でも少し触れましたが、thisポインタをshared_ptrにする親クラスとOnCreate純粋仮想関数、そしてPreCreate仮想関数(こちらは「純粋」ではない)が含まれます。
「ObjectInterfaceクラス」はそのあとの「ObjectFactoryクラス」と併用して使うことで効果的に利用できます。
以下は2つのクラスを使ったオブジェクト構築の例です。コンソールアプリケーションです

#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <vector>
#include <memory>
#include <exception>

using namespace std;

class ObjectFactory;

//--------------------------------------------------------------------------------------
/// CreateとPreCreateを持ち、Thisスマートポインタがとれるインターフェイス
//--------------------------------------------------------------------------------------
class ObjectInterface : public std::enable_shared_from_this<ObjectInterface> {
    friend class ObjectFactory;
    //クリエイト済みかどうか
    //Create関数が呼び出し後にtrueになる
    bool m_Created{ false };
    void SetCreated(bool b) {
        m_Created = b;
    }
protected:
    //--------------------------------------------------------------------------------------
    /*!
    @brief プロテクトコンストラクタ
    */
    //--------------------------------------------------------------------------------------
    ObjectInterface(){}
    //--------------------------------------------------------------------------------------
    /*!
    @brief プロテクトデストラクタ
    */
    //--------------------------------------------------------------------------------------
    virtual ~ObjectInterface() {}
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief thisポインタ取得
    @tparam T   thisポインタの型
    @return thisポインタ(shared_ptr)
    */
    //--------------------------------------------------------------------------------------
    template<typename T>
    std::shared_ptr<T> GetThis() {
        auto Ptr = dynamic_pointer_cast<T>(shared_from_this());
        if (Ptr) {
            return Ptr;
        }
        else {
            throw std::exception("型変換できません");
        }
        return nullptr;
    }
    //--------------------------------------------------------------------------------------
    /*!
    @brief 前初期化を行う(仮想関数)<br />
    *thisポインタが必要なオブジェクトはこの関数を多重定義して、取得できる
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnPreCreate(){}
    //--------------------------------------------------------------------------------------
    /*!
    @brief 初期化を行う(仮想関数)<br />
    *thisポインタが必要なオブジェクトはこの関数を多重定義して、取得できる
    @return なし
    */
    //--------------------------------------------------------------------------------------
    virtual void OnCreate() = 0;
    //--------------------------------------------------------------------------------------
    /*!
    @brief クリエイト済みかどうか
    @return クリエイト済みならtrue
    */
    //--------------------------------------------------------------------------------------
    bool IsCreated() {
        return m_Created;
    }
private:
    //コピー禁止
    ObjectInterface(const ObjectInterface&) = delete;
    ObjectInterface& operator=(const ObjectInterface&) = delete;
    //ムーブ禁止
    ObjectInterface(const ObjectInterface&&) = delete;
    ObjectInterface& operator=(const ObjectInterface&&) = delete;
};

//--------------------------------------------------------------------------------------
/// Objectインターフェイスの派生クラスを構築する
//--------------------------------------------------------------------------------------
class ObjectFactory {
public:
    //--------------------------------------------------------------------------------------
    /*!
    @brief オブジェクト作成(static関数)
    @tparam T   作成する型
    @tparam Ts...   可変長パラメータ型
    @param[in]  params  可変長パラメータ
    @return 作成したオブジェクトのshared_ptr
    */
    //--------------------------------------------------------------------------------------
    template<typename T, typename... Ts>
    static shared_ptr<T> Create(Ts&&... params) {
        shared_ptr<T> Ptr = shared_ptr<T>(new T(params...));
        //仮想関数呼び出し
        Ptr->OnPreCreate();
        Ptr->OnCreate();
        Ptr->SetCreated(true);
        return Ptr;
}
    //--------------------------------------------------------------------------------------
    /*!
    @brief オブジェクト作成(static関数)<br/>
    コンストラクタではなく、OnCreateWithParam()関数にパラメータを渡す場合
    @tparam T   作成する型
    @tparam Ts...   可変長パラメータ型
    @param[in]  params  可変長パラメータ
    @return 作成したオブジェクトのshared_ptr
    */
    //--------------------------------------------------------------------------------------
    template<typename T, typename... Ts>
    static shared_ptr<T> CreateWithParam(Ts&&... params) {
        shared_ptr<T> Ptr = shared_ptr<T>(new T());
        //仮想関数呼び出し
        Ptr->OnPreCreate();
        Ptr->OnCreateWithParam(params...);
        Ptr->OnCreate();
        Ptr->SetCreated(true);
        return Ptr;
        }
    //以下略
};

//前方参照のため必要
class ParentObject;
//子供オブジェクトクラス
class ChildObject : public ObjectInterface{
    //持ち合いになるのを避けるためにweak_ptrにする
    weak_ptr<ParentObject> m_Parent;
public:
    ChildObject(const shared_ptr<ParentObject>& Par)
        :ObjectInterface(),
        m_Parent(Par)
    {

    }
    virtual ~ChildObject(){}
    virtual void OnCreate()override{
        //何かの処理
    }
    void Display(){
        cout << "子供オブジェクトクラス" << endl;
    }
};

//親オブジェクトクラス
class ParentObject : public ObjectInterface{
    shared_ptr<ChildObject> m_Child;
public:
    ParentObject()
        :ObjectInterface()
    {}
    virtual ~ParentObject(){}
    virtual void OnCreate()override{
        m_Child = ObjectFactory::Create<ChildObject>(GetThis<ParentObject>());
    }
    void Display(){
        cout << "親オブジェクトクラス" << endl;
        m_Child->Display();
    }

};


int _tmain(int argc, _TCHAR* argv[])
{
    try{
        //親オブジェクトクラスの作成
        auto Par = ObjectFactory::Create<ParentObject>();
        Par->Display();
    }
    catch (exception& e) {
        cout << e.what() << endl;
    }
    catch (...){
        cout << "未定義のエラー" << endl;
    }

    return 0;
}

この出力は以下のようになります。

親オブジェクトクラス
子供オブジェクトクラス

まず、C++11の文法的な部分で、コメントします。
ObjectFactoryクラスのCreate関数を見てください。

    template<typename T, typename... Ts>
    static shared_ptr<T> Create(Ts&&... params) {
        shared_ptr<T> Ptr = shared_ptr<T>(new T(params...));
        //仮想関数呼び出し
        Ptr->OnPreCreate();
        Ptr->OnCreate();
        Ptr->SetCreated(true);
        return Ptr;
    }

このような記述になっています。テンプレート引数が見慣れない形になってます。
これはC++11から実装された「可変長テンプレート」という機能です。つまり、テンプレート関数を作るときに、引数を可変長にできるのです。
これはどういうことかといいますと、「ParentObjectクラス」や「ChildObjectクラス」のコンストラクタ引数を自在に変更できるのです!
現に「ChildObjectクラス」はコンストラクタの第一引数に「ParentObjectクラスのshared_ptr」を持ちます。でもテンプレートは、「ObjectFactory::Create関数」を利用できるのです。
この機能は、C++11の機能であまり話題にはなってませんが、かなり使える機能だと思います。ぜひ皆さんも覚えておいてください。

また、「ObjectFactory::CreateWithParam関数」を見てください。これも可変長テンプレートになってますが、その可変長パラメータをコンストラクタに渡すのではなく「OnCreateWithParam関数」に渡しています。この効果はどのようなものかというと、例えば、オブジェクトが持つコンポーネントのようなデータを保持するクラスがあったとします。そのクラスを構築するのには、オブジェクトのthisポインタ(shared_ptr)を渡さなければなりません。しかし、コンストラクタ内ではthisポインタのshared_ptrを作成することはできないので、OnCreate関数等でコンポーネントは作成しなければなりません。しかし、ObjectFactoryには、例えばオブジェクトの初期位置などが渡されます。仕方がないので、コンストラクタからOnCreateまでの橋渡し機関だけ、初期位置を保持するメンバ変数が必要になります。
実はこのメンバ変数がもったいない(というかバグのもとになります)。
位置情報などは、オブジェクトが複数保持する必要がありません。これをコンポーネント等が保持するのであれば、オブジェクトが保持するものとダブル形になります。そういった変数のダブりを避けるために、ObjectFactoryに渡された初期値を、オブジェクトにOnCreateWiThParamという関数を持たせ、そこで渡すのです。その状態ではオブジェクトは既にコンストラクタが終わっているのでshared_ptrのthisポインタを作成することができます。ですので、コンポーネントを作成し、そこに初期値を渡すことができるようになるのです、結果として、一度しか使わないメンバ変数を作らずに済みます。
 かなりくどい説明になってしまいましたが、ようは、コンストラクタ、GetThis、OnCreateのそれぞれの特徴(というか仕様)に合わせて、より効率よくクリエイト作業できるような関数が、OnCrteateWithParamということになります。

もう一つ注意したいことがあります。shared_ptrの「持ち合い」の問題です。
オブジェクトの親子関係(継承関係ではありません)を形成するとき、親は子供のポインタを持ち、子供は親のポインタを持つ形になることが多いと思います。
こうした場合、両方shared_ptrで持つと、メモリーリークが起こります。そのため、親が持ってる子供のポインタは「shared_ptr」で、子供の持ってる親のポインタは「weak_ptr」で保持して起き、子供が親のポインタを必要としたときだけ

    auto shptr = m_Parent.lock();
    if (shptr){
        //ここでshptrを使用する。(shptrは親のshared_ptr)
    }

のように記述します。

基本的にBaseCrossはこのようにしてshared_ptrとweak_ptrを使い分けしてメモリリークのないようにしてますが、weak_ptrからshared_ptrへの返還は結構おっくうになります。
そんな場合は、ゲームオブジェクトなどのshared_ptrを一括で管理する「メモリプールクラス」を作成して、あらゆるオブジェクトのshared_ptrを一括管理します。オブジェクト同士のかかわりをどう表現するかが問題になりますが、その方法ですとweak_ptrを使わずにshared_ptrだけで管理できると思います(必要な時だけプールから借りればいいので)。
また、そのような設計だと、unique_ptrを使えるかもしれません(こちらは僕は実装した経験がないですが)。

続く「ShapeInterfaceクラス」は「ObjectInterface」ほど重要ではありません。純粋仮想関数として「OnUpdate関数」と「OnDraw関数」を持ちます。C++は多重継承ができますので、以下のようなオブジェクトを作成することができます。

class TestObject : public ObjectInterface,public ShapeInterface{
public:
    TestObject():
        ObjectInterface().
        ShapeInterface(){}
    virtual ~TestObject(){}
    virtual void OnCreate()override{
        //何かの初期化処理
    }
    virtual void OnUpdate()override{
        //更新処理
    }
    virtual void OnDraw()override{
        //描画処理
    }
}

とするとこのクラスを構築するほうは以下のように書けます。

void func(){
    auto PtrTestObject = ObjectFactory::Create<TestObject>();
    //OnCreate()は呼ぶ必要ない
    //更新処理
    PtrTestObject->OnUodate();
    //描画処理
    PtrTestObject->OnDraw();
}

この場合、「OnCreate関数」は呼ばなくてもよいのがわかりますね。(ObjectFactoryが呼び出します)。

続く「BaseResourceクラス」は「リソース」の親クラスです。「リソース」というのは、後から出てきますが、テクスチャ、メッシュ、オーディオなど、メモリを消費する、あるいは読み込みに時間のかかるオブジェクトを、使いまわししたり、再読み込みしなくても済むように保持しておくものです。
派生クラスを作って保持するのですが、ここでは親クラスのみ作っておきます。

最後に出てくるのが「ObjStateクラス」と「StateMachineクラス」です。
これは、ゲーム上のオブジェクトの「状態」を管理するのに利用します。この二つのクラスを合わせて(あるいはObjStateの派生クラスも含め)、「ステートマシンやステート」と呼びます。
ややもすると助長的になりがちなオブジェクトのUpdate処理を、ステートマシンを使うことで非常にすっきりと見やすいコードになります。サンプルや、あるいはこの後、ゲームに配置するオブジェクトの項で詳しく説明します。
もし、どういうものか早めに知りたい人は「DxBase2015」「DxBase2016」のサンプルを参照ください。

「DxBase2015」GitHubサイト

https://github.com/WiZFramework/DxBase2015

「DxBase2016」GitHubサイト

https://github.com/WiZFramework/DxBase2016

ではこの項終わります。次回は「Vector3」などの計算クラスの説明です。

 

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP