ETC/Unity

[Unity] Script에 의존하지 않고 스토리 씬 구현하기

Jade Choe 2024. 3. 4. 19:00
SMALL

C# 스크립트로 스토리 진행 로직 작업을 하다가 문득 그런 생각이 들었다.
 

Xml이나 Json에 프리팹이나 리소스와 스토리 내용을 등록해 직렬화 하고,
거기에 필요한 데이터만 런타임중에 불러와서 표시해주면 간단하지 않을까?

 
 
 
바로 해보자.
 

Editor 폴더에 스크립트를 작성한다.
 
Story Scene Editor는 스토리 내용과 리소스를 등록해줄 에디터
Story Scene List는 위 에디터에서 직렬화 후 저장한 데이터들을 한번에 볼 수 있는 창이다.

 

    [SerializeField]
    public Dictionary<string, string> storySceneList;

    private SerializedObject storySceneObject;
    private SerializedProperty storySceneListProperty;

    [MenuItem("Tools/Story Scene Editor")]
    public static void ShowWindow()
    {
        GetWindow<StorySceneList>("Story Scene List");
    }
    
    void OnEnable()
    {
        storySceneObject = new SerializedObject(this);
        storySceneListProperty = storySceneObject.FindProperty("storySceneList");
        storySceneList = GetStorySceneList();
    }

 
 
파일명을 키로 갖는 파일 경로 딕셔너리를 만들어주고,
에디터에 표시해주기 위해 SerializedObject와 SerializedProperty를 추가해준 후
 
목록을 열어볼 수 있게 메뉴에 추가, 변수들을 초기화해준다.
 
GetStorySceneList() 메서드는 GetFiles 메서드를 사용해 Resources 폴더 내의 XML파일 리스트를 딕셔너리로 불러오는 역할을 한다.
 

    private void OnGUI()
    {
        storySceneObject.Update();
        storySceneObject.ApplyModifiedProperties();

        GUILayout.Label("Story Scene List", EditorStyles.boldLabel);

        EditorGUILayout.BeginVertical();
        foreach(var item in storySceneList)
        {
            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.LabelField(item.Key);
            if(GUILayout.Button("Edit", GUILayout.Width(100)))
            {
                // Open the XML File in the Editor
                TextAsset asset = AssetDatabase.LoadAssetAtPath<TextAsset>(item.Value);
                if(asset != null)
                {
                    doc = XDocument.Parse(asset.text);
                }

                if(doc != null)
                {
                    XElement data = doc.Root;
                    _storyName = data.Attribute("Name").Value;
                }
                // Open the XML Editor Window
                StorySceneEditor editor = (StorySceneEditor)GetWindow(typeof(StorySceneEditor));
                editor.Init(item.Value);
            }
            if(GUILayout.Button("Remove", GUILayout.Width(60)))
            {
                storySceneList.Remove(item.Key);
                break;
            }
            EditorGUILayout.EndHorizontal();
        }

        // Add Button
        EditorGUILayout.BeginHorizontal();
        _storyName = EditorGUILayout.TextField(_storyName);
        if(GUILayout.Button("Add", GUILayout.Width(60)))
        {
            CreateStoryScene();
            storySceneList = GetStorySceneList();
        }
        EditorGUILayout.EndHorizontal();

        EditorGUILayout.EndVertical();
    }

 
 
아래 그림과 같이 불러올 파일 목록을 윈도우에 띄워준다.

 
Edit을 누르면 딕셔너리 Value로 갖고있던 경로에서 XDocument를 역직렬화해 Story Scene Editor로 넘겨주며 윈도우를 띄워주고, 좌측의 TextField에 이름을 입력하고 Add 버튼을 누르면 새 XML파일을 생성해준다.
 
 
 

using System.Xml.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

public class StorySceneEditor : EditorWindow
{
    public XDocument doc;
    public string docPath;

    [SerializeField]
    public List<GameObject> prefabList;

    [SerializeField]
    public List<Texture2D> imageList;

    private SerializedObject prefabObject;
    private SerializedProperty prefabListProperty;

    private SerializedObject imageObject;
    private SerializedProperty imageListProperty;

    

