Unityで作成したAndroidアプリでファイルをxml形式で読み書きする

UnityでAndroid向けアプリを作成中にセーブ用ファイルの取り扱いでつまずいたので解決策を残します。

#やりたいこと
①キャラクターデータのファイルを読み込む。
②ゲーム中でキャラクターデータの編集が起こったらファイルを書き込む。
③XML形式で保存する。
④Android向けに出力する。

#ポイント
①初回ロード時はStreamingAssetsから読み込む。
②書き込みはpersistentDataPathに行い、以後の読み込みはpersistentDataPathから行う。
③XML形式はXmlSerializerを用いて扱う。
④StramingAssetsからの読み込みはWWWクラス、persistentDataPathからの読み込みはFileStreamを使う。日本語の場合はBOMなしのUTF-8を使用する。書き込みはStreamWriterで行う。


以下解説です。

#①初回ロードはStreamingAssetsから読み込む。
Androidでは直接Assets内のファイルにアクセス出来ません。また、通常はアセットが変換されてしまいます。しかしStreamingAssetsというフォルダを作ることにより生のファイルをアプリに含め、ファイルシステムを利用しアクセスすることができます。
Androidでは
"jar:file://" + Application.dataPath + "!/assets" + "/";
にStreamingAssetsの中身が出力されます。Chara.xmlを使うなら
"jar:file://" + Application.dataPath + "!/assets" + "/"+"Chara.xml";
となります。ビルドするとStreamingAssetsという名前は消えます。テキストやムービーなどのデータはこのフォルダから読み込み使用します。

#②書き込みはpersistentDataPathに行い、以後の読み込みはpersistentDataPathから行う。
StreamingAssetsフォルダですが、Androidでは書き込みに制限がかかっており読み込みはできるが書き込みは出来ません。そのため初回のデータはStreamingAssetsから読み込み、保存は別のフォルダpersistentDataPath内に行います。
Androidでは
Application.persistentDataPath+"/";
となります。キャラクターの初期値やデータベースはStreamingAssetsから読み込み、成長した手持ちキャラクターデータの更新は必要に応じてpersistentDataPathを利用します。

#③XML形式はXmlSerializerを用いて扱う。
今回は以下の様なXML形式でデータの保存を行いました。一部省略しています。

```xml
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfPlayerCharaParameter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <PlayerCharaParameter>
    <戦闘パラメーター>
      <攻撃>10</攻撃>
      <HP>2000</HP>
    </戦闘パラメーター>
  </PlayerCharaParameter>
  <PlayerCharaParameter>
    <戦闘パラメーター>
      <攻撃>100</攻撃>
      <HP>200</HP>
    </戦闘パラメーター>
  </PlayerCharaParameter>
</ArrayOfPlayerCharaParameter>
```

データを扱うクラスは以下です。

```csharp
//キャラクター全体のステータスまとめ
[System.Serializable]
public class PlayerCharaParameter
{
    [XmlElement("戦闘パラメーター")]
    public Battle battle;
    public PlayerCharaParameter(PlayerCharaParameter param)
    {
        battle = new Battle(param.battle);
    }
    public PlayerCharaParameter() { }
}
//バトル時に使用
[System.Serializable]
public class Battle
{
    [XmlElement("攻撃")]
    public float attackPower;
    [XmlElement("HP")]
    public float HP;
    public Battle() { }
    public Battle(Battle b)
    {
        attackPower = b.attackPower;
        HP = b.HP;
    }
}
```

これを以下のSerializerを用いてシリアライズ、デシリアライズします。

```csharp
public class XMLUtility
{
    // シリアライズ
    public static void Seialize<T>(string filename, T data)
    {
        using (var stream = new StreamWriter(filename, false, new System.Text.UTF8Encoding(false)))
        {
            var serializer = new XmlSerializer(typeof(T));
            serializer.Serialize(stream, data);
        }
    }

    // デシリアライズ
    public static T Deserialize<T>(string filename)
    {
        using (var stream = new FileStream(filename, FileMode.Open))
        {
            var serializer = new XmlSerializer(typeof(T));
            return (T)serializer.Deserialize(stream);
        }
    }
}
```

