Help us understand the problem. What is going on with this article?

[ゲーム制作]TiledをGUIエディタにしてソースコード生成システムをさくっと構築

More than 5 years have passed since last update.

はじめに

 最近はゲームエンジンを活用する事例が増えていますね。
 ゲームエンジンを使うと3Dが楽とかプログラミングの量が減るとか色々利点もあるんですが、痒い所に手が届かないとか、勉強するために直接コード書きたいとか、特定のプログラミング言語がどうしても使いたいとか、ロゴ消すのに金がかかるとか、独自仕様が多い2Dゲームの個人開発なのでゲームエンジンの恩恵が無いとか、使いたく無い/使う必要が無い人もたくさんいるわけです。

 全部コード書いてなんとかする場合に面倒なのがGUIのデザインです。座標等を直接コードに書くCUIのスタイルでGUIのレイアウトをするのは苦行すぎます。
 とはいえゲームエンジン的な物を一から作り出すと、もはや何をやっているのか分からなくなっていきます。

 そんなこんなでTiledをGUIエディタとして活用して短期間で必要十分なソースコード自動生成環境を作成する方法を説明します。
 まずTiledの仕様をざくっと解説した後、データの説明、コードに変換するシステムの設計を解説します。
 サンプルはC++で書きますが、設計の話が大半なので別のプログラミング言語でも同じような事が出来ると思います。

 一応、前回の続きです。(※読まくてもよい)

 プログラムの設計の話が中心になるので、Tiledの細かい仕様とかコードの詳細は説明しません。

Tiledとは?

 マップチップをタイルに貼っていくタイプのマルチプラットフォームのマップエディタです。
 レイヤー機能やRedo/Undo他、必要な機能がひと通り揃っている他にオブジェクトを配置する機能や、別ファイル形式でエクスポートする機能があります。

 カスタムプロパティ機能を使う事で色々出来るのが特徴で、いくつかTiledを使っているゲームエンジンなどもあるようです。

 オープンソースソフトウェアなのでその機になれば改変も可能ですが、ゲーム制作に戻ってこれなくなるリスクがあるので多分やめておいた方が良いかもしれません。

詳細は以下で確認して下さい。
Tiled公式
窓の社レビュー記事

Tiledのオブジェクト

 Tiledのオブジェクトは四角形、楕円形、ポリゴン、ポリライン、タイルの5種類あります。
あとレイヤーとタイルセットという物があります。
 とりあえずレイヤーとタイルセットと四角形とタイルだけ使えれば十分だし、面倒なので4つだけ説明します。

 レイヤーはいわゆる普通のレイヤー機能です、オブジェクトは各レイヤーに配置していく形になります。
『名前、表示の有無、透過度、色(RGBA)、描画順序、カスタムプロパティ』のパラメータを持ちます。

 タイルは画像をはりつけるあれで、タイルセットはその集合です。
 タイルセットはマップチップ画像を分割して一括登録するか、大きさの異なる画像を一つずつ登録します。
 タイルセットは『名前、描画オフセット(X,Y),カスタムプロパティ』のパラメータを持ちます。
各タイルは『GID(非表示)、ファイルパス(非表示)、大きさ(非表示)、カスタムプロパティ』のパラメータを持ちます。

 四角形オブジェクトは任意の長方形です。色は配置したレイヤーの色になります。
『名前、種類、場所(X,Y)、大きさ(幅、高さ)、回転、カスタムプロパティ』のパラメータを持ちます。

 タイルオブジェクトは登録したタイルを配置出来ます。枠の部分がレイヤーの色になります。
『GID(非表示)、名前、種類、場所(X,Y)、大きさ(幅、高さ)、回転、横反転、縦反転、カスタムプロパティ』のパラメータを持ちます、大きさを変更してもエディタ上では反映されません。

カスタムプロパティ

 各オブジェクト等はデフォルトのパラメータの他にカスタムプロパティを任意の数設定出来ます。
カスタムプロパティにはは、プロパティ名と文字列を入力します。

 この機能を使う事で、『この四角形はボタンとして扱うので、ボタン上に表示する文字とフォント名のパラメータを追加する』とか『このタイルはSTGの敵ユニットなので、どの方向から出現するかのパラメータを追加する』とか『このタイルは4方向3パターンのアニメを持つので、ランダム移動のパターンとか最初の向きとかのパラメータを追加する』等、柔軟に利用する事が出来ます。

