Help us understand the problem. What is going on with this article?

SpreadsheetからScriptableObjectを継承したクラスを生成する

More than 1 year has passed since last update.

はじめに

SpreadsheetからScriptableObjectを継承したクラスを自動生成したいので、サンプルを作りました。

今回のコードを下記に置きました。
https://github.com/jnhtt/ss2so

今までは、Spreadsheetの定義を決めてからScriptableObjectのクラスを書いていましたが、面倒です。

ここでは、SpreadsheetからScriptableObjectに変換して管理するデータをマスターデータとします。

フロー

データのフローは、Spreadsheet->json->CSharp/ScriptableObjectです。
作業フローはGoogle Drive側とUnity側に分かれます。

データフロー

Spreadsheetの1行目と2行目は特別な行です。
1行目はCSharpでメンバ変数名、2行目はCharpで型名になります。
この2つのデータからマスタークラスを作成します。

Spreadsheetのデータ

Spreadsheetの中身は単純です。

SpreadSheet CSharp 備考
 1行目 列名 メンバ変数名
2行目 データ形式 ignoreを記入した列はjsonに入らない
3行目〜 データ データ

作業フロー

Google Driveで外部からGoogle Apps Script(以降GASとします)を実行できるようにします。
GASへのアクセス設定は、「公開」にある「ウェブアプリケーションとして導入」を選びました。
「実行可能APIとして導入」の方が正しいような気がしますが、どうなんでしょうか。

Google Drive側

Google Driveに頻繁に行うことは、1のSpreadsheetの作成/更新です。
2と3は1度行えば、触ることはなくなると思います。
1. Spreadsheetの作成/更新
2. Google Apps Scriptを作成
3. Google Apps Scriptを外部から実行できるように設定

Unity側

Editor拡張の準備が終われば、やることはボタンを押すことだけです。
ただ、マスタークラスの更新とコンパイルの終了を待つ方法が分からなかったので、
クラス生成/更新とデータ作成/更新は別のステップにしました。
つまり、マスターでーたの構造が変わったらボタンを2回押す必要があります。
マスター関連クラスの作成->マスターデータの作成/更新の順番は守る必要がありますが、
データの追加だけならマスターデータの更新だけで十分です。

  1. マスタークラスの作成/更新
  2. マスターデータの作成/更新

GAS

Spreadsheetのデータをjsonで受け取るためのスクリプトです。
シート名(sheetName)を渡すとそのシートのデータをjsonで取得できます。

SpreadsheetApp.openById("ここは自分で入れてね");は使う環境のものを入れる必要があります。
あと、型名にignoreが入っているものをスキップします。

Spreadsheetをjsonで取得
function fakeDoGet() {
  var e = { "parameter" : { "sheetName" : "Test" } };
  doGet(e);
}

function doGet(e) {
  var sheetName = e.parameter.sheetName;
  var spreadSheet = SpreadsheetApp.openById("ここは自分で入れてね");
  var sheet = spreadSheet.getSheetByName(sheetName);
  if (sheet != null) {
    var json = convertSheetToJson(sheet);
    return ContentService.createTextOutput(JSON.stringify(json)).setMimeType(ContentService.MimeType.JSON);
  } else {
  }
}

function convertSheetToJson(sheet) {
  var columnStartIndex = 1;
  var rowNum = 1;
  var lastRow = sheet.getLastRow();

  var firstRange = sheet.getRange(1, 1, 1, sheet.getLastColumn());
  var nameRowValues = firstRange.getValues();
  var nameColumns = nameRowValues[0];

  var secondRange = sheet.getRange(2, 1, 1, sheet.getLastColumn());
  var typeRowValues = secondRange.getValues();
  var typeColumns = typeRowValues[0];

  var jsonHeader = new Object();
  for (var j = 0; j < nameColumns.length; j++) {
    var typeStr = typeColumns[j].toString();
    if (typeStr == "ignore") {
      continue;
    }
    jsonHeader[nameColumns[j]] = typeColumns[j];
  }

  var rowValues = [];
  var lastColumn = sheet.getLastColumn();
  for (var rowIndex = 3; rowIndex <= lastRow; rowIndex++) {
    columnStartIndex = 1;
    var range = sheet.getRange(rowIndex, columnStartIndex, rowNum, lastColumn);
    var values = range.getValues();
    rowValues.push(values[0]);
  }

  var jsonElementArray = [];
  for (var i = 0; i < rowValues.length; i++) {
    var line = rowValues[i];
    var json = new Object();
    for (var j = 0; j < nameColumns.length; j++) {
      if (typeColumns[j].toString() == "ignore") {
        continue;
      }
      json[nameColumns[j]] = line[j];
    }
    jsonElementArray.push(json);
  }
  var jsonArray = [];
  var obj = new Object();
  obj["masterHeader"] = jsonHeader;
  obj["masterArray"] = jsonElementArray;

  return obj;
}

UnityEditor拡張

Unityのメニューにボタンを追加します。
- マスター関連クラスを作成するボタン
- マスターデータのScriptableObjectを作成するボタン

