自習室

こもります

MiniJSON で UnityEngine.Vector3[] を永続化する

はじめに

本記事では「あるプレイヤーがゲーム中に作ったマップ的なものの情報をファイルに書き込んで保存し、それを別のプレイヤーに渡して読み込んで利用する」ようなことを想定しています。

ライブラリ捜索

PlayerPrefs だと、コンピュータごとにレジストリに書き込んだりするので、データの再配布は難しそうです。

Webのプログラムのように、テキストファイルに書き込んでしまうのが一番楽かと思い、Unityのデータのjsonへの書き込みおよびjsonテキストファイルからUnityのデータへの復元が出来るライブラリは無いかと調べて見たところ、 JsonFX と MiniJSON という2つのライブラリに出会いました。

JsonFXの方が多機能で便利そうですが、ライブラリが結構重量級です。今回は非常に軽量でファイル1個プロジェクトに置いておくだけで使えちゃうMiniJSONの方を利用してみます。

やること

「マップ的な物の情報」として、ここでは簡単のため、 Vector3[] 配列情報を扱う事にします。下記のようなデータをファイルに書き出し、その後読み込んでデータとして再現します。

    // Vector3 array to be serialized.
    Vector3[] v3Positions = new Vector3[]{
      new Vector3 (0.0f, 0.0f, 0.0f),
      new Vector3 (1.1f, 1.1f, 1.1f),
      new Vector3 (2.2f, 2.2f, 2.2f)
    };

行うのは以下の処理です

  • Vector3[] 配列をx,y,z にばらしながら List<float> に格納していく
  • MiniJSON の Json.Serialize() で、 List<float>json形式のテキストに変換する
  • そのテキストをファイルに書き込む

  • 上記ファイルからjsonテキストを読み込む
  • MiniJSON の Json.Deserialize() で、 json形式テキストを List<object> に変換する
  • そのListから1要素ずつ float に変換しながら、 Vector3 の x,y,z 要素として格納していく

以下では、填まった点をいくつかピックアップしてメモしておきます

List<float>エンコードデコードする

MiniJSONは IDictionary か IList を実装したクラスのみを扱えるので、ここでは Vector3[]List<float> に変換して扱っています。素直に考えると List<Vector3>jsonに変換、復号できるとナイス!と思うのですが、MiniJSONのコードを読んでみたところ、扱えるのはC# 組み込みのデータ型のみのようです。従って、Vector3 の中身を展開してx, y, zひとつずつ格納していきます。

MiniJSONで扱えるデータ型(これらをさらに Dictionary や Listでコレクションして使うのも可能)

MiniJSON.cs Serializer#SerializeOther() https://gist.github.com/darktable/1411710#file-minijson-cs-L523 を参照

  • string
  • bool
  • char
  • float
  • int, uint, long, sbyte, byte, short, ushort, ulong
  • double
  • decimal

読み書きするファイルの置き場

こちらの記事様が分かりやすかったです

この記事では以下の様な感じで、ディレクトリの生成、ファイルパスの指定を行っています。

using System.IO;
// 中略
    // write to a file
    string storagePath = Application.dataPath + "/data";
    // create directory if it doesn't exist.
    if (!Directory.Exists (storagePath)) {
      Directory.CreateDirectory (storagePath);
    }
    string filepath = storagePath + "/positions.json";

0.0f0 になる問題への対処

MiniJSONでは、Json.Serialize() 時に、floatのつもりで渡した 0.0f100.0f のような「ちょうど整数に変換できる浮動小数点値」が、jsonテキストに変換される際に整数になってしまいます。

このことは、MiniJSON のページの、ユーザコメントでも指摘がされていますが、いまのところ対処はなされていないようです。

なぜそんなことが起きるか調べてみたところ、数値を文字列に変換する際に、 value.ToString("R") という指定を行っているからのようです。 "R" の指定については、下記サイトが詳しいです

1.0f は間違いなく正確に 1 なので、数値の正確さを保つ、という目的に対してMiniJSONのこの措置は理にかなっていると思われます。

