1. mrdagon

    Posted

    mrdagon
Changes in title
+[ゲーム制作]TiledをGUIエディタにしてソースコード生成システムをさくっと構築
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,381 @@
+#はじめに
+
+ 最近はゲームエンジンを活用する事例が増えていますね。
+ ゲームエンジンを使うと3Dが楽とかプログラミングの量が減るとか色々利点もあるんですが、痒い所に手が届かないとか、勉強するために直接コード書きたいとか特定のプログラミング言語がどうしても使いたい、ロゴ消すのに金がかかるとか、独自仕様が多い2Dゲームの個人開発なのでゲームエンジンの恩恵が無いとか、使いたく無い/使う必要が無い人もたくさんいるわけです。
+
+ 全部コード書いてなんとかする場合に面倒なのがGUIのデザインです。座標等を直接コードに書くCUIのスタイルでGUIのレイアウトをするのは苦行すぎます。
+ とはいえゲームエンジン的な物を一から作り出すと、もはや何をやっているのか分からなくなっていきます。
+
+ そんなこんなでTiledをGUIエディタとして活用して短期間で必要十分なソースコード自動生成環境を作成する方法を説明します。
+ まずTiledの仕様をざくっと解説した後、データの解析、コードに変換するシステムの設計を解説します。
+ サンプルはC++で書きますが、設計の話が大半なので別のプログラミング言語でも同じような事が出来ると思います。
+
+ 一応、[前回の続き](http://qiita.com/mrdagon/items/58b814e25e0cc2950350)です。(※読まくてもよい)
+
+ プログラムの設計の話が中心になるので、Tiledの細かい仕様とかコードの詳細は説明しません。
+#Tiledとは?
+ マップチップをタイルに貼っていくタイプのマルチプラットフォームのマップエディタです。
+ レイヤー機能やRedo/Undo他、必要な機能がひと通り揃っている他にオブジェクトを配置する機能や、別ファイル形式でエクスポートする機能もあります。
+ カスタムプロパティ機能を使う事で色々出来るのが特徴です。
+実際にいくつかTiledを使っているゲームエンジンなどもあるようです。
+
+ オープンソースソフトウェアなのでその機になれば改変も可能ですが、ゲーム制作に戻ってこれなくなるリスクがあるので多分やめておいた方が良いかもしれません。
+
+詳細は以下で確認して下さい。
+[Tiled公式](http://www.mapeditor.org/)
+[窓の社レビュー記事](http://www.forest.impress.co.jp/docs/review/20130321_591792.html)
+
+##Tiledのオブジェクト
+ Tiledのオブジェクトは四角形、楕円形、ポリゴン、ポリライン、タイルの5種類あります。
+とりあえずレイヤーとタイルセットと四角形とタイルだけ使えれば十分だし、面倒なので3つだけ説明します。
+
+ レイヤーはいわゆる普通のレイヤー機能で、オブジェクトは各レイヤーに配置していく形になります。
+『名前、表示の有無、透過度、色(RGBA)、描画順序、カスタムプロパティ』のパラメータを持ちます。
+
+ タイルは貼り付ける画像で、タイルセットはその集合です。
+ タイルセットはマップチップ画像を分割して一括登録するか、大きさの異なる画像を一つずつ登録します。
+タイルセットは『名前、描画オフセット(X,Y),カスタムプロパティ』のパラメータを持ちます。
+各タイルは『GID(非表示)、ファイルパス(非表示)、大きさ(非表示)、カスタムプロパティ』のパラメータを持ちます。
+
+ 四角形オブジェクトは任意の矩形を置けます。色は配置したレイヤーの色になります。
+『名前、種類、場所(X,Y)、大きさ(幅、高さ)、回転、カスタムプロパティ』のパラメータを持ちます。
+
+ タイルオブジェクトは登録した画像を配置出来ます。枠の部分がレイヤーの色になります。
+『GID(非表示)、名前、種類、場所(X,Y)、大きさ(幅、高さ)、回転、横反転、縦反転、カスタムプロパティ』のパラメータを持ちます、大きさは反映はされません。
+
+##カスタムプロパティ
+ 各オブジェクト等はデフォルトのパラメータの他にカスタムプロパティを任意の数設定出来ます。
+カスタムプロパティは、プロパティ名と文字列を入力します。
+
+ この機能を使う事で、『この枠はボタンとして扱うので、ボタン上に表示する文字とフォント名のパラメータを追加する』とか『このタイルはSTGの敵ユニットなので、どの方向から出現するかのパラメータを追加する』とか『このタイルは4方向3パターンのアニメを持つので、ランダム移動のパターンとか最初の向きとかのパラメータを追加する』等、色々柔軟に利用する事が出来ます。
+
+##どのように運用するか?
+ 細かい部分どういうコードを生成したいかで変わるので基本的なコツだけ説明。
+Tiledの使い方は実際にダウンロードして試した方が理解は早いと思います。
+
+ マップチップレイヤーは不要なら削除する。
+
+ マップのサイズは1x1にして、タイルの大きさを必要な大きさにすると、タイルの目が邪魔にならない。
+
+ オブジェクトの種類は見分けを付けるため、種類ごとに別のレイヤーに置く。
+例えば、ボタンと枠と文字が必要ならそれぞれ別のレイヤーにすると良い。
+
+ カスタムパラメータを個別に追加するのは面倒なのでテンプレオブジェクトを範囲外に作っておき、右クリックメニューの『1つのオブジェクトを複製する』で追加すると楽。XML読み込み時特定の名前のデータは除外するとかすればOK。
+
+ 複数のオブジェクトやタイルを選択した状態だとカスタムパラメータもまとめて追加出来る。
+
+ 編集していくと以下の画像のような感じになると思います。
+ ![Tiled.jpg](https://qiita-image-store.s3.amazonaws.com/0/44067/c25d58b7-37a0-5c07-29b5-a4793f9dcb1b.jpeg)
+
+使用した画像の配布元
+[ぴぽや](http://piposozai.blog76.fc2.com)
+
+ 文字列オブジェクトが無い、カスタムプロパティの追加が仕様によってはやや面倒、座標値を小数点に出来る、画像の大きさが反映されないとかありますが、まぁ大丈夫かと。
+
+#データ解析
+ Tiledはデータを色んな形式でエクスポート出来るので、お好きな形式のデータを解析すれば良いと思います。luaのファイルとか出してプログラムレスのエンジンにしてどうのこうのとかも出来るようです。
+ 今回はデフォルトのデータ形式(.tmx)を使う事にします、tmxの内部形式はxmlなので簡単に解析出来ますね。
+
+ 以下解説。
+
+##共通部分
+ xmlのバージョンや全体の大きさ等が読み取れます。
+特に言う事も無い
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="right-down" width="1" height="1" tilewidth="640" tileheight="480" backgroundcolor="#7f7f7f" nextobjectid="81">
+```
+
+##tileset
+
+ タイルセットは、名前、最大のタイルの大きさ、カスタムプロパティ。
+タイルはid毎に大きさ、ファイルパス、カスタムプロパティになります。
+ ファイルパスはtmxファイルの位置との相対パスになるようです。
+
+```xml
+ <tileset firstgid="1" name="Image" tilewidth="20" tileheight="20">
+ <properties>
+ <property name="Test" value="ほげほげ"/>
+ </properties>
+ <tile id="0">
+ <image width="20" height="20" source="data/icon000.png"/>
+ </tile>
+ <tile id="1">
+ <image width="20" height="20" source="data/icon001.png"/>
+ </tile>
+ <tile id="2">
+ <properties>
+ <property name="Name" value="剣"/>
+ </properties>
+ <image width="20" height="20" source="data/icon002.png"/>
+ </tile>
+ </tileset>
+```
+
+##四角形のレイヤー
+ 四角形のレイヤーはこんな感じ。
+ objectgroupタグがレイヤー情報。見たら分かる感じ。
+ 
+objectタグが各オブジェクトの情報になっています。
+idはTiled用のパラメータなので無視しましょう。種類、回転、表示、デフォルト値のままなら出力されないようです。
+
+```xml
+ <objectgroup color="#ffffff" name="Text" opacity="0.8">
+ <object id="26" name="文字テンプレ" x="-91.5" y="16" width="69" height="28">
+ <properties>
+ <property name="text" value="テンプレ"/>
+ </properties>
+ </object>
+ <object id="54" name="タイトル" type="Font[A]" x="9" y="14" width="272.552" height="87.1414">
+ <properties>
+ <property name="text" value="進捗どうですか?"/>
+ </properties>
+ </object>
+ </objectgroup>
+```
+
+##タイルのレイヤー
+ 四角形のレイヤーと大体同じですね。gidはタイルセットの情報とリンクしています。
+
+```xml
+ </objectgroup>
+ <objectgroup color="#ff0000" name="Image" opacity="0.9">
+ <object id="58" name="薬瓶" gid="1" x="398" y="166" width="1" height="1"/>
+ <object id="64" name="剣" gid="3" x="282.873" y="155.644" width="1" height="1" rotation="42.3915">
+ <properties>
+ <property name="向き" value="上"/>
+ </properties>
+ </object>
+ </objectgroup>
+```
+
+##解析まとめ
+ 説明が面倒なので内部形式に変換するコードは省きますが、テキストとして読み込むにしてもxmlを解析するライブラリを使うにしても問題なさそうですね。
+
+#自動生成システムの設計
+ データからコードを生成するステップとしては。
+1.エディタで編集する
+2.tmxデータを内部形式に変換する
+3.コードを出力する
+ の手順になります。
+
+達成したい要件としては
+『実行中にエディタで座標等を調整すると反映される』
+『コードを手作業で編集した後にエディタで座標等を修正した場合をいい感じにする』
+『出力するコードのテンプレートも簡単に修正出来るようにする』
+『リリースビルド時はパフォーマンスも考慮する、デバッグビルドは気にしない』
+『オブジェクトの種類を後から追加しやすくする、仕様変更に強くする』
+『エディタの編集作業は出来るだけ楽にする』
+これぐらいでしょうか?
+
+『オブジェクトが処理を持つ?持たない?』
+ オブジェクト自体にDraw関数やらUpdate関数やら持たせる方針でやれば、プログラムレスのゲームエンジンも作れると思いますが、コードを直接編集して込み入った仕様に対応するのが面倒になります。
+
+ この辺りは一長一短だと思いますが、今回はコード生成型として説明します。
+ コード生成方式が理解出来るなら、プログラムレスの2Dゲームエンジン風にする方法も大体思いつくと思うので、別記事で解説とかもしません。
+
+『手作業でコードを変更するとtmxデータに反映されるようにする』
+これは面倒なのであきらめます。
+
+##内部形式をどうするか?
+ オブジェクトの種類毎に共通のインターフェースを持たせる事で仕様変更に強くします。
+また、どのようなコードを生成するかもオブジェクトが持つようにします。
+ 名前と座標と大きさはどのオブジェクトも持つので、インターフェースに入れても良さそうです。
+以下のような派生元のクラスを定義しておきます。
+
+```cpp
+ class IGUI
+ {
+ public:
+ IGUI() = default;
+
+ IGUI(const char* name):
+ name(name)
+ {}
+
+ std::string name;
+
+ virtual void DefineCode(File& 書込先) = 0;
+ virtual void UpdateCode(File& 書込先){};
+ virtual void DrawCode(File& 書込先){};
+ virtual void InitCode(File& 書込先){};
+ virtual void FinalCode(File& 書込先){};
+ };
+```
+
+ IGUIを継承したクラスにどういうコードを生成するか書きます。
+例えばこんな感じ。
+
+```cpp
+class GUI_Text : public IGUI
+{
+public:
+ std::string fontName;//type
+ Rect rect;
+
+ std::string text;
+
+ GUI_Text() = default;
+
+ GUI_Text(const char* name, const char* type, double x, double y, double w, double h, const char* text):
+ IGUI(name),
+ fontName(type),
+ rect(x,y,w,h),
+ text(text)
+ {}
+
+ void DefineCode(File& 書込先) override
+ {
+ 書込先.AddLine({ " GUI_Text ", name, " = {\"", name, "\",\"",frameName, "\",", rect.GetX(), ",", rect.GetY(), ",", rect.GetW(), ",",rect.GetH(),",\"", text, "\"};" });
+ }
+
+ void DrawCode(File& 書込先) override
+ {
+ 書込先.AddLine({ type , ".Draw(" , name , ",\"" , text , "\");" } )
+ }
+}
+```
+
+ 宣言部分で値を代入するようにしておけば、動的に読み込まなくても良くなります。
+Update時とかInit時の処理が無いなら無視しておけば良い感じです。
+
+##コードを生成する
+ あとはコードを生成するだけです。こんなんでコードが生成出来るんですね。
+
+```cpp
+void TMXtoCode(const char* className ,const char* tmxFile, const char* tempFile, const char* outputFile)
+{
+ std::vector<std::shared_ptr<IGUI>> guiS = TmxToIGUI(tmxFile);
+
+ File file(tempFile, FileMode::Read);
+ std::vector<std::string> strS = file.GetLineS();
+ file.Close();
+
+ File file2(outputFile, FileMode::Write);
+
+ bool isWrite = true;
+
+ for (auto &it : strS)
+ {
+ if (it.find("CLASSNAME") != std::string::npos)
+ {
+ int num = it.find("CLASSNAME");
+ it.erase(num, 9);
+ it.insert(num, className );
+ file2.AddLine(it);
+ }
+ else if (it.find("@Define") != std::string::npos)
+ {
+ for (auto &it : guiS){ it->DefineCode(file2); }
+ }
+ else if (it.find("@Init") != std::string::npos)
+ {
+ for (auto &it : guiS){ it->InitCode(file2); }
+ }
+ else if (it.find("@Final") != std::string::npos)
+ {
+ for (auto &it : guiS){ it->FinalCode(file2); }
+ }
+ else if (it.find("@Update") != std::string::npos)
+ {
+ for (auto &it : guiS){ it->UpdateCode(file2); }
+ }
+ else if (it.find("@Draw") != std::string::npos)
+ {
+ for (auto &it : guiS){ it->DrawCode(file2); }
+ }
+ else if (it.find("@Load") != std::string::npos)
+ {
+ int index = 0;
+ for (auto &it : guiS)
+ {
+ file2.AddLine({ "\t\t\t", it->name, " = *dynamic_cast<", typeid(*it).name(), "*>(guiData.dataS[", index, "].get());" });
+ ++index;
+ }
+ }
+
+ if (isWrite)
+ {
+ file2.AddLine(it);
+ }
+
+ //これに囲まれた区間は無視する
+ if (it.find("@End") != std::string::npos)
+ {
+ isWrite = true;
+ file2.AddLine(it);
+ }
+ else if (it.find("@Start") != std::string::npos)
+ {
+ isWrite = false;
+ }
+ }
+```
+ TmxToIGUI関数はxmlを解析して、その後コードを生成する感じです。
+面倒なのでxmlの変換は解説しません、データの解析をすると言ったな?あれは嘘だ。
+
+ 最初の引数にtmxファイル名、次に出力ファイルのテンプレートとなるファイル、最後に出力するファイル名を入れましょう。
+
+ tempFileはこんな感じにしておけば良いですね。
+
+```cpp
+#pragma once
+#include <MyLibrary.h>
+
+namespace MyName
+{
+ class CLASSNAME : public IScene
+ {
+ public:
+ //@Define @Start
+ //@End
+
+ CLASSNAME()
+ {
+ LoadGUI();
+ Init();
+ }
+
+ //初期化
+ void Init() override
+ {
+ //@Init @Start
+ //@End
+ }
+
+ //終了時
+ void Final() override
+ {
+ //@Final @Start
+ //@End
+ }
+
+ //更新
+ void Update() override
+ {
+ //@Update @Start
+ //@End
+ }
+
+ //描画
+ void Draw() override
+ {
+ //@Draw @Start
+ //@End
+ }
+ void LoadGUI() override
+ {
+ //@Load @Start
+ //@End
+ }
+ };
+}
+```
+
+ tempFileとoutputFileには同じファイルを指定すれば更新も可能、@Startと@Endを調整する事でここは更新しないみたいな仕様を実現しています。
+
+ 要件は全部解決したと思います。
+
+#まとめ
+ ゲームタイトル毎に汎用性の無い使い捨ての自動生成システムを作るぐらい2,3日あれば余裕で出来るんや!