[Unity] 튜토리얼 설계
![[Unity] 튜토리얼 설계](/_next/image?url=https%3A%2F%2Fdata.develog.develrocket.com%2Fupload%2Fdevelog%2Fuser_1777093328305%2F1787322579554-zopplu%2F_____2026-08-21_232731.png&w=3840&q=75)
댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.
타워 디펜스 게임에 튜토리얼을 붙이면서 정한 구조를 정리해봤습니다. 저번 미니게임 프로젝트 때 만들었던 구조가 아쉬워서 이번엔 클로드와 함께 최대한 확장성이 좋게 만들어봤습니다.
튜토리얼은 이렇게 짜기 쉽다.
switch (currentStep)
{
case 0: // 주민 배치를 기다린다
case 1: // 밤이 되길 기다린다
case 2: // 타워 배치를 기다린다
}
처음엔 잘 돌아간다. 그러다 기획이 "설명 순서를 바꾸자"고 하고, "합성도 알려주자"고 하고, 결국 이 switch가 20개짜리가 된다. 단계 하나 추가할 때마다 진행 로직을 열어야 한다.
그래서 처음부터 다르게 잡았다. 목표는 하나 — 새 단계를 붙이는 비용이 "조건 하나 + 데이터 하나"일 것.
전에 진행했던 미니 게임 프로젝트 튜토리얼을 이런식으로 작성했다...
| 하는 일 | 모르는 것 | |
|---|---|---|
| 진행부 | 단계를 순서대로 넘긴다 | 무엇을 기다리는지 |
| 표시부 | 팝업·말풍선을 그린다 | 지금이 몇 단계인지 |
| 판정부 | "행동했다"를 감지한다 | 튜토리얼의 존재 |
핵심은 모르는 것 칸이다. 셋이 서로를 모르게 만드는 게 설계의 전부다.
하나의 단계는 이렇게 흐른다.
팝업 뜸 → 확인 → 말풍선 남음 → 그 행동을 하면 → 다음 단계
판정부를 교체 가능한 단위로 만들어 완료 조건을 추상 타입 하나로 묶는다.
public abstract class TutorialCondition
{
public event Action Satisfied;
public abstract void Begin(TutorialContext context); // 감시 시작
public abstract void End(); // 감시 종료
protected void Fire() => Satisfied?.Invoke();
}
할 수 있는 건 셋뿐이다. 감시 시작 / 감시 종료 / 됐다고 알리기.
실제 조건은 이렇게 생겼다.
public class BuildingActionCondition : TutorialCondition
{
[SerializeField] private BuildingAction action; // 인스펙터에서 고른다
public override void Begin(TutorialContext ctx)
=> ctx.Management.OnBuildingAction += OnAction;
public override void End()
=> ctx.Management.OnBuildingAction -= OnAction;
private void OnAction(BuildingAsset _, BuildingAction happened)
{
if (happened == action) Fire();
}
}
게임이 원래 쏘던 이벤트를 그대로 구독한다. 튜토리얼을 위해 새 이벤트를 만들지 않았다. "몇 초 뒤에 넘긴다" 같은 타이머도 쓰지 않는다 — 플레이어가 실제로 그 행동을 했을 때만 넘어가야 하니까.
진행부가 조건에 대해 아는 건 두 줄이 전부다.
_active.Satisfied += OnConditionSatisfied; // 됐으면 알려줘
_active.Begin(_context); // 감시 시작해
진행부 코드에 BuildingActionCondition이라는 이름이 한 번도 등장하지 않는다.
단계 데이터는 ScriptableObject로 뺐다. 팝업 문구, 말풍선 문구, 그리고 완료 조건 정보가 들어있다.
[SerializeReference]
private TutorialCondition completion;
[SerializeReference]가 여기서 일을 한다. 일반 [SerializeField]는 추상 타입을 저장하지 못하는데, 이건 실제 타입 이름까지 함께 저장해서 추상 타입 칸에 아무 자식이나 담을 수 있게 한다.
덕분에 인스펙터에서 조건을 드롭다운으로 고르고, 그 아래 그 조건만의 설정값이 따라 나온다. 새 조건 클래스를 만들면 그 목록에 저절로 들어간다.
| 상황 | 비용 |
|---|---|
| "건물 업그레이드도 알려주자" | 코드 0줄 — 기존 조건의 값만 Upgraded로 |
| "타워 배치도 알려주자" | 파일 1개 — 조건 클래스 하나 |
| "순서를 바꾸자" | 코드 0줄 — 리스트 드래그 |
세 경우 모두 진행부·표시부는 열지 않는다.
BuildingActionCondition 하나가 주민 배치·회수·건물 업그레이드·주민 수 증가를 전부 커버한다. 무엇을 기다릴지는 코드가 아니라 인스펙터 값이 정하기 때문이다.
Unity는 단일 필드에 타입 피커를 안 그린다. [SerializeReference]를 걸어도 인스펙터에 라벨만 뜨고 고를 방법이 없다. 리스트로 감싸면 + 버튼으로 고르게 해주는데 단일 필드는 아니다. 작은 PropertyDrawer로 메웠다.
조건 객체는 에셋에 저장된다. 그래서 조건이 상태를 들고 있으면 이전 플레이의 값이 남는다. "타워 2개 배치" 같은 카운터를 Begin에서 초기화하지 않으면 두 번째 플레이 때 시작하자마자 통과해버린다.
Time.timeScale = 0은 입력을 막지 않는다. Update()는 계속 돈다. 팝업이 떠 있는 동안 뒤쪽 클릭을 막는 건 일시정지가 아니라 화면을 덮은 UI다.
지금 만든 건 흐름까지다. 화면을 어둡게 덮고 강조한 대상만 클릭되게 하는 것은 안 넣었다.
이게 생각보다 까다롭다. 입력 처리가 "포인터가 UI 위인가" 하나로 판정하는데, 화면을 덮으면 강조한 타일조차 클릭이 안 된다. 딤에 구멍을 뚫는 방식이 통할 것 같지만 아직 확인 전이라, 그 검증부터 하고 나머지를 쌓을 생각이다.
일단 임시로 패널을 만들고 이미지, 제목, 본문도 임시로 넣었다.
튜토리얼은 3단계로 만들었다.
먼저 튜토리얼에서 기능을 설명해주는 패널을 표시한다
확인 버튼을 누르면 왼쪽에 무슨 일을 하라고 패널이 새로 나온다(나중에 말풍선 느낌으로 설명하려고 한다)
생산 라인 주민을 늘리면
두 번째 튜토리얼이 나온다
마찬가지로 확인을 누르면 말풍선(예정) 패널이 나오고
다시 1을 줄이면
마지막 튜토리얼이 나온다
낮 종료를 하면(오른쪽 다음 버튼)

튜토리얼이 완료되고 몬스터 스폰이 시작되는것을 볼 수 있다.
중간에 행동 제약 기능은 아직 넣지 않았다
정리하면 이 구조는 튜토리얼에서 자주 바뀌는 건 "무엇을 기다리는가"이지 "어떻게 진행하는가"가 아니다. 그 둘을 갈라놓으면 자주 바뀌는 쪽만 건드리게 된다.
SerializeReference
![[Unity] Event Trigger Component](https://data.develog.develrocket.com/upload/develog/user_1777093328305/1786976296699-a3tu75/_____2026-08-17_231353.png)
Unity Event Trigger 컴포넌트

에픽게임즈에서 지원하는 EOS에 대해 알아보았다.
![[Unity] Event Trigger Component](https://data.develog.develrocket.com/upload/develog/user_1777093328305/1786976296699-a3tu75/_____2026-08-17_231353.png)