2016年5月4日水曜日

Tiled Map Editor で生成したJSONをC言語で読めるようにする

前回記事で紹介した Tiled Map Editor は、JavaScriptであれば割りと何も考えずに使えますが、これをC言語で扱える構造体形式に変換するツールを作ってみました。

Tiled Map Editor が吐き出せるデータ形式はデフォルトはXMLですが、今時XMLなんか使っている人なんて居るんですかね?JSON形式で吐き出せるので、JSON形式で吐き出したデータを構造体に変換します。

なお、JSONならダイレクトに読んで使っても良いかもしれませんが、C言語でJSONを読むのは少々面倒くさいので、JSONをバイナリ構造体に変換するCLIを(Cで書くのは面倒なのでC++11で)作ってみました。
// make stage data from tiled map editor data (JSON)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <string>
#include <fstream>
#include <streambuf>
#include "json11/json11.hpp"
// JSONから変換する構造体
// - ゲーム本体はCで書くのでバイナリ構造体にしてDSLOTに突っ込む
// - 固定長にしている
// - ゲームが起動後にメモリの確保・開放はしない主義だという理由もあるが、結構余裕を持たせても 合計約24KB程度 の小さなデータなので、これなら動的メモリ確保にするデメリットの方が目立つ
struct StageEvent {
short x; /* x position */
short y; /* y position */
short type; /* type of event */
short cnum; /* control-number of event */
short arg1; /* argument 1 of event */
short arg2; /* argument 2 of event */
short arg3; /* argument 3 of event */
short arg4; /* argument 4 of event */
};
struct StageData {
int width; /* width of map */
int height; /* height of map */
int gslot; /* number of GSLOT */
int events; /* number of events */
unsigned char data[8192]; /* map (max: 8KB) */
struct StageEvent event[1024]; /* event (max: 16KB) */
};
// テキストファイルを std::string形式 で読み込む(手抜き版)
std::string load_text_file(const char* filename) {
std::ifstream t(filename);
std::string str;
t.seekg(0, std::ios::end);
str.reserve(t.tellg());
t.seekg(0, std::ios::beg);
str.assign((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
return str;
}
int main(int argc, char* argv[])
{
struct StageData stg;
::memset(&stg ,0, sizeof(StageData));
// 引数チェック
if (argc < 3) {
fprintf(stderr, "usage: mkstg source-json output-data\n");
return 1;
}
// tiled map editor が出力する JSONファイル を読み込む
auto jsonString = load_text_file(argv[1]);
std::string err;
auto json = json11::Json::parse(jsonString, err);
if (!err.empty()) {
fprintf(stderr, "json parse error: %s\n", err.c_str());
return 2;
}
// 幅と高さを取得
stg.width = json["width"].int_value();
stg.height = json["height"].int_value();
printf("map-size: %d x %d\n", stg.width, stg.height);
// チップセットに使うGSLOT番号を取得
auto gslot = json["tilesets"][0]["name"].string_value();
printf("chip-set: %s\n", gslot.c_str());
if (::strncmp(gslot.c_str(), "GSLOT", 5)) {
fprintf(stderr, "invalid chip-set name\n");
return 2;
}
stg.gslot = ::atoi(gslot.c_str() + 5);
// レイヤー解析
auto layers = json["layers"].array_items();
int n = 0;
for (auto& layer : layers) {
// レイヤーの種別を取得
printf("layer #%d:\n", n++);
auto type = layer["type"].string_value();
printf("- type: %s\n", type.c_str());
// レイヤー種別に対応したパースをする
// 私は今回、
// - マップデータとしてタイルレイヤー(tilelayer) を 1枚
// - イベントデータ(敵キャラや各種制御全般)としてオブジェクトレイヤー(objectgroup)を 1枚
// を使うので、その前提でパースしている
if ("tilelayer" == type) {
// todo: 後で tilelayer はマルチレイヤにするかも
// todo: とりあえず、シングルレイヤ想定で
auto data = layer["data"].array_items();
int i = 0;
for (auto chipNumber : data) {
stg.data[i++] = (unsigned char)(chipNumber.int_value() & 0xff);
}
printf("- set %d chip-sets\n", i);
} else if ("objectgroup" == type) {
auto objects = layer["objects"].array_items();
int i = 0;
for (auto& object : objects) {
stg.event[i].x = (short)(object["x"].int_value() + object["width"].int_value() / 2);
stg.event[i].y = (short)(object["y"].int_value() + object["height"].int_value());
stg.event[i].type = (short)object["properties"]["type"].int_value();
stg.event[i].cnum = (short)object["properties"]["cnum"].int_value();
stg.event[i].arg1 = (short)object["properties"]["arg1"].int_value();
stg.event[i].arg2 = (short)object["properties"]["arg2"].int_value();
stg.event[i].arg3 = (short)object["properties"]["arg3"].int_value();
stg.event[i].arg4 = (short)object["properties"]["arg4"].int_value();
printf("- event[%d]: x=%d, y=%d, type=%d, cnum=%d\n", i, (int)stg.event[i].x, (int)stg.event[i].y, (int)stg.event[i].type, (int)stg.event[i].cnum);
i++;
}
stg.events = i;
printf("- number of events: %d\n", stg.events);
} else {
fprintf(stderr, "error: unknown type");
return 3;
}
}
// JSONから構造体に読み込んだ内容をファイルに保存
FILE* fp = fopen(argv[2], "wb");
if (NULL == fp) {
fprintf(stderr,"file write error\n");
return 4;
}
fwrite(&stg, sizeof(stg), 1, fp);
fclose(fp);
return 0;
}
view raw mkstg.cpp hosted with ❤ by GitHub

JSONパーサーにはDropbox社が提供しているjson11を使用。
C++界隈のJSONパーサーというと、国内ではpicojsonとかが推されているようですが、私が使ってみた限り、モダンC++で書くにはかなり野暮ったい気がするし、数値型がdouble限定なのもイケてないという印象でした。「数値なら何でも実数でいいじゃない」という考え方は、実用上問題無ければそれで良いと思いますがFPUの仕様上、実用上の問題(性能云々は今回気にしていないので性能上の問題ではなく正確性の方の問題ですね)があるので、整数で表現すべき箇所は明確に整数で定義しないとダメだということです。なお、性能云々を今回気にしていないのは、性能にシビアな部分ではJSONパース済みのバイナリデータしか使わないからです。
上記CLIのソースをコンパイルする手順:

$ git submodule add https://github.com/dropbox/json11.git
$ g++ -std=c++11 json11/json11.cpp mkstg.cpp -o mkstg

これで、あとは保存したバイナリをVGSのDSLOTに突っ込んでマップを表示するプログラムを書くだけで、C言語でTiled Map Editorで作ったマップが表示できるようになります。簡単ですね。
C言語でマップを表示してみた図(Macで撮影)
マップを表示させただけでかなりゲームっぽくなるのでテンションが上がります。

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。

合理的ではないものを作りたい

ここ最近、実機版の東方VGSの開発が忙しくて、東方VGSの曲追加が滞っています。 東方VGS(実機版)のデザインを作りながら検討中。基本レトロUIベースですがシークバーはモダンに倣おうかな…とか pic.twitter.com/YOYprlDsYD — SUZUKI PLAN (...