건축 시스템 상위 컨트롤러 ( PlacementManager (1) )
목차
1. 시스템 요구 사항
Grid 기반 건축 시스템은 단순히 구조물을 배치하는 기능만으로 구성되지 않는다.
플레이어는 건축 모드에서 구조물을 선택하고, 마우스를 움직여 위치를 지정하며, 클릭을 통해 실제 배치를 수행하게 된다.
또한 필요에 따라 구조물을 회전시키거나, 이미 배치된 구조물을 삭제하거나, 잘못된 배치를 되돌리는 Undo 기능을 사용할 수도 있다.
이러한 기능들은 각각 독립적인 로직을 가지지만, 실제 게임에서는 서로 긴밀하게 연결되어 동작한다.
예를 들어 플레이어가 구조물을 선택하면 미리보기 시스템이 활성화되어야 하고, 마우스를 움직이면 선택 위치가 갱신되어야 하며, 마우스를 클릭하면 실제 구조물 배치 로직이 실행되어야 한다.
또한 이 과정에서 Grid 좌표 계산, 충돌 검사, Undo 시스템, 삭제 모드, 이동 모드 등이 함께 작동해야 한다.
만약 이러한 기능들이 각각 독립적으로 서로를 직접 호출하는 구조로 구현된다면 시스템의 의존성이 복잡하게 얽히게 된다.
입력 처리, Grid 계산, 미리보기 시스템, 실제 구조물 배치 시스템이 서로 강하게 연결되면 코드 유지보수가 어려워지고 시스템 확장성도 크게 떨어지게 된다.
따라서 건축 시스템에서는 이러한 여러 하위 시스템들을 통합적으로 관리하고, 입력 이벤트를 적절한 시스템으로 전달하며, 건축 상태를 관리하는 상위 컨트롤러가 필요하다.
PlacementManager는 바로 이러한 역할을 수행하는 클래스이다.
이 클래스는 건축 시스템 전체의 흐름을 제어하며 다음과 같은 역할을 담당한다.
플레이어 입력 이벤트를 받아 건축 시스템 상태에 전달한다.
건축 상태(BuildingState)를 생성하고 관리한다.
구조물 배치, 제거, 이동과 같은 다양한 건축 동작을 실행한다.
Undo 시스템과 Command 패턴을 통해 건축 작업 기록을 관리한다.
미리보기 시스템과 Grid 시스템을 연결하여 플레이어에게 시각적 피드백을 제공한다.
즉, PlacementManager는 건축 시스템의 실제 로직을 직접 수행하는 클래스라기보다, 여러 하위 시스템을 연결하고 전체 흐름을 제어하는 상위 컨트롤러 역할을 수행하도록 설계된 시스템이다.
2. 흐름도
플레이어 입력
↓
InputManager
↓
PlacementManager
↓
현재 BuildingState
↓
Selection 계산
↓
Preview 업데이트
↓
Command 실행
↓
StructurePlacer
↓
GridData 업데이트
건축 시스템에서 PlacementManager는 모든 시스템의 중심에 위치한다.
플레이어 입력은 InputManager를 통해 전달되며, PlacementManager는 이를 받아 현재 건축 상태(BuildingState)에 전달한다.
BuildingState는 현재 선택 영역을 계산하고, 그 결과를 PlacementPreview 시스템으로 전달하여 플레이어에게 미리보기를 표시한다.
플레이어가 실제 배치를 확정하면 PlacementManager는 Command 객체를 생성하여 실행한다.
이 Command는 실제 구조물을 생성하거나 제거하는 작업을 수행하며, 동시에 GridData에 해당 구조물의 정보를 기록한다.
이러한 구조를 사용하면 건축 시스템의 각 기능이 독립적으로 동작하면서도 PlacementManager를 통해 하나의 흐름으로 통합될 수 있다.
3. 구현
3.1. 시스템 초기화
[SerializeField]private InputManager input;
[SerializeField]private GridManager gridManager;
GridData gridData;
private void Start()
{
gridData = new GridData(gridManager.GridSize);
gridManager.ToggleGrid(false);
input.OnDelete += HandleDeleteAction;
input.OnCancle += () => OnExitPlacementMode?.Invoke();
}PlacementManager는 여러 하위 시스템을 참조하는 구조로 설계되어 있다.
이 클래스는 직접 모든 기능을 구현하는 것이 아니라 각 시스템을 연결하는 역할을 수행하기 때문에 여러 컴포넌트를 필드로 보유하고 있다.
InputManager는 플레이어의 입력을 감지하는 시스템이며, PlacementManager는 이 입력 이벤트를 받아 건축 동작으로 변환한다.
Start 함수는 Unity의 MonoBehaviour 라이프사이클에서 게임 오브젝트가 활성화된 이후 호출되는 초기화 함수이다.
Unity에서는 Awake와 Start 두 가지 초기화 함수가 존재하는데, Awake는 오브젝트 생성 직후 실행되고 Start는 모든 오브젝트의 Awake가 끝난 뒤 실행된다.
PlacementManager에서는 GridManager, InputManager, PlacementPreview와 같은 여러 시스템이 서로 참조 관계를 가지고 있기 때문에 모든 객체 초기화가 완료된 이후 실행되는 Start에서 초기화를 수행하는 것이 더 안전하다.
gridData는 건축 시스템의 핵심 데이터 구조 중 하나이다.
이 객체는 현재 맵에 어떤 구조물이 어떤 Grid 위치에 배치되어 있는지를 관리하는 데이터 컨테이너 역할을 한다.
GridData는 GridManager의 GridSize 정보를 기반으로 초기화된다.
이는 건축 시스템이 사용할 Grid 영역의 크기를 결정하는 과정이다.
gridManager.ToggleGrid(false)는 건축 모드가 시작되지 않은 상태에서 Grid 시각화를 비활성화하기 위한 코드이다.
건축 모드에서는 Grid가 플레이어에게 배치 기준을 제공하는 시각적 가이드 역할을 하지만, 일반 플레이 상태에서는 Grid가 필요하지 않기 때문에 기본적으로 숨겨진 상태로 시작하도록 설계하였다.
또한 이 함수에서는 InputManager의 이벤트를 구독하는 작업이 수행된다.
input.OnDelete += HandleDeleteAction;이 코드는 InputManager에서 발생하는 삭제 입력 이벤트를 PlacementManager가 처리하도록 연결하는 코드이다.
C#에서는 이벤트 기반 시스템을 구성할 때 delegate와 event 키워드를 사용한다.
event에 메서드를 연결하는 방식은 Observer 패턴과 유사하게 동작하며, 입력 시스템과 건축 시스템 사이의 의존성을 크게 줄일 수 있다.
이 구조 덕분에 PlacementManager는 Unity Input API를 직접 호출하지 않고도 입력을 처리할 수 있게 된다.
3.2. 건축 상태 생성
[SerializeField]private ItemDataBaseSO structuresData;
[SerializeField]private PlacementPreview pp;
BuildingState buildingState;
public void StartPlacingObject(int id)
{
CancelMovement();
if (buildingState != null)
CancelState();
itemData = structuresData.GetItemWithID(id);
if (itemData == null)
{
Debug.LogError($"{id}에 해당 항목 없음");
}
buildingState = new PlacingObjectsState(gridManager, gridData, itemData);
buildingState.OnFinished += TryPlacingObjects;
buildingState.OnSelectionChanged += MovePreview;
gridManager.ToggleGrid(true);
pp.StartShowingPreview(itemData.previewObject);
ConnectInputToBuildingState();
}ItemDataBaseSO는 ScriptableObject 기반 데이터베이스로, 건축 가능한 구조물 데이터를 관리한다.
ScriptableObject는 Unity에서 데이터 자산을 독립적인 에셋으로 관리할 수 있도록 하는 기능이며, 코드와 데이터를 분리하는 데 매우 유용하다.
이 방식을 사용하면 새로운 건축 오브젝트를 코드 수정 없이 데이터만 추가하여 확장할 수 있다.
PlacementPreview는 구조물 배치 전에 화면에 표시되는 프리뷰 오브젝트를 관리하는 시스템이다.
플레이어가 마우스를 움직이면 프리뷰 오브젝트도 함께 이동하며 배치 가능 여부를 시각적으로 표시한다.
StartPlacingObject 함수는 건축 모드에서 새로운 구조물 배치를 시작할 때 호출되는 함수이다.
먼저 CancelMovement를 호출하여 현재 이동 상태가 활성화되어 있다면 이를 종료한다.
건축 시스템에서는 배치 상태와 이동 상태가 동시에 활성화되면 충돌이 발생할 수 있기 때문에, 새로운 배치를 시작하기 전에 기존 상태를 정리하는 과정이 필요하다.
그 다음 CancelState를 호출하여 기존의 건축 상태를 종료한다.
이 과정을 통해 시스템이 항상 하나의 상태만 유지하도록 보장할 수 있다.
이후 structuresData에서 해당 ID에 해당하는 구조물 데이터를 가져온다.
ItemDataBaseSO는 ScriptableObject 기반 데이터베이스로, 건축 시스템에서 사용할 구조물 정보들을 관리한다.
ScriptableObject를 사용하면 데이터를 코드와 분리하여 Unity 에디터에서 관리할 수 있기 때문에 게임 밸런스 조정이나 데이터 수정이 훨씬 쉬워진다.
그 다음 PlacingObjectsState 객체를 생성한다.
BuildingState는 건축 시스템에서 상태 패턴(State Pattern)을 구현하기 위한 추상 클래스이며, PlacingObjectsState는 실제 구조물 배치 상태를 담당하는 구체적인 상태 클래스이다.
이 구조를 사용하면 건축 시스템의 동작을 상태별로 분리할 수 있다.
예를 들어 구조물 배치 상태, 삭제 상태, 이동 상태가 서로 다른 로직을 가지더라도 동일한 인터페이스를 통해 관리할 수 있다.
이후 다음 두 이벤트를 연결한다.
buildingState.OnFinished
buildingState.OnSelectionChangedOnSelectionChanged는 마우스 이동으로 선택 영역이 변경될 때 호출되며, MovePreview 함수를 통해 미리보기 오브젝트 위치를 갱신한다.
OnFinished는 플레이어가 선택을 확정했을 때 호출되며, 실제 구조물 배치 로직을 실행한다.
마지막으로 Grid 시각화를 활성화하고, PlacementPreview 시스템을 통해 구조물 미리보기를 화면에 표시한다.
3.3. 입력과 상태 시스템 연결
private void ConnectInputToBuildingState()
{
input.OnMousePressed += HandleSelectionStarted;
input.OnMouseReleased += HandleSelectionFinished;
input.OnCancle += CancelState;
input.OnUndo += TryUndoLastPlacement;
input.OnRotate += HandleRotation;
}이 함수는 InputManager에서 발생하는 입력 이벤트를 현재 건축 상태(BuildingState)에 연결하는 역할을 한다.
입력 시스템은 InputManager에 집중되어 있으며, PlacementManager는 해당 이벤트를 구독하여 실제 게임 로직을 실행한다.
이 구조의 장점은 입력 시스템이 변경되더라도 건축 시스템의 다른 부분을 수정할 필요가 없다는 것이다.
예를 들어 기존 키보드 입력 대신 새로운 입력 시스템(Unity New Input System)을 도입하더라도 InputManager 내부만 수정하면 된다.
또한 ConnectInputToBuildingState 함수는 건축 상태가 변경될 때마다 호출되도록 설계되어 있다.
이는 각 상태가 입력을 독립적으로 처리할 수 있도록 하기 위한 구조이다.
3.4. 회전 입력 처리
private void HandleRotation(int modifier)
{
buildingState.HandleRotation(modifier);
OnRotate?.Invoke();
}이 함수는 InputManager로부터 전달된 회전 입력을 처리하는 역할을 한다.
modifier 값은 회전 방향을 나타내는 값으로 사용된다.
예를 들어 -1은 왼쪽 회전을 의미하고 1은 오른쪽 회전을 의미한다.
PlacementManager는 회전 로직을 직접 처리하지 않고, 현재 활성화된 BuildingState에 이를 전달한다.
이러한 구조는 상태 패턴의 핵심 개념을 잘 보여준다.
즉 PlacementManager는 입력을 전달하는 역할만 수행하고, 실제 동작은 상태 객체가 담당하도록 설계되어 있다.
3.5. 건축 상태 종료
public void CancelState()
{
if (buildingState == null)
return;
buildingState = null;
input.OnMousePressed -= HandleSelectionStarted;
input.OnMouseReleased -= HandleSelectionFinished;
input.OnCancle -= CancelState;
input.OnUndo -= TryUndoLastPlacement;
input.OnRotate -= HandleRotation;
commandManager.ClearCommandsList();
pp.StopShowingPreview();
gridManager.ToggleGrid(false);
}CancelState 함수는 현재 건축 상태를 종료하고 시스템을 초기 상태로 되돌리는 역할을 수행한다.
먼저 현재 상태 객체를 제거하고, InputManager에 연결된 입력 이벤트를 모두 해제한다.
이러한 이벤트 해제 과정은 매우 중요하다.
C# 이벤트 시스템에서 이벤트 구독을 해제하지 않으면 객체가 계속 이벤트를 수신하게 되어 예상치 못한 동작이나 메모리 문제가 발생할 수 있다.
그 다음 CommandManager의 명령 기록을 초기화한다.
이는 Undo 시스템이 이전 건축 작업 기록을 유지하지 않도록 하기 위한 처리이다.
마지막으로 PlacementPreview를 종료하고 Grid 시각화를 비활성화한다.
이 과정은 건축 모드가 종료될 때 플레이어 화면에서 건축 관련 UI와 시각 요소를 모두 제거하기 위한 것이다.
4. 개발 의도
PlacementManager의 핵심 설계 의도는 건축 시스템의 모든 흐름을 하나의 중앙 컨트롤러에서 관리하도록 만드는 것이다.
건축 시스템은 입력 처리, Grid 좌표 계산, 구조물 배치, 삭제 시스템, Undo 시스템, 미리보기 시스템 등 여러 기능이 동시에 작동하는 복잡한 구조를 가진다.
이러한 시스템을 각각 독립적으로 연결하면 코드 의존성이 복잡해지고 유지보수가 어려워질 수 있다.
PlacementManager는 이러한 문제를 해결하기 위해 건축 시스템의 상위 제어 계층으로 설계되었다.
이 클래스는 직접 모든 로직을 수행하기보다는 입력 시스템, Grid 시스템, 미리보기 시스템, 구조물 배치 시스템을 연결하고 전체 흐름을 조정하는 역할을 수행한다.
또한 BuildingState를 통해 상태 패턴을 적용하여 배치 상태, 삭제 상태, 이동 상태를 독립적인 로직으로 분리하였다.
이 구조 덕분에 새로운 건축 기능이 추가되더라도 PlacementManager를 크게 수정하지 않고 새로운 상태 클래스를 추가하는 방식으로 시스템을 확장할 수 있다.
결과적으로 PlacementManager는 건축 시스템의 핵심 로직을 직접 수행하는 클래스가 아니라, 여러 하위 시스템을 연결하고 전체 동작 흐름을 관리하는 오케스트레이터(Orchestrator) 역할을 수행하도록 설계된 시스템이다.
