LoginSignup
1
1

More than 5 years have passed since last update.

JsonUtilityで基底クラスのリストをシリアライズする際に子クラスのデータを消さない方法

Posted at

課題

以下のようなクラスがあったとして

Person.cs
[System.Serializable]
public class Person
{
    public string name;
    public int age;
}

[System.Serializable]
public class Man : Person
{
    public string manField;
}

[System.Serializable]
public class Woman : Person
{
    public int womanField;
}

TestPersonListをJsonUtilityでシリアライズすると

Test1.cs
    [System.Serializable]
    public class TestPersonList{
        public List<Person> personList = new List<Person>();
    }

    void TestPersonFailed () {
        var target = new List<Person> () {
            new Man { age = 18, name = "男性", manField = "man" },
            new Woman { age = 16, name = "女性", womanField = 100 }
        };
        var testPersonList = new TestPersonList(){personList = target};
        var json = JsonUtility.ToJson (testPersonList); // manField, womanFieldが消える
        Debug.Log (json);
    }

manFieldwomanFieldがJSONに含まれません。
スクリーンショット 2019-04-08 0.33.03.png

これをJsonUtilityを使うことは維持しつつ解決したい。

対応

JsonUtilityでシリアライズできる形に整形する。

リポジトリ

以下のようなスクリプトを作ることで解決できます。

PersonList.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class PersonList {
    [SerializeField] List<int> manIndexList = new List<int> ();
    [SerializeField] List<Man> manList = new List<Man> ();
    [SerializeField] List<int> womanIndexList = new List<int> ();
    [SerializeField] List<Woman> womanList = new List<Woman> ();

    public PersonList (List<Person> src) {
        for (int i = 0; i < src.Count; i++) {
            switch (src[i]) {
                case Man man:
                    manIndexList.Add (i);
                    manList.Add (man);
                    break;
                case Woman woman:
                    womanIndexList.Add (i);
                    womanList.Add (woman);
                    break;

            }
        }
    }

    public List<Person> Convert () {
        int length =
            manIndexList.Count +
            womanIndexList.Count;
        var rtn = new Person[length];
        for (int i = 0; i < manIndexList.Count; i++) {
            rtn[manIndexList[i]] = manList[i];
        }
        for (int i = 0; i < womanIndexList.Count; i++) {
            rtn[womanIndexList[i]] = womanList[i];
        }
        return new List<Person> (rtn);
    }

    public static List<System.Type> GetAllDerivedTypes () {
        return new List<System.Type> () {
            typeof (Man),
            typeof (Woman)
        };
    }
}

JSON化、復元ともにできました。

スクリーンショット 2019-04-08 0.33.28.png

Test2.cs
    void TestPersonListOK () {
        var target = new List<Person> () {
            new Man { age = 18, name = "男性", manField = "man" },
            new Woman { age = 16, name = "女性", womanField = 100 }
        };
        var personList = new PersonList (target);

        // Object -> string
        var json = JsonUtility.ToJson (personList);
        Debug.Log (json);

        // string -> Object
        var personList2 = JsonUtility.FromJson<PersonList> (json);
        var target2 = personList2.Convert ();
        var man = target2[0] as Man;
        Debug.Log ($"age={man.age},name={man.name}, manField={man.manField}");
        var woman = target2[1] as Woman;
        Debug.Log ($"age={woman.age},name={woman.name}, manField={woman.womanField}");
    }

問題点

PersonList作るの面倒すぎ

対応2

PersonListを自動生成する。

DerivedClassListGenerator.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

public class DerivedClassListGenerator : CodeGenerator {
    public void CreateAt (Type baseType) {
        var derivedTypes = GetAllDerivedTypes (AppDomain.CurrentDomain, baseType);
        if (derivedTypes.Count == 0) {
            Debug.LogError ($"No Derived Class : {baseType.Name}");
            return;
        }
        // テンプレートをロード
        var templateLoader = new TemplateLoader ();
        var writer = new Writer ();
        var template = templateLoader.LoadTemplate (TemplatesDir + "DerivedClassList.txt");
        // 各項目の文言を生成
        string className = baseType.Name + "List";
        string fieldContent = GetField(derivedTypes);
        string switchContent = GetSwitch(derivedTypes);
        string convertContent = GetConvert(derivedTypes, baseType);
        string getAllDerivedTypesContent = GetGetAllDerivedTypes(derivedTypes);
        var content = template.text;
        content = content.Replace("$ClassName$", className);
        content = content.Replace("$Field$", fieldContent);
        content = content.Replace("$DataName$", baseType.Name);
        content = content.Replace("$Switch$", switchContent);
        content = content.Replace("$Convert$", convertContent);
        content = content.Replace("$GetAllDerivedTypes$", getAllDerivedTypesContent);
        // スクリプト生成
        writer.CreateNewScriptAt(className, content);
    }

    string GetField (List<Type> derivedTypes) {
        var stringBuilder = new StringBuilder ();
        bool isFirst = true;
        foreach (var type in derivedTypes) {
            string className = type.Name;
            string camelClassName = UpperCamelToCamel (className);
            string prefix = isFirst ? "" : Tabs(1);
            stringBuilder.AppendLine (prefix + $"[SerializeField] List<int> {camelClassName}IndexList = new List<int>();");
            stringBuilder.AppendLine (Tabs(1) + $"[SerializeField] List<{className}> {camelClassName}List = new List<{className}>();");
            isFirst = false;
        }
        return stringBuilder.ToString ();
    }

