【Unity】カジュアルゲーム向けScriptableObjectでシーンオブジェクトデータの保存【C#】【エディタ拡張】

# 【Unity】カジュアルゲーム向けScriptableObjectでシーンオブジェクトデータの保存【C#】【エディタ拡張】

## はじめに

ステージ数が多くなりがちなUnityでのカジュアルゲーム開発。
シーン上のTag付きObjectのロードとセーブをエディタ上で行える実用的なスクリプトを作りました。

Scene内のObjectをPosition、Rotation、ObjectTypeを一括セーブ、ロードできるエディタは拡張性に富んでいて便利だと思います。ぜひ応用してもらいたいです。

荒い実装ではありますが、エクセルではなくScriptableObjectにセーブすることを学んだので共有。
エディタ拡張も用いてプランナーのステージ開発も助けます。
ちなみに本スクリプトは城を構成するPrefab（レンガなど）をセーブ、ロードするための変数などの命名をしています。

作成したEditorはUnityのEditorタブに表示されます。


<img width="495" alt="スクリーンショット 2019-11-21 21.23.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300820/de90d64b-3bce-8f8d-78d5-924553ad23e0.png">

## 開発環境

OS: MacOS Mojave
開発環境: Unity 2018.4.11f1 personal
開発言語: C#

## 動作環境

あるオブジェクト以下（本スクリプトではCastleオブジェクト）のTag付きObjectのPrefab（Prefab名とTag名を同じにしてください）のPosition、Rotation、種類を保存、ロードできるようになります。実行しなくてもエディタ上で編集したまま保存できます。

## ソースコードとその説明

このスクリプト（EditorWindow.cs）ではEditor上にセーブ機能とロード機能をもつエディタウインドウを作成します。（記事のメインテーマのクラスです）

```C#:EditorWindow.cs
using System;
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

#if UNITY_EDITOR
public class EditorWindowScript : EditorWindow{
    [MenuItem("Editor/SceneEditWindow")]
    private static void Create(){
        // エディタウインドウ生成
        GetWindow<EditorWindowScript>("SceneEditWindow");
    }

    //テキストフィールド用変数
    string textField1 = "";
    string textField2 = "";

    void OnGUI(){
        //エディタレイアウト
        //２行開ける
        EditorGUILayout.Space();
        EditorGUILayout.Space();
        //セーブ用テキストフィールド作成
        textField1 = EditorGUILayout.TextField( "セーブするシーン名",textField1 );

        using (new GUILayout.HorizontalScope()){
            //セーブ用ボタン作成
            if (GUILayout.Button("Save Scene")){
                //Castle Object内のTag付きObjectを全てセーブします
                GameObject castle = GameObject.Find("Castle");
                //Resources内のCastleObjectData(ScriptableObject)をtextField1を用いてアクセス
                CastleObjectData data = Resources.Load<CastleObjectData>("CastleObjectData/CastleObjectData"+textField1);
                //すでに存在するデータ名であれば上書きして良いか警告する
                if(data != null){
                    //Canselボタンあり、OKかCanselかで処理分けする場合のUnityDialog
                    bool b = EditorUtility.DisplayDialog("警告", "上書きしますか？?", "はい", "いいえ");
                    if(!b) {
                        //”いいえ”ならセーブに必要なTextField1をリセット
                        textField1 = "";
                    } 
                }
                Resources.UnloadUnusedAssets();

                if(textField1!=""){
                    //データ保存用CastleObjectData(ScriptableObject)を作成
                    CastleObjectData castleObjectData = ScriptableObject.CreateInstance<CastleObjectData>();
                    //Castle Object以下の全てのTag付きObjectのPosition,Rotation,ObjectType(Tag)を保存します。（形式についてはCastleObjectDataクラスを参照）(GetAllメソッドについてはGetAllChildrenクラスを参照)
                    List<GameObject> list = GetAllChildren.GetAll(castle);
		            foreach (GameObject obj in list) {
                        ObjectInfo objectInfo = new ObjectInfo();
                        if(obj.tag!="Untagged"){
                            objectInfo.objectTransform = obj.transform.position;
                            objectInfo.objectRotation  = obj.transform.rotation;
                            objectInfo.objectType = obj.tag;
                            
                            castleObjectData.castleObjects.Add(objectInfo);
                        }
		            }
                    //ファイル書き出し
                    AssetDatabase.CreateAsset(castleObjectData, "Assets/Resources/CastleObjectData/CastleObjectData"+textField1+".asset");
                }
            }
        }
        //２行開ける
        EditorGUILayout.Space();
        EditorGUILayout.Space();
        //ロード用テキストフィールド作成
        textField2 = EditorGUILayout.TextField( "ロードするシーン名",textField2);
        
        using (new GUILayout.HorizontalScope()){
            //ロードボタン
            if (GUILayout.Button("Load Scene")){
                int prefabNum = 0;
                GameObject castle = GameObject.Find("Castle");
                //Canselボタンあり、OKかCanselかで処理分けする場合
                bool b = EditorUtility.DisplayDialog("警告", "ロードしますか？?", "はい", "いいえ");
                if(b) {
                    //castle以下のObjectを取得（GetAllメソッドについてはGetAllChildrenクラスを参照）
                    List<GameObject> list = GetAllChildren.GetAll(castle);
                    //元々のシーン上のオブジェクトを破棄
                    foreach(GameObject obj in list){
                        DestroyImmediate(obj);
                    }

                    CastleObjectData data = Resources.Load<CastleObjectData>("CastleObjectData/CastleObjectData"+textField2);
                    foreach(ObjectInfo objectInfo in data.castleObjects){
                        prefabNum++;
                        //プレハブを取得(Tag名とPrefab名は便宜上同じにしておく)
                        GameObject prefab;
                        prefab = (GameObject)Resources.Load("Prefabs/"+objectInfo.objectType);
                        string prefabName = objectInfo.objectType;
                        
                        //プレハブからインスタンスを生成
                        if(prefab!=null){
                            var obj = Instantiate(prefab, objectInfo.objectTransform , objectInfo.objectRotation ,castle.transform);
                            obj.name = prefabName;
                        }
                    }
                }
            }
	    }
    }
}
#endif

```

以下、CastleObjectDataを保存するScriptableObjectの雛形クラス

```C#:CastleObjectData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

//ここにPrefab名とTag名と同じObjectTypeをEnumで定義しておく
public enum ObjectType{
    Block = 0,
    Bomb = 1,
    FiveBlock = 2,
    TowerBlock = 3
}

//ScriptableObjectの雛形を定義
public class CastleObjectData : ScriptableObject{
    //各ObjectのObjectInfoデータセットのList
    public List<ObjectInfo> castleObjects = new List<ObjectInfo>();

}

//ObjectInfoにはTransformPosition、TransformRotation、TagをStringで保存
[System.Serializable]
public class ObjectInfo{
    public Vector3 objectTransform;
    public Quaternion objectRotation;
    public string objectType;
}
```

以下、指定したObject以下の全てのObjectをListで取得するためのClass（参考文献1のScript参照）

```C#:GetAllChildren.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public static class GetAllChildren
{
	public static List<GameObject>  GetAll (this GameObject obj)
	{
		List<GameObject> allChildren = new List<GameObject> ();
		GetChildren (obj, ref allChildren);
		return allChildren;
	}

        //子要素を取得してリストに追加
	public static void GetChildren (GameObject obj, ref List<GameObject> allChildren)
	{
		Transform children = obj.GetComponentInChildren<Transform> ();
		//子要素がいなければ終了
		if (children.childCount == 0) {
			return;
		}
		foreach (Transform ob in children) {
			allChildren.Add (ob.gameObject);
			GetChildren (ob.gameObject, ref allChildren);
		}
	}
}
```
## 終わりに

Scene内のObjectをPosition、Rotation、ObjectTypeを一括セーブ、ロードできるエディタは拡張性に富んでいて便利だと思います。ぜひ応用してもらいたいです。

##参考文献
1.「全ての子要素を取得する(子要素の子要素の子要素の‥)」
http://kazuooooo.hatenablog.com/entry/2015/08/07/010938
2.「【エディタ拡張徹底解説】初級編①：ウィンドウを自作してみよう【Unity】」
https://caitsithware.com/wordpress/archives/1377
3.「【エディタ拡張徹底解説】初級編③：いろいろなGUI(EditorGUILayout編)【Unity】」
https://caitsithware.com/wordpress/archives/1454