#④StramingAssetsからの読み込みはWWWクラス、persistentDataPathからの読み込みはFileStreamを使う。日本語の場合はBOMなしのUTF-8を使用する。書き込みはStreamWriterで行う。
ファイルの読み込みですが、読み取り方に制限があるようでStreamingAssetsフォルダからはWWWクラスが使えますがFileStreamが使えない、逆にpersistentDataPathからはFileStreamのみが使える、となってます。（なぜなのかは分かりませんでした。）そのため読み取る場所で読み取り方法を変える必要があります。
以下のようなDataLoaderを作成し読み込む場所によりReadFromStreamingとReadFromPersistentを使い分けます。

```csharp
public class DataLoader
{
    //StreamingAssetsからの読み込み、WWWを使用
    public static T ReadFromStreaming<T>(string fileName)
    {
        var fullPath = DataLoader.GetFilePathFromStreaming() + fileName;
        //WWWを使った読み込みは可能
        WWW www = new WWW(fullPath);
        //終わるまで待機。非同期で読み込む際はyieldを使用
        while (!www.isDone) { }
        string readText = www.text;
        //必要ならBOMなしに変換
        if (HasBomWithText(www.bytes)) readText = GetDeletedBomText(www.text);
        //TextReaderを用いてstringからデータ読み込み
        using (TextReader stream = new StringReader(readText))
        {
            var serializer = new XmlSerializer(typeof(T));
            return (T)serializer.Deserialize(stream);
        }
    }
    //persistentDataPathからの読み込み
    public static T ReadFromPersistent<T>(string fileName)
    {
        var fullPath = DataLoader.GetFilePathFromPersistent() + fileName;
        //内部ではFileStreamを利用
        return XMLUtility.Deserialize<T>(fullPath);
    }
    //persistentDataPathへの書き込み
    public static void WriteToPersistent<T>(string fileName, T data)
    {
        var fullPath = GetFilePathFromPersistent() + fileName;
        XMLUtility.Seialize<T>(fullPath, data);
    }
    //BOM有無の判定
    static bool HasBomWithText(byte[] bytes)
    {
        return bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF;
    }
    //BOM消し
    static string GetDeletedBomText(string text)
    {
        return text.Remove(0, 1);
    }
    //persistentDataPathのパスを返す
    static string GetFilePathFromPersistent()
    {
        return Application.persistentDataPath + "/";
    }
    //プラットフォーム毎のStreamingAssetsのパスを返す
    static string GetFilePathFromStreaming()
    {
    #if UNITY_EDITOR
        return "file:///" + Application.dataPath + "/StreamingAssets/";
    #elif UNITY_IPHONE || UNITY_ANDROID
      return "jar:file://" + Application.dataPath + "!/assets" + "/";
    #endif
    }
}
```

日本語を扱う際はUnicode(UTF-8)を利用します。WWWクラスの読み込みではテキストデータをそのまま読み込むので先頭にBOMがあるとデシリアライズがはたらきません。そのため上記では先頭3Byteを比較しBOMなしに変換しています。また、デバッグの際はフォルダパスが変わるのでStreamingAssetsフォルダをプラットフォームで変えるようにしています。
実際に使用する際はXMLファイルにキャラクターデータが複数あると思うのでListを利用します。

```
初回読み込み
List<PlayerCharaParameter>　charaParameters = 
DataLoader.ReadFromStreaming<List<PlayerCharaParameter>>("DatabaseChara.xml");
データ作成
DataLoader.WriteToPersistent<List<PlayerCharaParameter>>("Chara.xml", charaParameters);
2回目以降の読み込み
List<PlayerCharaParameter>　charaParameters = DataLoader.ReadFromPersistent<List<PlayerCharaParameter>>("Chara.xml");
```

以上の様にしてデータの保存を行います。

StreamingAssetsのアクセスに"jar:file://" をつけ忘れない。StreamingAssetsではWWWクラスから、persistentDataPathはFileStreamから読み込む。日本語の場合はUTF-8のBOMなし。
以上3点がまとめられた情報源がなかったのでこれで解決出来た方がいれば幸いです。