    string GetSwitch (List<Type> derivedTypes) {
        var stringBuilder = new StringBuilder ();
        bool isFirst = true;
        foreach (var type in derivedTypes) {
            string className = type.Name;
            string camelClassName = UpperCamelToCamel (className);
            string prefix = isFirst ? "" : Tabs(4);
            stringBuilder.AppendLine (prefix + $"case {className} {camelClassName}:");
            stringBuilder.AppendLine (Tabs(5) + $"{camelClassName}IndexList.Add(i);");
            stringBuilder.AppendLine (Tabs(5) + $"{camelClassName}List.Add({camelClassName});");
            stringBuilder.AppendLine (Tabs(5) + "break;");
            isFirst = false;
        }
        return stringBuilder.ToString ();
    }

    string GetConvert(List<Type> derivedTypes, Type baseType){
        var stringBuilder = new StringBuilder ();
        stringBuilder.AppendLine("int length = ");
        for (int i = 0; i < derivedTypes.Count; i++){
            string className = derivedTypes[i].Name;
            string camelClassName = UpperCamelToCamel (className);
            if(i == derivedTypes.Count -1 ){
                stringBuilder.AppendLine (Tabs(3) + $"{camelClassName}IndexList.Count;");
            }else{
                stringBuilder.AppendLine (Tabs(3) + $"{camelClassName}IndexList.Count +");
            }
        }
        stringBuilder.AppendLine(Tabs(2) + $"var rtn = new {baseType.Name}[length];");
        for (int i = 0; i < derivedTypes.Count; i++){
            string className = derivedTypes[i].Name;
            string camelClassName = UpperCamelToCamel (className);
            stringBuilder.AppendLine (Tabs(2) + $"for (int i = 0; i < {camelClassName}IndexList.Count; i++)");
            stringBuilder.AppendLine (Tabs(2) + "{");
            stringBuilder.AppendLine (Tabs(3) + $"rtn[{camelClassName}IndexList[i]] = {camelClassName}List[i];");
            stringBuilder.AppendLine (Tabs(2) + "}");
        }
        stringBuilder.Append(Tabs(2) +$"return new List<{baseType.Name}>(rtn);");
        return stringBuilder.ToString ();
    }

    string GetGetAllDerivedTypes(List<Type> derivedTypes){
        var stringBuilder = new StringBuilder ();
        for (int i = 0; i < derivedTypes.Count; i++){
            string className = derivedTypes[i].Name;
            string camelClassName = UpperCamelToCamel (className);
            if(i == derivedTypes.Count -1 ){
                stringBuilder.Append ( Tabs(3) + $"typeof({className})");
            }else{
                string prefix = i == 0 ? "" : Tabs(3);
                stringBuilder.AppendLine (prefix + $"typeof({className}),");
            }
        }
        return stringBuilder.ToString ();
    }

    public List<System.Type> GetAllDerivedTypes (System.AppDomain appDomain, System.Type baseType) {
        var result = new List<System.Type> ();
        var assemblies = appDomain.GetAssemblies ();
        for (int i = 0; i < assemblies.Length; i++) {
            var types = assemblies[i].GetTypes ();
            for (int j = 0; j < types.Length; j++) {
                if (types[j].IsSubclassOf (baseType)) {
                    result.Add (types[j]);
                }
            }
        }
        return result;
    }
}
CodeGenerator.cs
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

public class CodeGenerator {

    [System.Serializable]
    public class Field {
        public string name;
        public string fieldType;

        public string GetText (bool isPublic = true) {
            string format = isPublic ? "public {0} {1};" : "{0} {1};";
            return string.Format (format, fieldType, name);
        }
    }

    public static readonly string TemplatesDir = "Assets/Scripts/Editor/CodeGenerator/Templates/";

    public class TemplateLoader {
        public TextAsset LoadTemplate (string path) {
            return AssetDatabase.LoadAssetAtPath<TextAsset> (path);
        }
    }

    public class Writer {

        public void CreateNewScript (string path, string content) {
            using (StreamWriter streamWriter = File.CreateText (path + ".cs")) {
                streamWriter.WriteLine (content);
            }
            AssetDatabase.Refresh ();
        }

        public void CreateNewScriptAt (string fileName, string content) {
            var path = EditorUtility.OpenFolderPanel ("Select Create Folder", "", "");
            CreateNewScript (System.IO.Path.Combine (path, fileName), content);
        }
    }

    public string Tabs (int level) {
        string rtn = "";
        for (int i = 0; i < level; i++) {
            rtn += "\t";
        }
        return rtn;
    }

    public string UpperCamelToCamel (string src) {
        if (string.IsNullOrEmpty (src)) return src;
        return char.ToLowerInvariant (src[0]) + src.Substring (1, src.Length - 1);
    }
}
DerivedClassList.txt
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class $ClassName$
{
    $Field$
    public $ClassName$(List<$DataName$> src){
        for (int i = 0; i < src.Count; i++)
        {
            switch (src[i])
            {
                $Switch$
            }
        }
    }

    public List<$DataName$> Convert(){
        $Convert$
    }

    public static List<System.Type> GetAllDerivedTypes(){
        return new List<System.Type>(){
            $GetAllDerivedTypes$
        };
    }
}

まとめ

FullSerializerを使わずにJsonUtilityでがんばろうという試み
ゴリ押しですがいちおう達成はできました。

参考

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1