건축 제거 상태 시스템
1. 시스템 요구 사항
Grid 기반 건축 시스템에서 구조물 제거는 단순한 오브젝트 삭제 기능이 아니라, 배치 시스템과 동일한 입력 흐름을 유지하면서도 전혀 다른 규칙으로 동작해야 하는 상태 기반 기능이다.
플레이어는 마우스를 통해 위치를 선택하고, 그 위치에 존재하는 구조물을 제거하게 된다.
이 과정에서 중요한 점은 선택 방식은 배치와 동일하지만, 선택의 의미는 다르다는 것이다.
배치에서는 선택된 위치가 유효한지를 판단하지만, 제거에서는 선택된 위치에 실제로 제거할 대상이 존재하는지를 판단해야 한다.
또한 제거 시스템은 Grid 데이터 기반으로 동작하기 때문에, 단순히 씬의 GameObject를 삭제하는 것이 아니라 해당 구조물이 점유하고 있던 Grid 데이터까지 함께 제거되어야 한다.
그리고 구조물은 셀 기반, 경계 기반, 벽 내부 구조 등 다양한 형태로 존재하기 때문에 제거 역시 이 구조를 그대로 반영해야 한다.
따라서 제거 상태 시스템은 다음을 만족해야 한다.
입력 흐름은 배치와 동일하게 유지해야 한다.
구조물 타입에 따라 다른 제거 규칙을 적용해야 한다.
Grid 데이터 레이어에 맞게 제거 대상이 결정되어야 한다.
선택 결과를 기반으로 제거 실행 계층에 전달해야 한다.
즉, RemovingState는 입력 구조는 동일하지만 해석 규칙만 다른 상태를 구현하는 계층이다.
2. 흐름도
Delete 입력
↓
PlacementManager.HandleDeleteAction
↓
RemovingState 생성
↓
SelectionData 초기화
↓
PlacementType에 따라 제거 전략 선택
↓
PlacementSelector 생성
↓
ConnectToPlacementSelection
↓
마우스 입력 시작
↓
HandleSelectionStarted
↓
HandleSelectionChanged
↓
제거 대상 계산 (SelectionStrategy)
↓
SelectionResult 생성
↓
OnFinished 이벤트
↓
PlacementManager.TryRemovingObject
이 흐름의 핵심은 배치와 동일한 입력 구조를 그대로 재사용한다는 점이다.
다만 내부 전략이 배치 전략이 아니라 제거 전략으로 바뀐다.
3. 구현
3.1. 생성자 구조와 상태 초기화
public RemovingState(GridManager gridManager, GridData gridData, ItemData itemData)
: base(gridManager, gridData, itemData)
{
this.ItemData = itemData;
selectionData = new SelectionData(itemData);RemovingState는 생성자에서 상태를 완전히 구성한다.
이 클래스는 Update에서 무언가를 계산하는 클래스가 아니라, 생성 시점에 동작 방식을 결정하는 상태 클래스다.
먼저 : base(...) 호출을 통해 BuildingState의 공통 필드를 초기화한다.
이 단계에서 gridManager, gridData, itemData가 상위 클래스에 저장된다.
그 다음 this.ItemData = itemData;는 상위에서 이미 전달된 데이터를 명시적으로 다시 할당하는 코드다.
구조적으로는 중복처럼 보일 수 있지만, 상태 내부에서 ItemData를 명확하게 참조하려는 의도가 반영된 코드다.
이후 selectionData = new SelectionData(itemData);가 실행된다.
이 객체는 현재 선택 상태를 저장하는 핵심 데이터다.
여기서 중요한 점은 SelectionData가 단순 좌표 저장이 아니라, 현재 어떤 구조물을 기준으로 선택하고 있는지까지 포함한다는 것이다.
즉, 제거 상태에서도 SelectionData는 그대로 재사용되며, 이 덕분에 SelectionResult 구조를 배치 시스템과 완전히 동일하게 유지할 수 있다.
3.2. 벽 제거 전략 구성
if (itemData.objectPlacementType == PlacementType.Wall)
{
currentPlacementData = gridData.WallPlacementData;
placementSelection = new(
new WallRemovalStrategy(
currentPlacementData,
gridData.InWallPlacementData,
gridData.ObjectPlacementData,
gridManager),
selectionData);
}이 분기는 제거 대상이 벽일 때 실행된다.
먼저 currentPlacementData를 WallPlacementData로 설정한다.
이 의미는 제거 대상이 벽 데이터 레이어에서 관리된다는 뜻이다.
그 다음 WallRemovalStrategy가 생성된다.
이 전략 생성자에는 WallPlacementData, InWallPlacementData, ObjectPlacementData 데이터가 들어간다.
이 구조는 매우 중요한 의미를 가진다.
벽 제거는 단순히 벽만 삭제하는 문제가 아니라, 벽 내부 오브젝트나 벽 근처 오브젝트와의 관계까지 고려해야 한다는 뜻이다.
즉 이 전략은 이 위치에 벽이 있는지, 벽과 연결된 다른 구조물이 있는지를 함께 판단한다.
여기서 PlacementSelector는 이 전략을 감싸는 역할을 한다.
즉, 실제 선택 계산은 전략이 수행하고, Selector는 이를 상태 시스템과 연결한다.
3.3. 벽 내부 구조물 제거 전략
if (itemData.objectPlacementType == PlacementType.InWalls)
{
currentPlacementData = gridData.InWallPlacementData;
placementSelection = new(
new InWallRemovalStrategy(
gridData.WallPlacementData,
currentPlacementData,
gridManager),
selectionData);
}이 분기는 벽 내부 구조물을 제거할 때 실행된다.
여기서는 currentPlacementData를 InWallPlacementData로 설정한다.
즉, 제거 대상은 벽 내부 레이어다.
하지만 전략 생성 시 WallPlacementData도 함께 전달된다.
이 구조는 벽 내부 오브젝트가 독립적으로 존재하는 것이 아니라, 항상 벽과 관계를 가지기 때문이다.
즉, 제거 조건은 단순히 이 위치에 오브젝트가 있는지가 아니라, 이 위치가 벽 위에 있는지까지 포함된다.
이 설계는 구조물 간 의존 관계를 반영한 것이다.
3.4. 바닥 제거 전략
if (itemData.objectPlacementType == PlacementType.Floor)
{
currentPlacementData = gridData.FloorPlacementData;
placementSelection = new(
new FloorRemovalStrategy(currentPlacementData, gridManager),
selectionData);
}이 분기는 바닥 제거를 처리한다.
바닥은 가장 단순한 구조다.
셀 기반 구조물이기 때문에 추가적인 레이어 참조가 필요 없다.
전략 생성 시 currentPlacementData와 gridManager만 전달된다.
이 구조는 셀 기반 구조물은 단순하다는 것을 보여준다.
즉, 벽과 달리 다른 레이어와의 관계를 고려할 필요가 없다.
3.5. 일반 오브젝트 제거 전략
if (itemData.objectPlacementType == PlacementType.NearWallObject
|| itemData.objectPlacementType == PlacementType.FreePlacedObject)
{
currentPlacementData = gridData.ObjectPlacementData;
placementSelection = new(
new ObjectRemovalStrategy(
currentPlacementData,
gridData.WallPlacementData,
gridData.InWallPlacementData,
gridManager),
selectionData);
}이 분기는 일반 오브젝트 제거를 처리한다.
여기서 중요한 점은 FreePlacedObject와 NearWallObject를 동일하게 처리한다는 것이다.
두 타입은 배치 규칙은 다르지만, 제거 규칙은 동일하다는 의미다.
currentPlacementData는 ObjectPlacementData로 설정된다.
하지만 전략 생성 시
WallPlacementData
InWallPlacementData
가 함께 전달된다.
이것은 일반 오브젝트도 벽이나 벽 내부 구조물과 충돌하거나 연결될 수 있기 때문이다.
즉, 제거 시에도 주변 구조와의 관계를 고려해야 한다.
3.6 ConnectToPlacementSelection 호출
ConnectToPlacementSelection();이 함수는 BuildingState의 핵심 연결 함수다.
이 호출을 통해 PlacementSelector의 이벤트가 현재 상태의 이벤트로 연결된다.
즉 이후부터는
OnSelectionChanged
OnFinished
이 이벤트가 정상적으로 동작하게 된다.
이 한 줄이 없다면 RemovingState는 선택 결과를 외부로 전달할 수 없다.
즉 상태 초기화의 마지막 단계다.
4. 개발 의도
RemovingState의 핵심 설계 의도는 배치 시스템과 동일한 구조를 유지하면서 제거 기능을 독립된 상태로 분리하는 것이다.
건축 시스템에서 제거는 배치와 거의 동일한 입력 흐름을 가진다.
하지만 내부 로직은 완전히 다르다.
배치는 데이터를 추가하고, 제거는 데이터를 삭제한다.
이 둘을 하나의 클래스에서 처리하면 조건 분기가 계속 증가하게 된다.
그래서 RemovingState를 별도로 분리하고, SelectionStrategy만 제거 전용으로 교체하는 구조를 사용하였다.
이 구조의 가장 큰 장점은
입력 시스템 재사용
선택 시스템 재사용
SelectionResult 재사용
이 가능하다는 점이다.
즉, 시스템 전체 구조는 그대로 유지하면서 행동만 바꿀 수 있다.
결과적으로 RemovingState는 단순한 삭제 기능이 아니라, 배치 시스템과 동일한 아키텍처 위에서 동작하는 제거 전용 상태 계층이며, 상태 패턴과 전략 패턴이 결합된 구조의 핵심 구현이다.
