건축 Undo 시스템 ( PlacementManager (4) )
1. 시스템 요구 사항
Grid 기반 건축 시스템에서는 플레이어가 구조물을 배치하거나 제거하는 과정에서 맵의 상태가 지속적으로 변경된다.
이러한 작업은 대부분 실시간으로 이루어지며, 플레이어의 의도와 다르게 수행되는 경우도 빈번하게 발생한다.
따라서 사용자가 이전 상태로 되돌릴 수 있는 Undo 기능은 선택적인 기능이 아니라 필수적인 시스템 요소이다.
본 건축 시스템은 단순히 개별 오브젝트를 하나씩 수정하는 구조가 아니라, SelectionResult를 기반으로 다수의 Grid 셀을 하나의 작업 단위로 처리한다.
예를 들어 플레이어가 드래그를 통해 여러 개의 셀을 선택하고 구조물을 배치하는 경우, 해당 작업은 내부적으로 하나의 명령으로 처리된다.
따라서 Undo 기능 역시 단일 오브젝트 단위가 아니라 작업 단위로 동작해야 하며, 하나의 Undo 요청으로 해당 작업 전체가 되돌려질 수 있어야 한다.
또한 본 시스템에서 구조물의 상태는 두 가지 계층으로 관리된다.
하나는 씬에 존재하는 실제 GameObject이며, 다른 하나는 GridData를 기반으로 관리되는 논리적 점유 상태이다.
Undo 기능은 이 두 계층을 동시에 복원해야 하며, 어느 한쪽만 복원되는 경우 시스템의 상태 불일치가 발생하게 된다.
예를 들어 GameObject만 제거되고 Grid 데이터가 유지되면 해당 위치는 여전히 점유된 것으로 판단되며, 반대로 Grid 데이터만 제거되면 실제 씬과 논리 상태가 분리되는 문제가 발생한다.
이러한 요구사항을 만족하기 위해 Undo 시스템은 상태를 직접 저장하는 방식이 아니라, 건축 동작 자체를 객체로 캡슐화하여 관리하는 Command Pattern 기반으로 설계되었다.
각 건축 동작은 독립적인 명령 객체로 표현되며, 이 객체는 자신의 실행과 복원 로직을 동시에 포함한다.
실행된 명령은 스택 구조로 관리되며, Undo 요청이 발생할 경우 가장 최근의 명령이 역순으로 실행되어 이전 상태를 복원한다.
결과적으로 본 Undo 시스템은 단순한 되돌리기 기능이 아니라, Grid 기반 다중 선택 작업과 데이터-오브젝트 동기화를 모두 고려한 행위 기반 상태 복원 시스템으로 동작해야 한다.
2. 흐름도
플레이어 입력
↓
InputManager
↓
PlacementManager
↓
CommandManager.Invoke()
↓
Command 실행
↓
명령 스택에 기록
↓
Undo 요청
↓
CommandManager.Undo()
↓
마지막 명령 취소
↓
Grid 데이터 복원
↓
GameObject 상태 복원
건축 시스템에서 구조물 배치나 제거와 같은 동작이 발생하면 PlacementManager는 해당 작업을 직접 실행하지 않고 CommandManager를 통해 실행한다.
CommandManager는 명령 객체를 실행한 뒤 이를 내부 스택에 저장한다.
이후 플레이어가 Undo 입력을 수행하면 CommandManager는 가장 최근에 실행된 명령을 스택에서 꺼내 해당 명령의 Undo 동작을 수행한다.
이 구조를 사용하면 건축 시스템에서 발생하는 모든 동작을 동일한 방식으로 관리할 수 있으며, Undo 기능을 매우 안정적으로 구현할 수 있다.
3. 구현
3.1. Command 시스템 구조
CommandManager commandManager = new CommandManager();PlacementManager 내부에서 생성되는 이 객체는 건축 시스템에서 발생하는 모든 동작을 기록하고 관리하는 핵심 컴포넌트이다.
이 시스템에서 중요한 점은 Undo를 위해 별도의 상태를 저장하지 않는다는 것이다.
대신 CommandManager는 실행된 동작 자체를 저장한다.
이는 결과 상태가 아니라 행위 자체를 기록하는 방식이다.
CommandManager 내부에서는 명령 객체들을 스택 구조로 관리한다.
스택은 후입선출 구조이기 때문에, 가장 마지막에 실행된 명령을 가장 먼저 되돌릴 수 있다.
이 방식은 Undo 시스템에 매우 적합하다.
예를 들어 플레이어가 구조물을 A → B → C 순서로 배치했다면, Undo를 실행했을 때 C → B → A 순서로 되돌아가야 한다.
이 동작을 자연스럽게 구현하기 위해 스택 구조를 사용한다.
또한 이 구조는 상태를 복제하거나 저장하지 않기 때문에 메모리 사용량 측면에서도 효율적이다.
각 명령 객체는 자신이 수행한 작업을 되돌릴 수 있는 최소한의 정보만을 가지고 있기 때문이다.
3.2. 명령 실행 구조
commandManager.Invoke(
new StructurePlacementCommand(
this,
buildingState.CurrentPlacementData,
itemData,
selectionResult
)
);이 코드는 구조물 배치를 실행하는 부분이지만, 실제로는 “실행 요청”에 가깝다.
PlacementManager는 구조물을 직접 생성하지 않고, StructurePlacementCommand라는 명령 객체를 생성하여 CommandManager에 전달한다.
이때 StructurePlacementCommand 객체는 단순한 데이터가 아니라 “실행 가능한 객체”이다.
이 객체 내부에는 Execute와 Undo 두 가지 기능이 함께 정의되어 있다.
CommandManager의 Invoke 함수가 호출되면 내부적으로 다음과 같은 순서로 동작한다.
먼저 전달된 명령 객체의 Execute 함수가 호출된다.
이 과정에서 실제 구조물 배치가 수행되며, 내부적으로는 PlacementManager의 PlaceStructureAt 함수가 호출되어 GameObject 생성과 Grid 데이터 기록이 동시에 이루어진다.
이후 Execute가 정상적으로 완료되면 해당 명령 객체는 CommandManager 내부 스택에 저장된다.
이 시점에서 해당 동작은 Undo 대상이 된다.
이 구조의 핵심은 PlacementManager가 “실행 로직”을 직접 가지지 않는다는 점이다.
모든 실행은 Command 객체를 통해 이루어지며, PlacementManager는 이를 생성하고 전달하는 역할만 수행한다.
이 설계를 통해 실행 로직과 관리 로직이 분리되며, Undo 기능을 자연스럽게 통합할 수 있다.
3.3. Undo실행
public void TryUndoLastPlacement()
{
if (commandManager.Undo() == false)
Debug.Log("되돌릴 수 있는 명령이 없습니다.");
else
OnUndo?.Invoke();
if (commandManager.GetCommandsCount() <= 0)
OnToggleUndo?.Invoke(false);
}이 함수는 플레이어가 Undo 입력을 수행했을 때 호출되는 진입점이다.
InputManager에서 Ctrl + Z 입력이 발생하면 PlacementManager는 이 함수를 실행한다.
핵심 동작은 'commandManager.Undo()' 이다.
이 함수는 내부 스택에서 가장 최근에 실행된 명령 객체를 꺼낸 뒤, 해당 명령의 Undo 함수를 호출한다.
이때 중요한 점은 CommandManager가 “무엇을 되돌리는지” 알 필요가 없다는 것이다.
CommandManager는 단순히 명령 객체를 꺼내고 Undo를 호출할 뿐이며, 실제 되돌리기 로직은 각 명령 객체 내부에 정의되어 있다.
예를 들어 StructurePlacementCommand의 경우 Undo가 호출되면 내부적으로 구조물을 제거하는 로직이 실행된다.
반대로 StructureRemoveCommand의 경우 Undo가 호출되면 제거되었던 구조물이 다시 생성된다.
즉, Undo는 단순히 반대 동작이 아니라, 각 명령 객체가 정의한 복원 로직을 실행하는 과정이다.
Undo가 성공적으로 수행되면 OnUndo 이벤트가 호출된다.
OnUndo?.Invoke();이 이벤트는 UI 시스템과 연결되어 있으며, Undo 애니메이션이나 효과를 트리거하는 데 사용된다.
이 구조를 통해 게임 로직과 UI 로직이 분리된다.
마지막으로 명령 스택이 비어 있는 경우 Undo 버튼을 비활성화한다.
if (commandManager.GetCommandsCount() <= 0)
OnToggleUndo?.Invoke(false);이 코드는 단순 UI 제어가 아니라, 시스템 상태를 UI에 반영하는 역할을 한다.
Undo가 더 이상 불가능한 상태를 사용자에게 명확히 전달하기 위한 설계이다.
4. 개발 의도
건축 Undo 시스템의 핵심 설계 의도는 동작을 상태가 아닌 행위 단위로 관리하는 것이다.
일반적인 방식으로 Undo를 구현하면 이전 상태를 저장해야 하기 때문에 메모리 사용량이 증가하고, 상태 관리가 복잡해진다.
하지만 Command Pattern을 사용하면 각 동작을 객체로 캡슐화할 수 있고, 해당 객체가 자신의 Undo 로직을 직접 가지게 된다.
이 구조를 통해 시스템은 다음과 같은 장점을 가지게 된다.
먼저, 모든 건축 동작을 동일한 방식으로 처리할 수 있다.
구조물 배치, 제거, 교체와 같은 서로 다른 동작들도 모두 Command 객체로 표현되기 때문에 일관된 흐름을 유지할 수 있다.
또한 Undo 기능이 자연스럽게 통합된다.
각 명령 객체가 자신의 Undo 로직을 가지고 있기 때문에 별도의 복잡한 상태 저장 없이도 이전 상태로 정확하게 되돌릴 수 있다.
마지막으로 확장성이 크게 향상된다.
새로운 건축 기능이 추가되더라도 기존 시스템을 수정할 필요 없이 새로운 Command 클래스만 추가하면 된다.
이 Undo 시스템은 단순한 되돌리기 기능보다, 건축 시스템 전체를 명령 단위로 관리하는 구조를 만들어내는 핵심 아키텍처로 설계되었다.
