Unityをレベルエディタとして使う

はじめに

Unityは多くのプラットフォームに対応したゲームエンジンですが、様々な理由でUnityをゲームエンジンとして使用して開発を行えない場合があります。

しかしながら、Unity自体のエディタとしての機能は便利なものがありますので、エディタ部分だけをお借りして、レベルエディタとして使ってしまおうというのが今回のお題です。

おおよその要件として、以下を想定しています。

  1. Mayaデータを配置用モデルとして使用する
  2. キャラクター、オブジェクトごとに固有の追加データを設定する
  3. 中間データを出力する
  4. バイナリデータへコンバートする

使用するUnityのバージョンはUnity5以降を使うことを想定していますが、バージョンによって違いがあるかもしれません。
また、スクリプトはC#で記述しています。
合わせてご了承ください。

順番に進めていきます。

1. Mayaデータを配置用モデルとして使用する

UnityとMayaがインストールされている環境であれば、Unityエディタを実行した状態で、Assetフォルダ以下にコピーするだけでインポートが行われます。

MayaAscii、MayaBinaryどちらの形式も可能です。

テクスチャについては、別途あらかじめTexturesというフォルダをモデルのコピー先フォルダに作り、そこに入れておく必要があります。

Mayaデータに限らず、モデルデータさえあればUnityエディタの機能でレベルの配置が行えます。

地形モデルの上に、キャラクター(敵やNPCなど)、オブジェクト(壊せる壺や宝箱など)を配置して、レベルをエディットしていきます。

ここまでは、Unityをゲームエンジンとして使用する開発とあまり変わりません。

Unity等のツールを使用しない場合は

  • モデルのインポートやビューワ
  • マウスによる選択や移動
  • 配置されているモデルの一覧リスト表示
  • 座標等の情報表示
  • 配置情報の保存と読み込み

等々を、自分で作ることになりますので、ここをスキップできるだけで個人的にはUnityを使う価値があると思います。

2. キャラクター、オブジェクトごとに固有の追加データを設定する

さて、配置はできましたが、これだけでは各配置物は座標や向きしか情報を持っていません。

例えば木箱を2か所に配置したとして、その耐久力、壊れた時のエフェクトなどはどちらも同じで良いですが、壊したときに中から出てくるものは、置いた木箱ごとに決めたいです。

そのため、各木箱に固有の追加データを持たせます。

固有の追加データを設定できるクラスをC#で作り、生成時にアタッチします。(もしくは、アタッチしたものをprefabにしておきます)

データクラスについては、ゲームアプリケーション側で同じ基底クラスを持つものはまとめてしまうのがよいかと思います。(敵はEnemyData、オブジェクトはObjectData、アイテムはItemData、など)

さらに、各データクラスに同じ基底クラスを継承させておくと、後述の出力時に楽ができます。

また、それぞれのクラスにカスタムエディタを書いておくと、インスペクタから編集がしやすくなります。(カスタムエディタの書き方については割愛します。)

3. 中間データを出力する

配置データと、固有データの出力を行います。

最終的にゲームアプリケーションにデータをロードする場合はデータサイズの都合上バイナリ形式にしますが、最初からバイナリ形式で出力してしまうと、

  • バージョン管理システムで差分を取れない
  • 他の環境で再ロードしづらい
  • 一般的には、そのままでは人が読めない

などの問題があります。

今回はJSON形式で中間データを出力し、バイナリにはコンバータを書いてコンバートすることにします。

JSONであればアスキーですので、上記の問題はおおむね解決します。また、広くサポートされている形式なので、データを流用しやすくなります。(資料作成など)

JSONの出力ですが、Unity5.3以降であれば標準でJsonUtility があります。
しかし、Dictionaryの出力にはひと手間かかるので、LitJsonなどお好みのJSONパーサを使用するとよいかと思います。

さて、JSONパーサが用意できたら、各モデルをリストアップし、それぞれの座標・向き・スケールと追加の固有データを出力します。
この際、各データクラスごとに分けておくと後々便利なので、データクラスの名前でDictionaryに入れて、それをJSONデータ化し、ファイルへ出力します。

{
    "ObjectData" : [
        {
            "Pos" : {
                "x" : 0.0,
                "y" : 0.0,
                "z" : 0.0
            },
            "Angle" : {
                "x" : 0.0,
                "y" : 0.0,
                "z" : 0.0
            },
            "Scale" : {
                "x" : 1.0,
                "y" : 1.0,
                "z" : 1.0
            },
            "drop_item" : 1
        },
        {
            ...
        }
    ],
    "EnemyData" : [
        {
            ...
        }
    ]
}
4.バイナリデータへコンバートする

出力されたJSONファイルは、その後バイナリへコンバートする必要がありますが、これもUnity内でC#クラスを作って行うことが可能です。

バイナリコンバータは、ジェネリックとリフレクションを使用することで、各データクラスごとに書く必要がなくなるため、C#で書くのがおすすめです。

JSONを、各データクラスの名前からデータリストへとパースし、データクラスの型情報から、フィールド情報を取得し、リフレクションを使用して値を取得、バイナリに書き込みを行います。

JSONデータ読み込みと、バイナリへの書き込み例を以下に示します。(実際のコードではありません)
フィールドがint型ならint型として、float型ならfloat型として出力しています。
(LitJsonを使用しています。Tはデータクラスです)

using LitJson;

// あらかじめ、以下のようにファイルをリードしてJSONデータにマップしています。
// var json_txt = File.ReadAllText(file);
// var json_data_root = JsonMapper.ToObject(json_txt);

// JSONデータから特定のデータクラスの配列のみを取得
public T[] ReadData<T>(JsonData json_data_root)
{
	T[] data_array = null;
	string type_name = typeof(T).Name;
	if (json_data_root.Keys.Contains(type_name)) {
		var json_array = json_data_root[type_name];
		data_array = JsonMapper.ToObject<T[]>(json_array.ToJson());
	}
	return data_array;
}

// データクラスの情報をバイナリに出力
public void WriteData<T>(BinaryWriter bw, T data)
{
	// データクラスの型情報から、フィールド情報を取得
	FieldInfo[] field_infos = typeof(T).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);

	// InvokeMemberで値を取得して、BinaryWriterに書き込む
	foreach (var field_info in field_infos)
	{
		var field_type = field_info.FieldType;
		if (field_type == typeof(Int32))
		{
			Int32 value = (Int32)typeof(T).InvokeMember(field_info.Name, BindingFlags.GetField, null, data, null);
			bw.Write(value);
		}
		else if (field_type == typeof(float))
		{
			float value = (float)typeof(T).InvokeMember(field_info.Name, BindingFlags.GetField, null, data, null);
			bw.Write(value);
		}
	}
}

このようにすると、データクラスにどのような型でなんという名前のフィールドがあるかを関知せず、Tに各データクラスを入れるだけでバイナリの出力が可能です。

バイナリ出力できる型の対応を十分なものにすれば、データクラスにフィールドが追加や削除されても、コンバータをメンテナンスする必要はなくなります。
データクラスの追加についても、少ないコストで対応できます。
処理負荷が高くなることは想定できますが、PC上での作業なので、大きな問題とはならないでしょう。

まとめ

Unityを使用することで、比較的簡単にレベルエディタを実装できました。

  • モデルデータの取り扱いが簡単
  • 基本的なGUIが揃っている
  • データの追加や操作など拡張が可能
  • C#でバイナリコンバータが書ける

などの利点があることが分かりました。

急にレベルエディタが必要になった際は、是非ご検討ください。