    public void Init(string docPath)
    {
        this.docPath = docPath;
        this.doc = XDocument.Load(docPath);

        prefabList = GetPrefabList();
        imageList = GetImageList();

        prefabObject = new SerializedObject(this);
        prefabListProperty = prefabObject.FindProperty("prefabList");

        imageObject = new SerializedObject(this);
        imageListProperty = imageObject.FindProperty("imageList");
        
        Show();
    }

    public static void ShowWindow()
    {
        GetWindow<StorySceneEditor>("Story Scene Editor");
    }

    void OnEnable()
    {

    }

    private void OnGUI()
    {
        prefabObject.Update();
        imageObject.Update();
        
        GUILayout.Label("Story Scene Editor", EditorStyles.boldLabel);
        EditorGUILayout.BeginVertical();
        GUILayout.Label("Name", EditorStyles.boldLabel);

        EditorGUILayout.BeginHorizontal();

        GUILayout.TextField(doc.Root.Attribute("Name").Value);
        GUILayout.Button("Save", GUILayout.Width(100));

        EditorGUILayout.EndHorizontal();

        EditorGUILayout.PropertyField(prefabListProperty, true);
        EditorGUILayout.PropertyField(imageListProperty, true);

        EditorGUILayout.EndVertical();

        if(GUILayout.Button("Save"))
        {
            prefabObject.Update();
            imageObject.Update();

            prefabObject.ApplyModifiedProperties();
            imageObject.ApplyModifiedProperties();
            Save();
        }

        prefabObject.ApplyModifiedProperties();
        imageObject.ApplyModifiedProperties();

    }


    private List<GameObject> GetPrefabList()
    {
        List<GameObject> prefabList = new List<GameObject>();

        if(doc != null)
        {
            // Get Prefabs from the XML File, using GUIDs
            foreach(var item in doc.Root.Elements("Prefab"))
            {
                string guid = item.Attribute("Guid").Value;

                GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(guid));
                prefabList.Add(prefab);
            }
        }

        return prefabList;
    }

    private List<Texture2D> GetImageList()
    {
        List<Texture2D> imageList = new List<Texture2D>();

        if(doc != null)
        {
            // Get Images from the XML File, using GUIDs
            foreach(var item in doc.Root.Elements("Image"))
            {
                string guid = item.Attribute("Guid").Value;
                Texture2D image = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(guid));
                imageList.Add(image);
            }
        }

        return imageList;
    }

    private void Save()
    {
        doc.Root.Elements("Prefab").Remove();
        foreach(var item in prefabList)
        {
            if(item == null) continue;

            string name = item.name;
            string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(item));
            XElement prefab = new XElement("Prefab");
            prefab.Add(new XAttribute("Name", name));
            prefab.Add(new XAttribute("Guid", guid));
            doc.Root.Add(prefab);
        }

        doc.Root.Elements("Image").Remove();
        foreach(var item in imageList)
        {
            if(item == null) continue;

            string name = item.name;
            string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(item));
            XElement image = new XElement("Image");
            image.Add(new XAttribute("Name", name));
            image.Add(new XAttribute("Guid", guid));
            doc.Root.Add(image);
        }
        
        doc.Save(docPath);
    }
}

 
 
Edit 버튼을 누르면 해당 XML을 읽고, Prefab과 Image의 GUID를 불러와 리스트에 띄워준다.
 

 
 
+버튼이 먹지 않아서 아래와 같이 수동으로 데이터를 넣어보았다.
 
코드를 다시 보니 Apply 프로퍼티 메서드를 위에서 호출하고 있었다.
멍청한 청년..


<?xml version="1.0" encoding="utf-8"?>
<Data Name="Day0">
  <Day0 />
  <Prefab Name="MB_OKCANCEL" Guid="750e3b84724cd9b48858c5ae2fe82fa5"/>
  <Image Name="Title" Guid="dd28157f4928f234b80e880799fd87e6"/>
</Data>

 

 
잘 뜨는 것을 볼 수 있다.
 

    public void Test()
    {
        StorySceneManager storySceneManager = new StorySceneManager();
        XDocument doc = storySceneManager.GetStoryScene("Day0");

        prefabList = storySceneManager.GetPrefabList(doc);
        imageList = storySceneManager.GetImageList(doc);

        Instantiate(prefabList[0], new Vector3(0, 0, 0), Quaternion.identity);
    }

 
 
대충 테스트코드를 짜보고
 

 
 

 
 
잘된다.
 

BIG