푸시다운 스택
쌓아 올려 재개 가능한 게임 상태 스코프 — 인터럽트를 위에 푸시하면 실행 중이던 것이 팝될 때까지 그대로 멈춥니다.
게임 위에 인터럽트를 푸시하세요. 실행 중이던 것은 팝될 때까지 그 자리에서 그대로 멈춥니다.
| 인터페이스 | IPushdownStackService — IPushdownStack 인스턴스를 만드는 팩토리 |
| 끄기 스위치 | NullPushdownStackService |
| 어셈블리 | CommonGameSystem.Core |
| 시작 | 부팅 시 자동 등록 (23개 서비스 중 하나) — 별도 설정 불필요 |
하는 일
푸시다운 스택은 스코프라 부르는 게임 상태들의 헤드리스 후입선출(LIFO) 스택입니다. 실행 중인 게임플레이 시뮬레이션(또는 AI, 메뉴)을 잠시 중단했다가 멈춘 그 지점에서 재개할 수 있게 해 줍니다. 스택에 스코프를 푸시하면 — 대화 인터럽트, AI 결정, 논리적 메뉴 — 그 아래에서 살아 움직이던 것이 정확히 그 자리에서 얼어붙습니다. 팝하면 이전 스코프가 아무것도 다시 만들 필요 없이 깨어납니다(세션 내에서).
최상단 스코프만 틱합니다. 일시 중단된 스코프는 재개될 때까지 상태를 얼린 채 유지합니다. 스택은 순수 C#이고 결정론적이며, 그 구조는 저장 / 불러오기 서비스를 통해 저장하고 복원할 수 있습니다.
빠른 시작
서비스 로케이터에서 팩토리를 resolve한 뒤 스택을 만드세요:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyStackUser : MonoBehaviour
{
private IPushdownStack _stack;
private void Awake()
{
_stack = ServiceLocator.Resolve<IPushdownStackService>().Create();
// Or construct one directly — works standalone, with no service registration:
// _stack = new PushdownStack();
}
}
API 레퍼런스
IPushdownStack — 주 인터페이스
| 멤버 | 설명 |
|---|---|
void Push(string key, IStackScope scope) | 스코프를 새 최상단으로 푸시합니다. 이전 최상단이 일시 중단된 뒤 새 스코프에 진입합니다. key는 저장에 쓰이는 라벨입니다. |
void Pop() | 최상단 스코프를 팝합니다: 그 스코프가 종료되고 아래 스코프가 재개됩니다. 빈 스택에서는 no-op입니다. |
void Replace(string key, IStackScope scope) | 최상단 스코프를 제자리에서 교체합니다(이전 것 종료, 새것 진입). 아래 스코프는 계속 일시 중단 상태이며 suspend/resume 호출을 받지 않습니다. |
void Clear() | 위에서 아래로 모든 스코프를 종료하고(아무것도 재개되지 않음) 스택을 비웁니다. |
void Tick(float deltaTime) | 최상단 스코프만 틱합니다. 일시 중단된 스코프는 얼어붙은 채입니다. deltaTime을 여러분이 전달하므로 클록도 여러분이 고릅니다. |
int Depth { get; } | 현재 스택에 있는 스코프의 수. |
IStackScope Top { get; } | 최상단 스코프. 스택이 비어 있으면 null. |
string TopKey { get; } | 최상단 스코프의 키. 스택이 비어 있으면 null. |
string[] GetFrameKeys() | 아래에서 위 순서의 스코프 키. 새 배열을 할당하므로 저장용으로 쓰고, 매 프레임 호출하지 마세요. |
PushdownStackSnapshot TakeSnapshot() | 저장과 복원을 위해 스택 구조를 캡처합니다. |
void Restore(PushdownStackSnapshot snapshot, IScopeFactory factory) | 여러분의 팩토리를 사용해 스냅샷에서 스택을 재구성합니다. 재구성 중에는 enter/suspend/resume 훅이 전혀 발생하지 않습니다 — 조용한 하드 리셋입니다. |
event Action<IStackScope, IStackScope> ScopeChanged | 완료된 각 연산 후 (pushed, popped)와 함께 발생합니다. 전역 이벤트 버스의 메시지가 아니라 평범한 인스턴스별 C# 이벤트입니다. |
IStackScope — 각 스코프마다 여러분이 구현하는 것
| 멤버 | 설명 |
|---|---|
void OnEnter() | 스코프가 푸시될 때 한 번 호출됩니다. |
void OnSuspend() | 다른 스코프가 위에 푸시될 때 호출됩니다(이 스코프는 얼어붙습니다). |
void OnResume() | 위 스코프가 팝될 때 호출됩니다(이 스코프가 다시 활성화됩니다). |
void OnExit() | 스코프가 팝·교체·클리어될 때 한 번 호출됩니다. |
void Tick(float deltaTime) | 이 스코프가 최상단인 동안 매 틱 호출됩니다. 일시 중단된 스코프는 절대 틱하지 않습니다. |
IPushdownStackService — Bootstrap이 등록하는 팩토리
| 멤버 | 설명 |
|---|---|
IPushdownStack Create(PushdownStackOptions options = default) | 주어진 옵션으로 새 스택을 만듭니다. |
int ActiveStackCount { get; } | 아직 살아 있는 팩토리 생성 스택의 수. |
PushdownStackOptions — readonly 설정 구조체
| 멤버 | 설명 |
|---|---|
int EffectiveMaxStackDepth { get; } | Push에 대한 안전 한도. 기본 32, 범위 1–256. |
int EffectiveMaxOpsPerDispatch { get; } | 디스패치당 처리되는 큐잉된 후속 연산의 한도(아래 재진입 함정 참고). 기본 8, 범위 1–64. |
int EffectiveInitialCapacity { get; } | 내부 스코프 리스트의 사전 할당 힌트. 기본 8, 범위 1–256. |
bool LetScopeExceptionsPropagate { get; } | true이면 스코프 훅이 던진 예외가 여러분의 코드로 전파됩니다(fail-fast 개발 모드). false(기본)이면 잡아서 로그하고, 연산은 계속됩니다. |
bool SuppressEmptyPopWarning { get; } | true이면 빈 스택에서의 Pop이 남기는 경고를 끕니다. 추측성으로 팝하는 게임용입니다. 기본 false. |
PushdownStackSnapshot — 저장과 복원용 [Serializable] 구조체
| 멤버 | 설명 |
|---|---|
int schemaVersion | 스냅샷 형식 버전(현재 1). |
string[] frameKeys | 아래에서 위 순서의 스코프 키. 절대 null이 아니며, 빈 스택은 빈 배열로 저장됩니다. |
IScopeFactory — 게임이 구현하며, Restore에만 전달
| 멤버 | 설명 |
|---|---|
IStackScope Create(string key) | 저장된 키의 스코프를 이미 진입된 상태로 재구성합니다. null을 반환하면 그 스코프를 건너뛰며, 건너뜀은 경고로 로그됩니다. |
예제
게임플레이를 일시 중단하는 대화 인터럽트:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Run after the framework has started.
public class GameplayController : MonoBehaviour
{
private IPushdownStack _stack;
void Awake()
{
_stack = ServiceLocator.Resolve<IPushdownStackService>().Create();
}
void Update()
{
RunGameplay();
_stack.Tick(Time.deltaTime);
}
public void StartDialogue(DialogueScope dialogue)
{
_stack.Push("dialogue", dialogue);
// Gameplay is now frozen; only the dialogue ticks.
}
// Called by the dialogue when it closes:
public void EndDialogue()
{
_stack.Pop();
// Gameplay resumes mid-stride.
}
void OnDestroy() => _stack?.Clear(); // Exits every scope top-to-bottom (nothing resumes).
private void RunGameplay()
{
// Your per-frame gameplay update.
}
}
// A simple dialogue scope:
public class DialogueScope : IStackScope
{
public void OnEnter() => Debug.Log("Dialogue opened.");
public void OnSuspend() { } // Freezes naturally: suspended scopes never tick.
public void OnResume() { } // Nothing to do for a self-contained scope.
public void OnExit() => Debug.Log("Dialogue closed.");
public void Tick(float deltaTime)
{
// Advance the dialogue text here.
}
}
끄는 방법
ServiceLocator.Replace<IPushdownStackService>(new NullPushdownStackService());
NullPushdownStack은 호출 코드를 바꾸지 않고 모든 연산을 무음 처리합니다: Depth는 항상 0, Top은 항상 null이고, 모든 변경 호출은 아무것도 하지 않으며, ScopeChanged는 절대 발생하지 않습니다. 헤드리스나 서버 빌드에서, 또는 스택을 쓰지 않을 때 사용하세요. 생성 시 한 번 경고를 로그하므로 활성화 여부를 알 수 있습니다.
흔한 함정
- 메인 스레드 전용입니다.
Push,Pop,Tick을 비롯한 모든 연산은 메인 스레드에서 실행해야 합니다. 워커 스레드 호출은InvalidOperationException을 throw합니다. - 스코프 내부 상태는 여러분의 책임입니다. 스냅샷은 스택 구조만 캡처합니다: 어떤 스코프 키가 어떤 순서인지. 각 스코프의 내부 상태 — 대화의 현재 줄, 상태 머신의 현재 상태 — 는 따로 저장하고(저장 / 불러오기 참고) 여러분의
IScopeFactory.Create(key)에서 재구성해야 합니다. - 훅 안에서 발행한 연산은 지연됩니다. 훅(
OnEnter,OnSuspend,OnResume,OnExit,Tick)이Push,Pop,Replace,Clear를 호출하면, 그 연산은 큐에 들어가 현재 훅이 끝난 뒤 요청된 순서대로 실행됩니다. 디스패치당 최대EffectiveMaxOpsPerDispatch개(기본 8)의 큐잉된 연산이 실행되며, 그 이상은 경고가 로그되고 나머지는 버려집니다. ScopeChanged구독자는 throw하면 안 됩니다. 이 이벤트는 try/catch 없이 연산 중간에 발생합니다.Pop과Clear에서는 새 최상단의OnResume전에 발생합니다 — 구독자가 throw하면 연산이 반쯤 적용된 채 남습니다.- IL2CPP: 자신의 스코프 클래스를 보존하세요. 프레임워크는 자기 타입만 보존합니다. 인터페이스로만 사용되는,
IStackScope나IScopeFactory를 구현한 게임 클래스는 IL2CPP 코드 스트리핑에 제거될 수 있습니다.[Preserve]나 프로젝트link.xml의 항목으로 클래스를 보호하세요. - 재개 후 AI 스코프는 재검증하세요. 대화나 메뉴는 있던 그대로 재개되며, 그것이 맞습니다. 하지만 AI 스코프가 캐시해 둔 세계에 대한 시각 — 마지막으로 본 목표 위치, 실시간 타이머 — 은 중단되어 있던 시간만큼 낡아 있습니다. 그대로 재개하면 캐릭터가 적이 있던 자리로 걸어갈 수 있습니다.
OnResume에서 세계에 대한 가정을 다시 확인하세요.Tick으로 구동되는 타이머는 자연스럽게 멈추므로 리셋이 필요 없습니다. 실시간(벽시계) 타이머만 낡습니다.
관련 페이지
- 부트스트랩 — 서비스가 시작되는 방식
- FSM (상태 머신) — 스코프 안에서 돌릴 평평한 상태 머신
- UI 프레임워크 — 시각적 UI 패널 스택(별개의 무관한 스택)
- 저장 / 불러오기 — 스냅샷 저장