どのように運用するか?

 細かい部分どういうコードを生成したいかで変わるので基本的なコツだけ説明。
Tiledの使い方は実際にダウンロードして試した方が理解は早いと思います。

 マップチップレイヤーは不要なら削除しても良い。

 マップのサイズは1x1にして、タイルの大きさを必要な大きさにすると、タイルの目が邪魔にならない。

 オブジェクトの種類は見分けを付けるため、種類ごとに別のレイヤーに置くという方法もある。
例えば、ボタンと枠と文字が必要ならそれぞれ別のレイヤーにするとか。
 ただこれだとタイルセットをエクスポートして使う事になる関係で少しややこしくなるから微妙かも?

 カスタムパラメータを個別に追加するのは面倒なのでテンプレオブジェクトを範囲外に作っておき、右クリックメニューの『1つのオブジェクトを複製する』で追加すると楽。XML読み込み時特定の名前のデータは除外するとかすればOK。

 複数のオブジェクトやタイルを選択した状態だとカスタムパラメータもまとめて追加出来る。

 編集していくと以下の画像のような感じになると思います。
 Tiled.jpg

使用した画像の配布元
ぴぽや

 文字列オブジェクトが無い、カスタムプロパティの追加がやや面倒、座標値を小数点に出来る、画像の大きさがエディタ上で反映されないとかありますが、まぁ大丈夫かと。
 複数の画面をレイアウトしたい場合にそれぞれ別のタイル設定になると後で困るみたいな事も注意しておいた方が良さそうです。

データ解析

 Tiledはデータを色んな形式でエクスポート出来るので、お好きな形式のデータを解析すれば良いと思います。luaのファイルとか出してプログラムレスのエンジンにしてどうのこうのとかも出来るようです。
 今回はデフォルトのデータ形式(.tmx)を使う事にします、tmxの内部形式は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ファイルの位置との相対パスになるので、特に問題無く使えます。
普通は複数画面が必要になるのでエクスポートして使った方が良いと思います。

 <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用のパラメータなので無視しましょう。種類、回転、表示、デフォルト値のままなら出力されないようです。

 <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はタイルセットの情報とリンクしています。

 </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ゲームエンジン風にする方法も大体思いつくと思うので、別記事で解説とかもしません。

 STGの敵配置エディタみたいな使い方なら前者、ステータス画面のレイアウトエディタみたいな使い方なら後者にすると良い感じですね。

『手作業でコードを変更するとtmxデータに反映されるようにする』
これは面倒なのであきらめます。

内部形式をどうするか?

 オブジェクトの種類毎に共通のインターフェースを持たせる事で仕様変更に強くします。
どのようなコードを生成するかもオブジェクトが持つようにする事になります。
 名前と座標と大きさはどのオブジェクトも持つので、共通クラスのメンバーにしても良さそうです。
以下のような派生元のクラスを定義しておきます。

 Fileクラスは独自の物を使っているので脳内で動作を補完して下さい。

    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を継承したクラスにどういうコードを生成するか書きます。
例えばこんな感じ。

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時の処理が必要ないなら実装しなければ良いですね。
レイヤーのカスタムプロパティに変数名と変数の型を入れておけば、この部分のコードを生成する事も可能ですね。

コードを生成する

 あとはコードを生成するだけです。こんな感じでコードが生成出来ると思います。

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を解析して、その後コードを生成する感じです。

 最初の引数にtmxファイル名、次に出力ファイルのテンプレートとなるファイル、最後に出力するファイル名を入れましょう。

 tempFileはこんな感じにしておけば良いですね。

#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を調整する事でここは更新しないみたいな仕様も実現出来ますね。
 あるいは別ヘッダーに記述するようにして#includeを使うとかでも良さそうです。

 要件は全部解決出来ましたね。

まとめ

 ゲームタイトル毎に汎用性の無い使い捨ての自動生成システムを作るぐらい2,3日あれば余裕で出来るんや!標準エディタの出来がアレなライブラリでもなんとかなるで!!!

 ゲームエンジン使う場合もこういう事を知っておくと内部で何やってるか理解しやすくなるんじゃないかと思いました。

 おわり

mrdagon
趣味でゲーム作って無料公開したりしてます http://tacoika.blog87.fc2.com/
https://www.vector.co.jp/vpack/browse/person/an047099.html
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away