본문 바로가기

무제_LR

다이어로그 에디터 툴 - EditorWindow와 AssetDataBase로 json 생성, 불러오기, 저장, 삭제

 

다이어로그 데이터 제작용 툴을 먼저 만들기로 했습니다.

EditorWindow를 사용해 유니티 내부에서 만들 수 있게 하는게 이후에도 훨씬 편하겠죠.

 

이를 위해 원대한 계획을 세웠으나 설명은 생략하겠습니다.

CustomEditor는 몇번 만져봤으나 EditorWindow까지 사용해본 적은 거의 없어 좋은 공부가 될 것이라고 생각했습니다.

 

Editor 폴더에 곤히 잠들어있는 에디터 스크립트

 

  [MenuItem("Editor Window/Dialogue Editor")]
  public static void OpenWindow()
  {
    EditorWindow wnd = GetWindow<DialogueEditorWindow>();
    wnd.titleContent = new GUIContent(nameof(DialogueEditorWindow));
  }

오브젝트를 클릭하면 따라 나오는 컴포넌트 Inspector를 다루는 CustomEditor와 다르게

EditorWindow는 ScriptableObject마냥 해당 Window를 여는 메뉴를 추가해줘야 합니다.

 

  private void CreateGUI() //혹은 OnEnable()
  {
    //대충 데이터 시작 및 미리 불러오기
  }
  
  private void OnGUI()
  {
    //Window에 나올 놈들 그리는 코드들
    SomethingDrawingMethod()
    //무조건 OnGUI에 통짜로 넣을 필요 없이 메소드 분할해서 그려내도 무방
  }
  
  private void SomethingDrawingMethod()
  {
  	EditorGUILayout.SomethingDrawer();
  }

이 정도는 CustomEditor만 만져봤어도 대충 무슨 구조일지 알겠죠.

 

이번 작업물의 주 메뉴는 AssetDataBase입니다.

챗 GPT야 고마워~~~~~

  private void LoadAllDatas()
  {
    string[] guids = AssetDatabase.FindAssets("t:TextAsset", new[] { FolderPath });
	//FolderPath는 const string값
    //json을 사용할거라 TextAsset 타입의 파일 guid만 가져오도록 설정
    
    foreach (string guid in guids)
    {
      var path = AssetDatabase.GUIDToAssetPath(guid);
      //AssetDataBase에서 바로 가져올 수 있을 줄 알았는데
      //guid를 먼저 가져온 후 해당 guid를 바탕으로 path를,
      var textAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
      //또 path를 바탕으로 에셋을 가져옵니다.
      //GPT야 고마워~~~

      if (textAsset == null)
        continue;

      var fileName = Path.GetFileNameWithoutExtension(path);
      var parts = fileName.Split('_');

      var headNumber = int.Parse(parts[0]);
      var subName = parts[1];
      //데이터 파일 포멧을 00_name 이렇게 잡았기에 개인적으로 분류하는 부분

      var data = JsonUtility.FromJson<DialogueData>(textAsset.text);
	  //textAsset -> string(json) -> 원하는 클래스로 변환해줍니다.
    
      var dataSet = new DataSet(headNumber, subName, data);
      dataSet.guid = guid;
      //어디서 불러왔는지 guid를 저장해줍니다.
      dataSets.Add(dataSet);
      //이후 매핑
    }
  }

  private void CreateData()
  {
    var newDataSet = new DataSet(dataSets.Count, NewDataSetName, new DialogueData());
    //위 영상 List 부분의 + 버튼 클릭 시 호출
    dataSets.Add(newDataSet);
    SaveData(newDataSet);
    //새로 데이터 만들고, 리스트 마지막에 넣고, 에셋으로 저장해줍니다.
    Repaint();
    //혹시 모르니 Window를 Refresh해주는 Repaint까지 호출
  }

  private void SaveData(DataSet dataSet)
  {
    if (dataSet.IsDirty == false)
      return;
      //해당 데이터에 유의미한 변화가 있을 때만 저장을 실행합니다.

    DeleteData(dataSet);
      //해당 데이터의 순서 혹은 이름이 바뀌었을 때가 있겠죠.
      //아쉽게도 AssetDataBase는 같은 guid의 파일을 덮어쓰고 이름만 바꾸는 기능이 없기 때문에
      //기존 guid의 파일을 삭제, 이후 새로운 파일을 생성해야 합니다.

    var filePath = string.Format(FileNameFormat, ParseToHeadNumber(dataSet.Index), dataSet.Name);
    var newPath = Path.Combine(FolderPath, filePath);
    var json = JsonUtility.ToJson(dataSet.Data);
    File.WriteAllText(newPath, json);
    //평범한 json 저장하듯이 저장합니다.
    //생각해보니까 로드하는 것도 걍 IO.File 쓰면 되던거 아닌가???
    //한번 더 생각해보니 TextAsset만 골라서 로드해야 하니 AssetDataBase 쓰는게 맞을지도?
    AssetDatabase.Refresh();
    //파일이 생성되었으면 Refresh를 한번 돌려줍니다.
    //이 과정이 없다면 새로 생성된 파일을 인식하지 못해요.

    dataSet.guid = AssetDatabase.AssetPathToGUID(newPath);
    //그래서 새로 생성된 파일의 guid를 가져오지 못하는 찐빠가 발생할 수 있습니다.
    dataSet.ClearDirty();
    //저장 과정이 끝나면 해당 데이터의 Dirty를 초기화합니다.
  }

  private void DeleteData(DataSet dataSet)
  {
    var path = AssetDatabase.GUIDToAssetPath(dataSet.guid);
    AssetDatabase.DeleteAsset(path);
    //별 과정 없는 삭제 메소드
  }

AssetDataBase를 통해 guid를 가져와 캐싱하니 파일의 불러오기, 이름이 변경된 파일 추적 및 삭제가 상당히 수월했습니다.

 

그리고 데이터들의 순서 인덱스를 유지하는데 큰 공헌을 한 ReorderableList 클래스도 설명을 빼놓을 수가 없습니다.

대충 Inspector에서 노출된 List형 필드를 보면 +, -뿐만 아니라 드래그로 순서까지 바꿀 수 있었죠?

이 필드가 유니티 에디터에서 기본으로 제공하는 ReorderableList입니다.

콜백이 상당히 많은데, 이상하게 유니티 공식 API 홈페이지가 안 보이네요.

아무튼 제가 사용한 콜백만 보여드리겠습니다.

 

    private ReorderableList dialogueDataList;

private void CreateDataList()
  {
    dialogueDataList = new ReorderableList(
      elements: dataSets,			//연결할 소스 List
      elementType: typeof(string),	//소스의 타입
      draggable: true,				//드래그 가능 여부
      displayHeader: false,			//헤더(대충 헤더다 싶은 그거) 여부
      displayAddButton: true,		//+ 버튼 여부
      displayRemoveButton: true);	//- 버튼 여부
	

    dialogueDataList.drawElementCallback = (rect, i, _, __) =>
    {
      EditorGUI.LabelField(rect, dataSets[i].FileName);
    };
    //drawElementCallback: 각 요소들 그려낼 때 커스텀할 수 있는 세팅
    //데이터 이름이 A일 때 A 그대로 표시하기보다 "data:A", "superA" 등 원하는 대로 수정 가능
    //본인의 경우 데이터 클래스의 index와 subname을 합쳐 "01_asdf"로 표시하도록 만듬(FileName)
	//(Rect rect, int index, bool isActive, bool isFocused)를 인자로 받음

    dialogueDataList.onSelectCallback = reordableList =>
    {
      int index = reordableList.index;
      if (index < 0) return;

      var targetDataSet = dataSets[index];
      if(selectedDataSet == targetDataSet) 
        return;

      if (selectedDataSet != null)
        SaveData(selectedDataSet);

      selectedDataSet = targetDataSet;

      Repaint();      
    };
    //onSelectCallback: 무언가 선택되었을 때 호출
    //선택된 요소를 반환하는게 아니라 그냥 '뭐가 선택되었을 때' 호출된다는 것에 주목
    //해당 ReordableList를 그대로 인자로 전달
    //현재 인덱스를 알려주는 reordableList.index를 이용하면 됨
    
    dialogueDataList.onReorderCallback = reordableList =>
    {
      ReorderDataSets();
      foreach(var dataSet in dataSets)
        SaveData(dataSet);

      Repaint();
    };
    //onReorderCallback: 마우스 드래그로 요소 순서를 변경했을 때 호출
    //무엇이 어떻게 변경되었는지 알려주는 놈이 아니니 주의
    //소스 순서 역시 전부 바뀐 상태에서 호출이 되기 때문에
    //이후 바뀐 소스를 어떻게 감지하고 정돈하느냐는 본인의 역할

    dialogueDataList.onAddDropdownCallback = (buttonRect, reordableList) =>
    {
      CreateData();
    };
    //onAddDropdownCallback: + 버튼 눌렀을 때 호출
    //onAddCallback이라는 놈은 따로 있는데, 이 콜백은 요소 추가까지 자동으로 해준다고 한다

    dialogueDataList.onRemoveCallback = reoredableList =>
    {
      var index = reoredableList.index;
      if (index < 0) return;

      var removeData = dataSets[index];
      dataSets.Remove(removeData);
      DeleteData(removeData);

      ReorderDataSets();
      foreach (var dataSet in dataSets)
        SaveData(dataSet);

      if (dataSets.Count > index)
        selectedDataSet = dataSets[index];
      else if (dataSets.Count > 0)
        selectedDataSet = dataSets.Last();

      Repaint();
    };
    //onRemoveCallback: - 버튼 눌렀을 때 호출
    //onRemoveDropDownCallback이라는 콜백은 없었음
  }

 

ReordableList의 header 인자를 false로 설정한 결과

머리가 약간 휑해졌다

 

dialogueDataList.DoLayoutList();
//GUI에서 ReordableList를 활성화시키기

isFoldDataList = EditorGUILayout.Foldout(isFoldDataList, "Dialogue Datas", true);
if (isFoldDataList)
{
	dialogueDataList.DoLayoutList();
}
//폴더블 요소가 직접 붙어있는 것 같지는 않았다.
//EditorGUILayout.Foldout이랑 같이 쓰면 확실히 편해짐

 

일단 AssetDataBase로 첫 단추는 꿰맸습니다.

이후에도 시간이 약간 걸리긴 하겠네요.

 

다음 목표: 

Window에 선택지 데이터 툴 추가하기

선택지, 다이어로그 툴 윈도우에 공용으로 사용할 이벤트 등장 조건 설정 툴 추가하기