Edited at
DxLibDay 17

DXライブラリのリソースをスマートポインタで管理する


はじめに

この記事はDxLib Advent Calendar 2018 の17日目の記事です。

この記事では、DXライブラリでのリソース管理をC++のスマートポインタで行う方法について書いていきます。

コードをテストした環境は以下の通りです。


  • Dxライブラリ Ver3.19f

  • VC++ 15.9.3


DXライブラリでのリソース管理と問題点

DXライブラリでは、ゲーム内で使用する様々なリソースを管理する関数が提供されています。ここでのリソースとは、画像、サウンド、フォントなどです。

これらのリソースを読み込み込んだり開放したりする場合は、リソースに応じたLoad系関数、Delete系関数を呼び出します。

例えば、画像の場合はLoadGraph、DeleteGraphでそれぞれ画像の読み込みと解放を行うことができます。

作成したリソースへのアクセスは、Load系関数を呼び出したときの戻り値であるint型の「ハンドル」を使用します。

このようにDXライブラリではハンドルを使用してリソースを管理できるのですが、ハンドルをそのまま使用してリソース管理をする方法には以下の二つの問題点があります。


明示的な開放が必要

Load系関数を用いて確保したリソースは、不要になったらDelete系関数で開放しなければなりません。

万が一解放されるべきなのに解放を忘れているリソースがあった場合、ゲームを実行しているうちにメモリを徐々に圧迫していくことになってしまいます。

これは解放忘れなどのミスを起こさなければ防げる問題ですが、自動的に行わせることで解決できるのならそれに越したことはありません。生ポインタを使用する場合の問題と同じです。


リソースの種類をハンドルにより区別できない

DXライブラリのハンドルは、画像、音声などリソースの種類にかかわらず全てint型ですが、異なる種類のリソースではハンドラ間には互換性がありません。

ある種類のハンドルを別の種類のハンドル変数に格納してしまうことも可能で、何らかの原因でそのようなことをしてしまった場合、意図せずリソースの開放ができていないなどの問題が起こりえます。

例えば、画像ハンドラを入れるべき変数に音声ハンドラを入れてしまい、音声ハンドラを画像ハンドラのDelete関数で解放しようとしてしまうと、音声ハンドラの示す音声データは解放されることなくメモリ上に残ることになります。

これが繰り返し実行される処理の場合、本来解放されるべきなのに未開放の音声データが蓄積していき、最終的にはメモリを使い過ぎてゲームの停止などを引き起こす恐れがあります。

これについても気を付けてミスを起こさなければ防げる問題ですが、コンパイル時にエラーがでれば、万が一このようなミスをしてしまっても事前に気づくことができます。


スマートポインタを用いたリソースの管理

スマートポインタは、RAIIによりメモリの開放を自動的に行い、生ポインタの使用で問題になるリソース解放の問題、例外安全の問題を解消することができるポインタです。

上述の問題を解決するために、このスマートポインタを用いてDXライブラリのリソースを管理する方法を二つ考えてみます。


カスタムデリータを使用する方法

一つ目はスマートポインタのカスタムデリータを指定する機能を利用する方法です。カスタムデリータはスマートポインタのメモリ解放時の動作をカスタマイズするための機能で、この方法ではこの機能をDXライブラリのリソース管理に利用します。


コード

std::shared_ptrとstd::unique_ptrの作成時に、作成したリソースのDelete系関数を呼び出すようなデリータを指定しています。

これにより、メモリの解放時にDXライブラリでハンドルが示すリソースの開放も行っています。

この方法によりリソースの自動開放を行うことができますが、shared_ptrの場合はリソースの種類にかかわらず全てint型のshared_ptrを使用しているため、型によるリソース種類の区別はできません。


dxlib_handle.hpp

#include <string>

#include <memory>

#include "DxLib.h"

//shared_ptrを用いて管理されるハンドルを作成する関数のテンプレート
template<int(*Loader)(const std::string&), void(*Deleter)(int)>
std::shared_ptr<int> make_dxlib_shared_handle(const std::string& file_path) {
auto handle = (*Loader)(file_path);
if (handle > -1) {
auto shared_handle = std::shared_ptr<int>(new int{ handle }, [](int* ptr) {
if (*ptr > -1) {
(*Deleter)(*ptr);
}
delete ptr;
});
return shared_handle;
}
else {
return nullptr;
}
}

//unique_ptrのカスタムデリータとして用いる関数オブジェクト用クラス
template<void(*Deleter)(int)>
class dxlib_handle_deleter final {
public:
void operator()(int* ptr) {
if (*ptr > -1) {
(*Deleter)(*ptr);
delete ptr;
}
}
};

//unique_ptrを用いて管理されるハンドルを作成する関数のテンプレート
template<int(*Loader)(const std::string&), void(*Deleter)(int)>
std::unique_ptr<int, dxlib_handle_deleter<Deleter>> make_dxlib_unique_handle(const std::string& file_path) {
auto handle = (*Loader)(file_path);
if (handle > -1) {
return std::unique_ptr<int, dxlib_handle_deleter<Deleter>>(new int{ handle });
}
else {
return nullptr;
}
}

int DxLibGraphLoader(const std::string& file_path) { return LoadGraph(file_path.c_str()); }
void DxLibGraphDeleter(int handle) { DeleteGraph(handle); }
int DxLibFontLoader(const std::string& file_path) { return LoadFontDataToHandle(file_path.c_str()); }
void DxLibFontDeleter(int handle) { DeleteFontToHandle(handle); }
int DxLibSoundLoader(const std::string& file_path) { return LoadSoundMem(file_path.c_str()); }
void DxLibSoundDeleter(int handle) { DeleteSoundMem(handle); }