ただ、MiniJSONはListを復号する際、Genericとしてどのデータ型のListになっているかを保障せず、List<object> として復号します。Listの値をひとつずつ取り出してfloatとして再利用したいのですが、この際に、objectとして0というデータが入っているか0.0というデータが入っているかで、castの仕方がことなります。そこで、ここでは以下の様にtry-catchでそれを回避しています

    for (int i = 0; i< v3posNum; i++) {
      v3PositionsAgain [i] = new Vector3 ();
      v3PositionsAgain[i].x = objectToFloat(deserializedDoubleList [3 * i + 0]);
      v3PositionsAgain[i].y = objectToFloat(deserializedDoubleList [3 * i + 1]);
      v3PositionsAgain[i].z = objectToFloat(deserializedDoubleList [3 * i + 2]);
    }

  // 中略

  float objectToFloat(object objInList) { // 復号されたobjectが intでもdoubleでも対処出来るように用意した関数
    try{
      return (float)(double)objInList;
    } catch {
      return (float)(long)objInList;
    }
  }

またここにも一癖あります。MiniJSONは仕様として、floatはdoubleに復号、intはlongに復号するよう求めています。上記 objectToFloat() 関数も、それに従ったかたちとなっています。

完成

最後にコードをまとめて記載します

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using MiniJSON;
using System.IO;

public class Serializer : MonoBehaviour {

  void Start () {

    // Vector3 array to be serialized.
    Vector3[] v3Positions = new Vector3[]{
      new Vector3 (0.0f, 0.0f, 0.0f),
      new Vector3 (1.1f, 1.1f, 1.1f),
      new Vector3 (2.2f, 2.2f, 2.2f)
    };

    List<float> positionList = new List<float> ();
    foreach (Vector3 v3pos in v3Positions) {
      positionList.Add (v3pos.x);
      positionList.Add (v3pos.y);
      positionList.Add (v3pos.z);
    }
    string serialized = Json.Serialize (positionList);
    Debug.Log ("serialized text = " + serialized);

    // write to a file
    string storagePath = Application.dataPath + "/data";
    // create directory if it doesn't exist.
    if (!Directory.Exists (storagePath)) {
      Directory.CreateDirectory (storagePath);
    }
    string filepath = storagePath + "/positions.json";
    Debug.Log (filepath);
    File.WriteAllText (filepath, serialized);

    //-----------------------------------------------------------

    // re-read from the same file
    var jsonTextFromFile = File.ReadAllText (filepath);

    var deserializedDoubleList = Json.Deserialize (jsonTextFromFile) as List<object>;

    int v3posNum = deserializedDoubleList.Count / 3;
    Vector3[] v3PositionsAgain = new Vector3[v3posNum];
    for (int i = 0; i< v3posNum; i++) {
      v3PositionsAgain [i] = new Vector3 ();
      v3PositionsAgain[i].x = objectToFloat(deserializedDoubleList [3 * i + 0]);
      v3PositionsAgain[i].y = objectToFloat(deserializedDoubleList [3 * i + 1]);
      v3PositionsAgain[i].z = objectToFloat(deserializedDoubleList [3 * i + 2]);
    }
  }

  float objectToFloat(object objInList) {
    try{
      return (float)(double)objInList;
    } catch {
      return (float)(long)objInList;
    }
  }

}

その他メモ

参考にさせていただいたサイト様

JsonFXについてはこちらの記事様が良くまとまっていました。

MiniJSON の問題への、本記事で扱わなかった対処

  • 0.0f が 0 になる問題

こちらのコメント にあるように、 .ToString("F") 系の指定をすると、数値がちゃんと固定小数点っぽい表示で文字列に変換されます。しかしこの場合、 .ToString("R") 指定をした場合のような数値の正確性は担保されません。ご利用方に気をつけましょう、という感じ。

  • Vector3 とか扱えない問題

下記サイトで、MiniJSONの改善版がアップされています。Vector3 , Color や自作のクラスも扱えるようになる様です。試していません。

JSON formatting and parsing in Unity3D | WyrmTale articles