건축 선택 처리 시스템 ( PlacementManager (6) )
1. 시스템 요구 사항
Grid 기반 건축 시스템에서 플레이어의 입력은 단순히 마우스 위치를 읽는 것만으로 끝나지 않는다.
플레이어가 마우스를 누르고, 움직이고, 놓는 과정은 건축 시스템 안에서는 각각 다른 의미를 가진다.
마우스를 누르는 순간은 선택의 시작점이 되고, 마우스를 움직이는 동안에는 선택 범위가 계속 갱신되며, 마우스를 놓는 순간에는 선택이 확정되어 실제 배치, 제거, 이동과 같은 다음 단계의 로직으로 이어지게 된다.
이때 중요한 점은 플레이어가 입력한 좌표를 그대로 사용하는 것이 아니라, 이를 건축 시스템이 이해할 수 있는 선택 정보로 변환해야 한다는 것이다.
예를 들어 화면에서 클릭한 위치는 월드 좌표로 변환된 뒤 다시 Grid 좌표로 해석되어야 하며, 현재 상태가 배치 모드인지 제거 모드인지 이동 모드인지에 따라 동일한 입력도 다르게 처리되어야 한다.
또한 선택 결과는 단순 좌표 하나가 아니라, 선택된 Grid 위치 목록, 실제 프리뷰 위치, 회전 정보, 배치 가능 여부까지 포함하는 데이터 형태로 정리되어야 이후 시스템이 안정적으로 동작할 수 있다.
또한 건축 시스템은 UI와 동시에 동작하기 때문에, UI 위에서 발생한 입력을 건축 선택으로 잘못 해석하지 않도록 막아야 한다.
플레이어가 버튼을 클릭했는데 건축 선택까지 함께 처리된다면 시스템 사용성이 크게 떨어지게 된다.
따라서 선택 처리 시스템은 입력을 받는 것뿐만 아니라, 그 입력이 실제 건축 선택으로 해석되어도 되는 상황인지까지 판단해야 한다.
결국 건축 선택 처리 시스템은 단순한 입력 전달 계층이 아니라, 입력 데이터를 건축 시스템이 사용할 수 있는 선택 결과로 변환하고, 현재 상태에 맞는 선택 의미를 부여하며, 실행 가능한 데이터로 정리해 주는 해석 계층으로 동작해야 한다.
2. 흐름도
마우스 입력
↓
InputManager.GetSelectedMapPosition()
↓
PlacementManager.HandleSelectionStarted()
↓
BuildingState.HandleSelectionStarted()
↓
Update() 동안 HandleSelectionChanged()
↓
SelectionData 누적 및 갱신
↓
PlacementManager.HandleSelectionFinished()
↓
BuildingState.HandleSelectionFinished()
↓
SelectionResult 생성
↓
배치 / 제거 / 이동 시스템으로 전달
건축 선택 처리 시스템의 핵심 흐름은 입력을 곧바로 실행으로 넘기지 않고, 먼저 선택 데이터로 해석한 뒤 확정하는 것이다.
플레이어가 마우스를 누르면 선택의 시작점이 설정된다.
이때 PlacementManager는 직접 선택 좌표를 계산하지 않고, 현재 활성화된 BuildingState에 시작 위치를 전달한다.
이후 마우스가 움직이는 동안 Update 함수가 매 프레임 현재 위치를 받아 선택 상태를 계속 갱신한다.
이 과정에서 SelectionData 내부에는 선택된 Grid 좌표, 프리뷰 위치, 회전 정보, 배치 가능 여부 같은 정보가 계속 누적되고 수정된다.
마우스를 놓으면 선택이 완료되며, BuildingState는 내부적으로 정리해 두었던 SelectionData를 기반으로 SelectionResult를 만든다.
이 결과는 이후 배치 시스템, 제거 시스템, 이동 시스템으로 전달되어 실제 실행 단계로 이어진다.
즉 이 시스템은 입력을 선택으로 해석하고, 선택을 결과 데이터로 정리해서 다음 계층으로 넘기는 중간 처리 계층이라고 볼 수 있다.
3. 구현
3.1. 선택 시작 처리
private void HandleSelectionStarted()
{
if (input.IsInteractingWithUI())
return;
if (buildingState == null)
throw new Exception("buildingState is null. Check if it is initialized.");
buildingState.HandleSelectionStarted(input.GetSelectedMapPosition());
}이 함수는 플레이어가 마우스 버튼을 눌렀을 때 호출되며, 건축 선택의 시작 지점을 결정하는 역할을 한다.
건축 시스템에서 선택은 단번에 완성되는 것이 아니라 '시작점 → 진행 → 확정'의 흐름으로 처리되기 때문에, 이 함수는 그 첫 번째 단계에 해당한다.
가장 먼저 수행하는 일은 UI와의 입력 충돌을 막는 것이다.
if (input.IsInteractingWithUI()) return;이 코드는 현재 마우스 포인터가 UI 위에 있는지를 검사하고, 그렇다면 건축 선택을 시작하지 않는다.
이 판단은 InputManager 안에서 EventSystem.current.IsPointerOverGameObject()를 사용해 수행되는데, Unity의 EventSystem은 현재 입력이 UI와 상호작용 중인지 확인할 수 있게 해 주는 시스템이다.
이 기능을 사용하면 UI 버튼 클릭과 게임 월드 클릭을 분리할 수 있다.
장점은 구현이 단순하고 신뢰성이 높다는 점이고, 단점은 EventSystem이 제대로 존재하지 않거나 UI 이벤트 체계가 별도로 구성되어 있는 경우 추가 처리가 필요할 수 있다는 점이다.
하지만 일반적인 Unity UI 구조에서는 가장 안정적인 방법이기 때문에 이 방식을 사용한 것으로 볼 수 있다.
그 다음에는 현재 건축 상태가 존재하는지를 검사한다.
if (buildingState == null)
throw new Exception("buildingState is null. Check if it is initialized.");이 코드는 선택 처리 시스템이 반드시 특정 상태 위에서만 동작하도록 강제하는 역할을 한다.
배치 상태도 아니고 제거 상태도 아닌데 선택 로직이 실행된다면, 시스템은 선택 결과를 어떤 의미로 해석해야 하는지 알 수 없다.
그래서 여기서는 단순히 return으로 넘어가는 대신 예외를 발생시켜 문제를 명확하게 드러내고 있다.
C#의 Exception은 비정상적인 흐름을 강제로 드러내기 위한 메커니즘이다.
장점은 잘못된 상태를 조기에 발견하기 쉽다는 점이고, 단점은 실제 빌드 환경에서는 예외가 사용자 경험을 해칠 수 있다는 점이다.
하지만 이 코드는 개발 단계에서 상태 초기화 누락을 빠르게 찾아내기 위한 방어 코드라는 의미가 크다.
마지막으로 실제 선택 시작 처리를 현재 상태 객체에 위임한다.
buildingState.HandleSelectionStarted(input.GetSelectedMapPosition());이 한 줄이 이 구조의 핵심이다.
PlacementManager가 직접 선택 계산을 하지 않고, 현재 활성화된 BuildingState에 월드 좌표를 전달한다.
input.GetSelectedMapPosition()은 InputManager 내부에서 Raycast를 통해 얻은 실제 맵 위의 월드 좌표를 반환하는 함수이다.
즉 이 함수는 입력을 위치로 해석한 결과를 받는 것이고, BuildingState는 그 위치를 상태에 맞는 선택의 시작점으로 해석한다.
이렇게 위임 구조를 사용하면 동일한 입력이라도 상태에 따라 다른 의미를 부여할 수 있다.
예를 들어 배치 상태에서는 배치 시작점, 제거 상태에서는 제거 대상 시작점, 이동 상태에서는 이동 대상 선택 시작이 될 수 있다.
이것이 상태 패턴을 선택 처리에 결합한 이유다.
3.2. 선택 진행 처리
void Update()
{
if (buildingState == null || input.IsInteractingWithUI())
return;
buildingState.HandleSelectionChanged(input.GetSelectedMapPosition());
}이 함수는 플레이어가 마우스를 움직이는 동안 선택 상태를 계속 갱신하는 역할을 한다.
건축 선택 시스템에서 가장 중요한 특징 중 하나는 선택이 고정된 결과가 아니라 진행 중인 상태라는 점이다.
마우스를 누른 뒤 드래그하는 동안 프리뷰 위치가 바뀌고 선택 범위가 계속 달라지기 때문에, 이 정보를 실시간으로 다시 계산해야 한다.
이 로직이 Update에 들어가 있는 이유는 Unity의 입력과 화면 갱신이 기본적으로 프레임 단위로 이루어지기 때문이다.
Update는 MonoBehaviour에서 매 프레임 호출되는 대표적인 생명주기 함수이며, 마우스 위치처럼 지속적으로 변하는 입력을 처리할 때 가장 일반적으로 사용된다.
장점은 입력과 화면 반응을 실시간으로 맞출 수 있다는 점이고, 단점은 매 프레임 호출되기 때문에 불필요한 연산이 많아지면 성능 부담이 생길 수 있다는 점이다.
하지만 선택 시스템은 마우스 움직임에 즉각 반응해야 하므로 Update가 가장 자연스러운 위치라고 판단했다.
함수의 첫 줄에서는 현재 상태가 존재하지 않거나, 입력이 UI 위에서 발생한 경우를 제외한다.
if (buildingState == null || input.IsInteractingWithUI())
return;이 조건은 선택 계산을 수행해도 의미가 없는 상황을 걸러내는 필터 역할을 한다.
상태가 없으면 선택을 해석할 기준이 없고, UI 위에 있으면 건축 선택으로 처리하면 안 되기 때문이다.
이후 실제 현재 위치를 상태 객체에 전달한다.
buildingState.HandleSelectionChanged(input.GetSelectedMapPosition());이 코드는 현재 마우스 위치를 상태 객체가 해석하여 선택 데이터를 갱신하도록 한다.
여기서 중요한 점은 PlacementManager가 선택 범위를 직접 계산하지 않는다는 것이다.
이 함수는 오직 현재 위치를 전달하는 역할만 수행하고, 실제로 어떤 Grid를 선택할지, 프리뷰는 어디에 그릴지, 배치 가능 여부를 어떻게 계산할지는 BuildingState 내부에 맡긴다.
이 구조의 장점은 선택 처리 로직을 상태별로 분리할 수 있다는 점이다.
배치 상태에서는 드래그 범위 계산이 필요할 수 있고, 제거 상태에서는 제거 가능한 대상 검사 중심으로 동작할 수 있으며, 이동 상태에서는 단일 구조물만 추적하면 된다.
동일한 HandleSelectionChanged 호출이지만 상태마다 전혀 다른 동작을 하게 되는 것이다.
3.3. 선택 완료 처리
private void HandleSelectionFinished()
{
if (buildingState == null)
return;
if (input.IsInteractingWithUI())
{
buildingState.SelectionData.Clear();
}
buildingState.HandleSelectionFinished();
}이 함수는 플레이어가 마우스 버튼을 놓았을 때 호출되며, 선택을 확정하는 역할을 한다.
즉, 시작과 진행 단계를 거쳐 축적된 선택 데이터를 최종 결과로 정리하는 단계라고 볼 수 있다.
먼저 상태가 존재하는지를 검사한다.
if (buildingState == null)
return;이 코드는 선택 완료 시점에 유효한 상태가 없는 경우를 안전하게 무시하기 위한 처리이다.
시작 단계에서는 예외를 던져도 되는 상황이었지만, 완료 단계에서는 사용자가 마우스를 놓는 순간 상태가 바뀌었을 가능성도 있기 때문에 상대적으로 안전한 종료를 선택한 구조라고 볼 수 있다.
다음으로 UI 위에서 입력이 끝난 경우 선택 데이터를 비운다.
if (input.IsInteractingWithUI())
{
buildingState.SelectionData.Clear();
}이 부분은 사용성 측면에서 매우 중요한 코드다.
플레이어가 마우스를 누른 상태로 움직이다가 UI 위에서 버튼을 놓았을 경우, 시스템이 그 선택을 그대로 유지하면 건축 선택과 UI 상호작용이 뒤섞여 버린다.
이를 방지하기 위해 현재 SelectionData를 직접 초기화한다.
SelectionData.Clear()는 내부적으로 선택된 Grid 좌표, 월드 좌표, 프리뷰 위치, 회전 정보, 유효성 정보 등을 모두 비우는 함수다.
즉 이 코드는 단순한 값 하나를 지우는 것이 아니라 현재 진행 중인 선택 상태 전체를 취소한다는 의미를 가진다.
마지막으로 상태 객체에 선택 완료를 알린다.
buildingState.HandleSelectionFinished();이 호출을 통해 상태 객체는 내부의 SelectionData를 기반으로 최종 SelectionResult를 생성하고, 이를 PlacementManager가 이후 로직에서 사용할 수 있도록 전달하게 된다.
이 함수는 단순히 마우스 업 이벤트를 처리하는 것이 아니라, 진행 중이던 선택을 실행 가능한 결과로 확정하는 단계이다.
* SelectionData와 SelectionResult의 의미
건축 선택 처리 시스템에서 가장 중요한 데이터 구조는 SelectionData와 SelectionResult이다.
이 둘은 이름이 비슷하지만 역할이 다르다.
SelectionData는 선택이 진행되는 동안 상태 내부에 유지되는 “가변 상태 데이터”이다.
플레이어가 마우스를 누르고 움직이는 동안 계속 수정되며, Grid 좌표 목록, 월드 좌표 목록, 프리뷰 좌표, 회전 정보, 배치 가능 여부 같은 값이 누적된다.
즉 선택 과정 전체를 담고 있는 작업용 컨테이너라고 볼 수 있다.
반면 SelectionResult는 선택이 확정된 뒤 외부 시스템으로 전달되는 결과 데이터이다.
배치 시스템, 제거 시스템, 이동 시스템은 SelectionData 내부를 직접 건드리지 않고, SelectionResult를 받아 실행한다.
이 구조의 장점은 선택이 완료된 시점의 데이터를 하나의 묶음으로 안전하게 전달할 수 있다는 것이다.
코드 관점에서 이 구조가 중요한 이유는 데이터의 수명 주기를 분리했기 때문이다.
진행 중에는 변경 가능한 SelectionData를 사용하고, 완료 시점에는 외부로 넘겨줄 불변에 가까운 SelectionResult를 사용함으로써 시스템 전체 데이터 흐름이 명확해진다.
* 선택 처리와 상태 패턴의 관계
이 시스템에서 선택 처리는 단독으로 존재하지 않고 BuildingState와 강하게 결합되어 있다.
같은 마우스 입력이라도 현재 상태가 무엇인지에 따라 전혀 다른 의미를 가지기 때문이다.
예를 들어 배치 상태에서는 선택된 Grid 좌표를 기반으로 구조물을 놓을 위치를 계산해야 하고, 제거 상태에서는 해당 위치에 제거 가능한 구조물이 있는지를 검사해야 하며, 이동 상태에서는 특정 구조물 하나를 선택하고 그 원점과 회전 값을 추적해야 한다.
따라서 선택 처리 로직을 PlacementManager 안에 직접 넣지 않고, HandleSelectionStarted, HandleSelectionChanged, HandleSelectionFinished 같은 호출만 상태 객체에 전달하는 구조를 사용한 것이다.
이렇게 하면 입력 처리 흐름은 공통으로 유지되면서도, 실제 선택 해석 로직은 상태별로 완전히 분리할 수 있다.
이 구조의 장점은 확장성이다.
새로운 상태가 추가되더라도 PlacementManager의 선택 처리 흐름은 그대로 유지되고, 새 상태 클래스 안에서 선택 의미만 새로 정의하면 된다.
즉, 입력 흐름은 공통, 해석 로직은 상태별 분리라는 구조가 만들어진다.
4. 개발 의도
건축 선택 처리 시스템의 핵심 설계 의도는 플레이어의 입력을 단순 좌표가 아닌 의미 있는 선택 결과로 변환하는 것이다.
입력 시스템이 가져오는 것은 기본적으로 마우스 위치일 뿐이지만, 건축 시스템은 그 입력을 기반으로 배치, 제거, 이동 같은 고수준 동작을 수행해야 한다.
따라서 입력을 곧바로 실행 로직에 넘기는 대신, 먼저 선택이라는 중간 단계로 해석하고, 그 결과를 SelectionResult라는 실행 가능한 데이터로 정리하는 구조를 만들었다.
또한 선택 처리 로직을 BuildingState 내부로 분리함으로써, 동일한 입력 흐름을 유지하면서도 상태에 따라 전혀 다른 선택 의미를 부여할 수 있도록 했다.
이 구조는 건축 시스템 전체를 단순한 입력-실행 구조가 아니라, 입력-해석-실행의 다층 구조로 만들며, 이후 기능 확장이나 유지보수에도 유리하다.
