[Unity] SerializeReference

muchan
muchan
∙∙조회 17

문제

튜토리얼 22단계를 데이터로 만들려는데 단계마다 "완료 조건"이 다 다르다. 타워 3개 놓기, 2초 기다리기, 건물 업그레이드하기 등이 있어서 추상 클래스로 만들고 싶었다.

csharp
[SerializeField] private TutorialCondition completion;  // 안 된다

Unity 기본 직렬화는 다형성을 담지 못한다. 추상 클래스라서 안 되고, 구체 클래스여도 값 복사라서 파생 타입 정보가 날아간다. 우회책 둘 다 별로였다. 조건마다 ScriptableObject로 만들면 .asset 파일이 조건 수만큼 늘어난다. enum + switch로 하면 모든 조건의 필드가 한 클래스에 몰려서 Delay를 골랐는데 인스펙터에 Target Tower 칸이 보인다.

SerializeReference

값이 아니라 참조로 저장하고, 타입 이름을 같이 적어둔다.

csharp
[SerializeReference]
private TutorialCondition completion;

인스펙터에 드롭다운이 뜨고, 고른 타입의 인스턴스가 스텝 에셋 안에 직접 저장된다. 별도 파일이 안 생긴다.

얻은 것 1 — 진행부가 조건을 모른다. 컨트롤러가 하는 일은 이게 전부다.

csharp
_active.Satisfied += OnConditionSatisfied;
_active.Begin(_context);

새 조건 추가 = 클래스 파일 하나 추가. 지금 조건이 14개인데 컨트롤러는 처음 그대로다.

얻은 것 2 — 조건마다 자기 필드만 갖는다. TowerPlacedCondition은 count와 targetTower만, DelayCondition은 seconds와 ignoreGameSpeed만. [Min], [Tooltip] 같은 어트리뷰트도 그대로 먹는다.

대가

1. 클래스 이름이 곧 데이터 키다. rename 하면 저장된 데이터가 깨진다. 그래서 조건 파일 13개 전부 첫 줄에 경고를 박아뒀다.

csharp
// ⚠ 클래스 이름을 바꾸면 [SerializeReference]로 저장된 기존 스텝 데이터가 깨진다.

2. 삭제하면 null로 남는다. 컴파일 에러가 아니라 조용한 런타임 null이다. 리스트 순회할 땐 null 검사가 필수.

3. 인스턴스가 에셋에 눌러앉는다 → 상태가 남는다. MonoBehaviour와 달리 플레이를 껐다 켜도 같은 객체라서, private 필드에 지난 판 값이 그대로 있다.

csharp
public override void Begin(TutorialContext context)
{
    // 조건 객체는 에셋에 저장되므로 이전 플레이의 값이 남아 있다 — 반드시 여기서 다시 잡는다.
    _baseline = CountMatching();

4. GameObject 수명에 안 묶인다. 아무도 정리해주지 않는다. DelayCondition이 CancellationTokenSource를 직접 들고 End()에서 끊는 이유 — 안 끊으면 이미 지나간 단계의 완료 신호가 뒤늦게 날아온다.

결론

[SerializeReference]의 진짜 비용은 Begin**/**End 짝 맞추기 규율이다. MonoBehaviour가 공짜로 해주던 초기화와 정리를 손으로 해야 한다. 그 대가로 파일 폭발도 거대한 switch도 없는 데이터 주도 확장을 얻는다.