jsonのパースにはLitJsonを使っています。
Editor内だけです。ゲーム中ではJsonUtilityを使います。
LitJson

JsonUtilityでデータのレイアウトがわかっていないとものをパースでする方法が分からなかったからです。

マスター関連クラス

事前に用意するクラス

MasterElementはマスターデータの要素で、Spreadsheetの行の部分です。
MasterElementCollectionはMasterElementを管理します。

クラス名 説明
MasterElement マスターデータの要素
MasterElementArray JsonUtiity.FromJson用のラッパー
MasterElementCollection マスターデータの要素を管理

テンプレから生成するクラス

MasterElementClassCreatorMasterElementを継承したマスターデータの要素クラスを作成します。
MasterElementCollectionClassCreatorMasterElementCollectionを継承したクラスを作成します。

例として、マスター名をTestとすると、下記のクラスが生成されます。

クラス名 説明
MasterTest MasterElementを継承
MasterTestCollection MasterElementCollectionを継承

MasterElementClassCreator

テンプレからMasterElement継承クラスを生成します。

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

public static class MasterElementClassCreator
{
    public static readonly string MASTER_GENERATED_PATH = Application.dataPath + "/Master/Generated";
    public const string PRIMARY_KEY = "id";

    public static bool Create(string masterName, string jsonString)
    {
        try {
            var masterJsonData = LitJson.JsonMapper.ToObject<LitJson.JsonData>(jsonString);
            CreateMasterElementClassScript(masterName, masterJsonData);

        } catch (System.Exception e) {
            Debug.LogError(e.ToString());
            return false;
        }
        return true;
    }

    private static void CreateMasterElementClassScript(string masterName, LitJson.JsonData masterJsonData)
    {
        string classContent =
            @"// GENERATED BY SCRIPT! DO NOT EDIT!
using System;

namespace Master
{
    [Serializable]
    public class $0 : MasterElement<$1>
    {
        $2

        $3
    }
}
            ";

        classContent = classContent.Replace("$0", masterName);
        classContent = classContent.Replace("$1", GetClassPrimaryKeyType(masterJsonData));
        classContent = classContent.Replace("$2", GetClassProperties(masterJsonData));
        classContent = classContent.Replace("$3", GetClassConstructor(masterName, masterJsonData));

        string path = string.Format("{0}/{1}.cs", MASTER_GENERATED_PATH, masterName);
        System.IO.File.WriteAllText(path, classContent);
    }

    private static string GetClassPrimaryKeyType(LitJson.JsonData masterJsonData)
    {
        var primary = masterJsonData["masterHeader"][0];
        return primary.ToString();
    }

    private static string GetClassProperties(LitJson.JsonData masterJsonData)
    {
        string properties = "";
        foreach (KeyValuePair<string, LitJson.JsonData> t in masterJsonData["masterHeader"])
        {
            if (PRIMARY_KEY == t.Key)
            {
                continue;
            }
            properties += string.Format("public {0} {1};", t.Value, t.Key) + System.Environment.NewLine;
        }

        return properties;
    }

    private static string GetClassConstructor(string className, LitJson.JsonData masterJsonData)
    {
        string constructor = @"
        public $0($1) : base($2)
        {
$3
        }
        ";

        string args = "";
        foreach (KeyValuePair<string, LitJson.JsonData> t in masterJsonData["masterHeader"])
        {
            args += string.Format("{0} {1},", t.Value, t.Key);
        }
        args = args.TrimEnd(',');

        string properitiesSetter = "";
        foreach (KeyValuePair<string, LitJson.JsonData> t in masterJsonData["masterHeader"])
        {
            if (PRIMARY_KEY == t.Key)
            {
                continue;
            }
            properitiesSetter += string.Format(@"           this.{0} = {1};", t.Key, t.Key) + System.Environment.NewLine;
        }

        constructor = constructor.Replace("$0", className);
        constructor = constructor.Replace("$1", args);
        constructor = constructor.Replace("$2", PRIMARY_KEY);
        constructor = constructor.Replace("$3", properitiesSetter);
        return constructor;
    }
}

MasterElementCollectionClassCreator

MasterElementCollectionを継承したクラスを作成します。

