【Unity】カジュアルゲーム向けScriptableObjectでシーンオブジェクトデータの保存【C#】【エディタ拡張】
はじめに
ステージ数が多くなりがちなUnityでのカジュアルゲーム開発。
シーン上のTag付きObjectのロードとセーブをエディタ上で行える実用的なスクリプトを作りました。
Scene内のObjectをPosition、Rotation、ObjectTypeを一括セーブ、ロードできるエディタは拡張性に富んでいて便利だと思います。ぜひ応用してもらいたいです。
荒い実装ではありますが、エクセルではなくScriptableObjectにセーブすることを学んだので共有。
エディタ拡張も用いてプランナーのステージ開発も助けます。
ちなみに本スクリプトは城を構成するPrefab(レンガなど)をセーブ、ロードするための変数などの命名をしています。
作成したEditorはUnityのEditorタブに表示されます。
開発環境
OS: MacOS Mojave
開発環境: Unity 2018.4.11f1 personal
開発言語: C#
動作環境
あるオブジェクト以下(本スクリプトではCastleオブジェクト)のTag付きObjectのPrefab(Prefab名とTag名を同じにしてください)のPosition、Rotation、種類を保存、ロードできるようになります。実行しなくてもエディタ上で編集したまま保存できます。
ソースコードとその説明
このスクリプト(EditorWindow.cs)ではEditor上にセーブ機能とロード機能をもつエディタウインドウを作成します。(記事のメインテーマのクラスです)
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(){
//エディタレイアウト
//2行開ける
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");
}
}
}
//2行開ける
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の雛形クラス
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参照)
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