//共有画像ハンドルを作成する
std::shared_ptr<int> make_dxlib_shared_graph_handle(const std::string& file_path) {
return make_dxlib_shared_handle<&DxLibGraphLoader, &DxLibGraphDeleter>(file_path);
}

//共有フォントハンドルを作成する
std::shared_ptr<int> make_dxlib_shared_font_handle(const std::string& file_path) {
return make_dxlib_shared_handle<&DxLibFontLoader, &DxLibFontDeleter>(file_path);
}

//共有サウンドハンドルを作成する
std::shared_ptr<int> make_dxlib_shared_sound_handle(const std::string& file_path) {
return make_dxlib_shared_handle<&DxLibSoundLoader, &DxLibSoundDeleter>(file_path);
}

//非共有画像ハンドルを作成する
auto make_dxlib_unique_graph_handle(const std::string& file_path) {
return make_dxlib_unique_handle<&DxLibGraphLoader, &DxLibGraphDeleter>(file_path);
}

//非共有フォントハンドルを作成する
auto make_dxlib_unique_font_handle(const std::string& file_path) {
return make_dxlib_unique_handle<&DxLibFontLoader, &DxLibFontDeleter>(file_path);
}

//非共有サウンドハンドルを作成する
auto make_dxlib_unique_sound_handle(const std::string& file_path) {
return make_dxlib_unique_handle<&DxLibSoundLoader, &DxLibSoundDeleter>(file_path);
}



使い方

//uniqueな画像ハンドラの作成

auto unique_graph_handle = make_dxlib_unique_graph_handle("graph.png");
//sharedな画像ハンドラの作成
auto shared_graph_handle = make_dxlib_shared_graph_handle("graph.png");
//uniqueなサウンドハンドラの作成
auto unique_sound_handle = make_dxlib_unique_sound_handle("sound.wav");

//ハンドルを使用する場合は間接演算子を使用
DrawGraph(0, 0, *unique_graph_handle, TRUE);


ハンドルを保持するクラスを作成する方法

二つ目は、ハンドルを保持するクラスを作成しそのクラスをスマートポインタにより管理する方法です。

ポインタによる参照がなくなった際のリソース自動開放ができることに加えて、異なる種類のリソースを型で区別することができます。


コード


dxlib_resource.hpp

#include <string>

#include <memory>

#include "DxLib.h"

//DXライブラリのハンドラを保持するクラス
template<int(*Loader)(const std::string&), void(*Deleter)(int)>
class dxlib_resource final {
public:
explicit dxlib_resource(const std::string& resource_path) {
_dx_handle = (*Loader)(resource_path);
}

~dxlib_resource() {
if (_dx_handle > -1) {
(*Deleter)(_dx_handle);
}
}

int get_dx_handle()const {
return _dx_handle;
}
private:
int _dx_handle = -1;
};

int DxLibGraphLoader(const std::string& file_path) { return LoadGraph(file_path.c_str()); }
void DxLibGraphDeleter(int handle) { DeleteGraph(handle); }
int DxLibFontLoader(const std::string& file_path) { return LoadFontDataToHandle(file_path.c_str()); }
void DxLibFontDeleter(int handle) { DeleteFontToHandle(handle); }
int DxLibSoundLoader(const std::string& file_path) { return LoadSoundMem(file_path.c_str()); }
void DxLibSoundDeleter(int handle) { DeleteSoundMem(handle); }

//共有リソースを作成する
template<typename T>
std::shared_ptr<T> make_dxlib_shared_resource(const std::string& resource_path) {
return std::make_shared<T>(resource_path);
}

//非共有リソースを作成する
template<typename T>
std::unique_ptr<T> make_dxlib_unique_resource(const std::string& resource_path) {
return std::make_unique<T>(resource_path);
}

//DXライブラリの画像を保持する型
using dxlib_graph = dxlib_resource<&DxLibGraphLoader, &DxLibGraphDeleter >;
//DXライブラリのフォントを保持する型
using dxlib_font = dxlib_resource<&DxLibFontLoader, &DxLibFontDeleter>;
//DXライブラリのサウンドを保持する型
using dxlib_sound = dxlib_resource<&DxLibSoundLoader, &DxLibSoundDeleter>;



使い方

//uniqueな画像リソースの作成

auto unique_graph_resource = make_dxlib_unique_resource<dxlib_graph>("graph.png");
//sharedな画像リソースの作成
auto shared_graph_resource = make_dxlib_shared_resource<dxlib_graph>("graph.png");
//uniqueなサウンドリソースの作成
auto unique_sound_resource = make_dxlib_unique_resource<dxlib_sound>("sound.wav");

//ハンドルを使用する場合はget_dx_handle()を使用
DrawGraph(0, 0, unique_graph_resource->get_dx_handle(), TRUE);


最後に

DXライブラリでもC++っぽくRAIIでリソース管理ができたらいいんじゃないかと思ってこの記事を書いたのですが、もしかしたら実際の使用ケースには合わない場合があるかもしれません。

また、この記事に書いたコードは最低限の機能しか実装していないので、すでにメモリ上に読み込まれたデータからのリソース作成などには対応していません。参考にする場合は自分の使いやすいようにカスタマイズして使用してください。

しばらくUnityばかり使っていてDXライブラリの最近の情勢には詳しくないので、何か不足している点や誤っている点などがあれば、コメントで教えていただけると助かります。