MasterElementCollectionClassCreator
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class MasterElementCollectionClassCreator
{
    public static readonly string MASTER_GENERATED_PATH = Application.dataPath + "/Master/Generated";

    public static bool Create(string masterName, string jsonString)
    {
        try {
            var masterJsonData = LitJson.JsonMapper.ToObject<LitJson.JsonData>(jsonString);
            CreateMasterElementCollectionClassScript(masterName, masterJsonData);

        } catch (System.Exception e) {
            Debug.LogError(e.ToString());
            return false;
        }
        return true;
    }

    private static void CreateMasterElementCollectionClassScript(string masterName, LitJson.JsonData masterJsonData)
    {
        string classContent =
            @"// GENERATED BY SCRIPT! DO NOT EDIT!
using System;

namespace Master
{
    [Serializable]
    public partial class $0Collection : MasterElementCollection<$1, $2>
    {
        $3
    }
}
            ";

        classContent = classContent.Replace("$0", masterName);
        classContent = classContent.Replace("$1", GetClassPrimaryKeyType(masterJsonData));
        classContent = classContent.Replace("$2", masterName);
        classContent = classContent.Replace("$3", GetLoadFromJsonFunction(masterName, masterJsonData));

        string path = string.Format("{0}/{1}Collection.cs", MASTER_GENERATED_PATH, masterName);
        System.IO.File.WriteAllText(path, classContent);
    }

    private static string GetClassPrimaryKeyType(LitJson.JsonData masterJsonData)
    {
        var primary = masterJsonData["masterHeader"][0];
        return primary.ToString();
    }

    private static string GetLoadFromJsonFunction(string className, LitJson.JsonData masterJsonData)
    {
        string loadFromJsonFunction = @"
        public static $0Collection LoadFromJson(string jsonString)
        {
            var instance = CreateInstance<$0Collection>();
            return LoadFromJson<$0Collection>(instance, jsonString);
        }
        ";

        loadFromJsonFunction = loadFromJsonFunction.Replace("$0", className);
        return loadFromJsonFunction;
    }
}

マスターデータの生成

下記の手順でマスターデータを生成します。

  1. シート名をからMasterElementCollectionを継承したクラスを取得
    • シート名がTestなら、MasterTestCollection
  2. Type.InvokeMemberMasterTestCollection.LoadFromJsonを実行
  3. Convert.ChangeTypeMasterTestCollectionに型を変換
  4. AssetDatabase.SaveAssets();でアセットをデータベースに保存

Type.GetTypeでは上手くいかなかったので、GetTypeByClassNameでTypeを取得します。

jsonからScriptableObjectを作成
    private static void LoadScriptableObject(string masterName, string jsonString)
    {
        string masterElementCollectionType = string.Format("{0}Collection", masterName);
        try
        {
            Type type = GetTypeByClassName(masterElementCollectionType);
            object result = type.InvokeMember("LoadFromJson", System.Reflection.BindingFlags.InvokeMethod, null, null, new object[] { jsonString });

            var masterElementCollection = (UnityEngine.Object)Convert.ChangeType(result, type);
            AssetDatabase.CreateAsset(masterElementCollection, string.Format("Assets/Resources/MasterData/{0}.asset", masterName));

            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        } catch (Exception e) {
            Debug.LogError(e.ToString());
        }
    }

    private static bool TryGetMasterName(string url, out string sheetName)
    {
        sheetName = "";
        var r = new System.Text.RegularExpressions.Regex(@"sheetName=(?<sheetName>.+)", System.Text.RegularExpressions.RegexOptions.ECMAScript);
        var m = r.Match(url);
        if (m.Success)
        {
            sheetName = m.Groups["sheetName"].Value;
            sheetName = string.Format("master{0}", sheetName);
            return true;
        }
        return false;
    }

    private static Type GetTypeByClassName(string className)
    {
        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
            foreach (var t in asm.GetTypes()) {
                if (t.Name == className) {
                    return t;
                }
            }
        }
        return null;
    }

テスト

簡単なデータで、フローの確認をします。

UnityWebRequest.GetにGASにアクセスするurlを入れます。
request.downloadHandler.textにjsonが入っています。

var request = UnityWebRequest.Get(url);
yield return request.SendWebRequest();
while (!request.isDone) {
   yield return null;
}
string jsonString = request.downloadHandler.text

Testスプレッドシート

Testシートを作り、こんな感じのデータを用意します。
スクリーンショット 2018-11-10 17.52.33.png

MasterTest/MasterTestCollection

コンバートの結果、下記のファイルができます。

MasterTest.cs
// GENERATED BY SCRIPT! DO NOT EDIT!
using System;

namespace Master
{
    [Serializable]
    public class MasterTest : MasterElement<uint>
    {
        public string name;



        public MasterTest(uint id,string name) : base(id)
        {
           this.name = name;

        }

    }
}

MasterTestCollection.cs
// GENERATED BY SCRIPT! DO NOT EDIT!
using System;

namespace Master
{
    [Serializable]
    public partial class MasterTestCollection : MasterElementCollection<uint, MasterTest>
    {

        public static MasterTestCollection LoadFromJson(string jsonString)
        {
            var instance = CreateInstance<MasterTestCollection>();
            return LoadFromJson<MasterTestCollection>(instance, jsonString);
        }

    }
}

MasterTest.asset

こんな感じのScriptableObjectができます。

スクリーンショット 2018-11-10 23.30.58.png

残った課題

テンプレからクラスを生成する処理は、テンプレの文字列を別ファイルにする方が管理しやすいと思います。。。が、力尽きました。時間があればやりたいです。

マスターデータのロードに関しては、Type.GetType("名前空間.クラス名");で上手く取得できない理由が分からないままになっています。

しかしながら、ファーストステップとしては十分じゃないかと。

さいごに

Spreadsheetからマスター関連クラスとマスターデータを生成できるようになったと思います。
コードからコードを生成する処理は動くと達成感がありますね。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした