다이어로그 데이터 제작용 툴을 먼저 만들기로 했습니다.
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에 선택지 데이터 툴 추가하기
선택지, 다이어로그 툴 윈도우에 공용으로 사용할 이벤트 등장 조건 설정 툴 추가하기
'무제_LR' 카테고리의 다른 글
| 다이어로그 에디터 - csv->Localization 및 TalkingData 툴 완성 (0) | 2025.12.21 |
|---|---|
| 다이어로그 에디터 툴 - 선택지 툴, Dirty 체크 (0) | 2025.12.19 |
| 다이어로그 시스템 및 테이블 기획 (0) | 2025.12.16 |
| 게임 컨셉 결정, 에너지 시스템 추가 (1) | 2025.12.16 |
| 스테이지 별 카메라 설정 (0) | 2025.12.08 |