#if UNITY_EDITOR
if (GlobalManager.instance.PlayVeryFirstCutscene)
#else
if(gameDataService.IsVeryFirst())
#endif
//이후 최초 컷씬 시작
public bool IsVeryFirst()
{
var topClearData = GetTopClearData();
return topClearData.chapter == 0 && topClearData.stage == 0;
//아직 클리어한 스테이지가 하나도 없을 경우 VeryFirst로 판정
}
게임 최초 시작 시(IsVeryFirst)
Initialize -> Preloading -> Lobby가 아닌
Initialize -> Preloading -> Game 순서대로 실행하도록 하는 최초 컷씬(Timeline 이용) 기능과,

StageDataContainer에 ChatCard 이벤트들을 삽입할 수 있는 툴을 만들었습니다.
데이터 자체는 크게 중요하지 않고, 이 데이터들을 어떻게 분기로 나눠 이벤트를 판단했느냐:
public class ChatCardEventService
{
private readonly GameObject disposeAttachTarget;
private readonly IChatCardService chatCardService;
private readonly IStageEventSubscriber stageEventSubscriber;
//스테이지 이벤트 구독용
private readonly IStageStateProvider stageStateProvider;
//정지 여부 판단용
private readonly ITriggerTileEventSubscriber triggerTileEventSubscriber;
//트리거 타일 이벤트 구독용
private readonly ISignalSubscriber signalSubscriber;
//Signal 신호 구독용
//이후생략
}
기존의 스테이지 관련 로직을 구독용 인터페이스로 분리해놓은게 많이 그렇게 어렵진 않았습니다.

Inspector에서 수정이 용이하게 CustomEditor 영역까지 건드리는데 챗 GPT의 도움을 매우 크게 받았습니다.
public enum EventType
{
Stage,
Player,
Trigger,
Signal,
}
public class ChatCardEventSet
{
public ChatCardEnum.ID id;
public ChatCardEnum.EventType eventType;
public bool playOnce = true;
public float delay = 0.0f;
public StageEventData stageEventData = new();
public PlayerEventData playerEventData = new();
public SignalEventData signalEventData = new();
public TriggerTileEventData triggerTileEventData = new();
}
ChatCardEventSet의 eventType에 관련없이 모든 Data들을 전부 노출시키면 대체 어떻게 편집을 하겠습니까?
eventType에 따라 노출되는 Data를 다르게 해야 했지요.

Editor에서 Data마다 그려내는 방식을 다르게 하는건 문제가 되지 않았지만, List 프로퍼티의 형식을 유지하면서 그 내부마다 그려지는 방식을 다르게 하는건 쉽지 않았습니다.
chatCardEvents = new(
serializedObject,
chatCardEventList)
{
drawHeaderCallback = rect =>
{
GUI.Label(rect, "Chat Card Events");
},
drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
var element = chatCardEventList.GetArrayElementAtIndex(index);
float line = EditorGUIUtility.singleLineHeight;
float space = 6f;
float y = rect.y + 2;
var idProp = element.FindPropertyRelative("id");
var eventTypeProp = element.FindPropertyRelative("eventType");
var playOnceProp = element.FindPropertyRelative("playOnce");
var delayProp = element.FindPropertyRelative("delay");
float half = rect.width * 0.5f;
// ───── 1줄: id / eventType ─────
EditorGUI.PropertyField(
new Rect(rect.x, y, half - 4, line),
idProp,
GUIContent.none
);
EditorGUI.PropertyField(
new Rect(rect.x + half, y, half, line),
eventTypeProp,
GUIContent.none
);
y += line + space;
// ───── 2줄: playOnce / delay (라벨 + 필드) ─────
float labelHeight = EditorGUIUtility.singleLineHeight;
float fieldHeight = EditorGUIUtility.singleLineHeight;
float spacing = 2f;
float playOnceWidth = rect.width * 0.3f;
float delayWidth = rect.width - playOnceWidth - 4;
// 라벨 줄
EditorGUI.LabelField(
new Rect(rect.x, y, playOnceWidth, labelHeight),
"Play Once"
);
EditorGUI.LabelField(
new Rect(rect.x + playOnceWidth + 4, y, delayWidth, labelHeight),
"Delay"
);
y += labelHeight + spacing;
// 필드 줄
EditorGUI.PropertyField(
new Rect(rect.x, y, playOnceWidth, fieldHeight),
playOnceProp,
GUIContent.none
);
EditorGUI.PropertyField(
new Rect(rect.x + playOnceWidth + 4, y, delayWidth, fieldHeight),
delayProp,
GUIContent.none
);
y += fieldHeight + 6;
// ───── EventData ─────
SerializedProperty dataProp = eventTypeProp.enumValueIndex switch
{
(int)ChatCardEnum.EventType.Stage => element.FindPropertyRelative("stageEventData"),
(int)ChatCardEnum.EventType.Player => element.FindPropertyRelative("playerEventData"),
(int)ChatCardEnum.EventType.Trigger => element.FindPropertyRelative("triggerTileEventData"),
(int)ChatCardEnum.EventType.Signal => element.FindPropertyRelative("signalEventData"),
_ => null
};
if (dataProp != null)
{
float dataHeight = EditorGUI.GetPropertyHeight(dataProp, true);
EditorGUI.PropertyField(
new Rect(rect.x, y, rect.width, dataHeight),
dataProp,
includeChildren: true
);
}
},
elementHeightCallback = index =>
{
var element = chatCardEventList.GetArrayElementAtIndex(index);
var eventTypeProp = element.FindPropertyRelative("eventType");
float height = 0f;
float line = EditorGUIUtility.singleLineHeight;
float space = 6f;
// id / eventType
height += line + space;
// playOnce / delay (라벨 + 필드)
height += (EditorGUIUtility.singleLineHeight * 2) + 6;
SerializedProperty dataProp = eventTypeProp.enumValueIndex switch
{
(int)ChatCardEnum.EventType.Stage => element.FindPropertyRelative("stageEventData"),
(int)ChatCardEnum.EventType.Player => element.FindPropertyRelative("playerEventData"),
(int)ChatCardEnum.EventType.Trigger => element.FindPropertyRelative("triggerTileEventData"),
(int)ChatCardEnum.EventType.Signal => element.FindPropertyRelative("signalEventData"),
_ => null
};
if (dataProp != null)
height += EditorGUI.GetPropertyHeight(dataProp, true);
return height + 8;
},
};
거의 다 GPT가 짜준 코드입니다.
ReordableList의 drawElementalCallback을 사용해 해당 영역 내부의 기본 제공 데이터 레이아웃을 바꾸려면 Rect와 좌표까지 계산해야 하더라구요.
그 때문에 상당히 애먹었습니다.
다음에 할거: 만들어두기만 한 UI들 기본적인 트윈이나 연출 적용해 게임처럼 보이게라도 만들어보기
'무제_LR' 카테고리의 다른 글
| 로비 UI 완성 (0) | 2026.01.11 |
|---|---|
| 로비 UI 개선 - 메인 패널, 스테이지 패널 (0) | 2026.01.11 |
| Signal Trigger, AutoMover 추가 (0) | 2026.01.08 |
| InputProgress(연타), InputQTE 추가 (0) | 2026.01.06 |
| ChatCard(가제) 추가, Runtime Pivot 수정 주의점 (0) | 2026.01.04 |