건축 제거 전략 시스템
1. 시스템 요구 사항
건축 시스템에서 제거 기능은 단순히 오브젝트를 삭제하는 기능이 아니라, 선택 시스템과 동일한 흐름을 따르면서 검증 규칙만 반대로 동작하는 구조가 필요하다.
배치 시스템에서는 선택된 위치가 유효한지, 점유되지 않았는지, 특정 조건을 만족하는지를 검증해야 하지만, 제거 시스템에서는 오히려 선택된 위치가 무엇이든 간에 제거를 시도할 수 있어야 한다.
조건을 만족해야 실행이 아니라 조건과 상관없이 실행 허용이라는 반대 성격을 가진다.
또한 제거 시스템 역시 배치 시스템과 동일하게 Grid 기반 선택 구조를 사용해야 하며, 벽, 벽 내부, 가구, 바닥 등 각 구조물 타입에 맞는 선택 전략을 그대로 유지해야 한다.
제거는 완전히 새로운 시스템이 아니라, 기존 배치 전략 시스템을 그대로 재사용하면서 검증 로직만 무력화한 구조로 설계되어야 한다.
2. 흐름도
플레이어 Delete 입력
↓
PlacementManager
↓
RemovingState
↓
SelectionStrategy 선택
(벽 / 벽내부 / 가구 / 바닥)
↓
ModifySelection (기존 전략 그대로 사용)
↓
SelectionData 생성
↓
ValidatePlacement (항상 true)
↓
TryRemovingObject
↓
RemoveStructureAt 실행
이 구조에서 핵심은 하나다.
선택 흐름은 100% 동일하지만, 검증만 제거용으로 변경하였다.
3. 구현
3.1. 벽 제거 전략
public class WallRemovalStrategy : WallPlacementStrategy
{
public WallRemovalStrategy(PlacementGridData wallPlacementData, PlacementGridData inWallPlacementData, PlacementGridData objectPlacementData, GridManager gridManager) : base(wallPlacementData,inWallPlacementData,objectPlacementData, gridManager)
{
}
protected override bool ValidatePlacement(SelectionData selectionData)
{
return true;
}
}WallRemovalStrategy는 이름 그대로 벽을 제거하기 위한 전략 클래스이지만, 이 코드를 읽을 때 가장 먼저 봐야 하는 부분은 WallPlacementStrategy를 상속하고 있다는 점이다.
이 클래스는 벽을 어떻게 선택할 것인가에 대해서는 새로운 로직을 만들지 않는다.
벽 배치 전략에서 이미 구현해 둔 선택 방식, 경계 기준 Grid 좌표 해석, 경로 계산, 회전 계산, SelectionData 구성 방식 전체를 그대로 재사용한다는 뜻이다.
이 구조는 매우 중요하다.
제거 전략을 따로 만든다고 해서 선택 시스템까지 다시 구현하면 코드가 중복되고, 배치와 제거가 서로 다른 방식으로 동작하면서 버그가 생길 가능성이 커진다.
그런데 이 클래스는 부모를 WallPlacementStrategy로 두면서 벽을 선택하는 방식은 배치 때와 완전히 동일하다는 것을 코드 구조로 보장하고 있다.
생성자를 보면 매개변수가 wallPlacementData, inWallPlacementData, objectPlacementData, gridManager 네 개이고, 본문은 비어 있으며 base(...) 호출만 존재한다.
이건 아무 일도 하지 않는 생성자가 아니라, 초기화는 부모 클래스가 전부 담당한다는 의미를 가진 생성자다.
C#에서 : base(...) 문법은 상위 클래스 생성자를 먼저 호출하는 문법이고, 상속 구조에서 부모가 요구하는 초기화 과정을 그대로 따르겠다는 선언이기도 하다.
여기서 WallRemovalStrategy가 별도로 필드를 추가하지 않고, 생성자에서도 자기만의 작업을 하지 않는 이유는 분명하다.
제거 전략은 벽 선택에 필요한 데이터 구조가 배치 전략과 완전히 같기 때문이다.
벽 데이터, 벽 내부 데이터, 일반 오브젝트 데이터, GridManager까지 모두 부모 전략과 동일하게 필요하고, 그것을 그대로 넘겨서 부모의 초기화 로직을 사용하면 충분하다.
이건 짧은 코드지만 설계적으로는 “제거 전략은 선택 구조를 새로 만드는 것이 아니라, 기존 선택 구조를 그대로 물려받는다”는 매우 강한 메시지를 담고 있다.
이 클래스의 실제 핵심은 ValidatePlacement 하나뿐이다.
부모인 WallPlacementStrategy에서는 이 함수가 매우 많은 일을 한다.
선택된 벽 경로가 Grid 범위 안에 있는지, 이미 다른 벽이 존재하지는 않는지, 벽 내부 오브젝트와 충돌하지 않는지, 일반 오브젝트를 비정상적으로 가로지르지 않는지까지 단계적으로 검사한다.
그런데 제거 전략에서는 이런 검증이 필요하지 않다.
왜냐하면 제거의 목적은 놓을 수 있는지를 판단하는 것이 아니라, 현재 선택한 것을 제거 대상으로 삼을 것인지 이기 때문이다.
그래서 여기서는 return true를 반환한다.
이 한 줄은 굉장히 단순해 보이지만 의미는 매우 크다.
선택 과정은 그대로 유지하되, 배치 전략이 가지고 있던 제약 조건을 모두 해제한다는 뜻이기 때문이다.
WallRemovalStrategy는 벽을 선택하는 방법은 배치 전략과 같고, 단지 배치 가능 여부 검사는 항상 통과시킨다는 구조를 가진다.
3.2. 벽 내부 제거 전략
public class InWallRemovalStrategy : InWallPlacementStrategy
{
public InWallRemovalStrategy(PlacementGridData inWallObjectsPlacementData, PlacementGridData placementData, GridManager gridManager) : base(inWallObjectsPlacementData, placementData, gridManager)
{
}
protected override bool ValidatePlacement(SelectionData selectionData)
{
return true;
}
}InWallRemovalStrategy는 문이나 창문처럼 벽 내부에 설치되는 구조물을 제거하기 위한 전략이다.
이 코드 역시 가장 먼저 봐야 하는 부분은 InWallPlacementStrategy를 상속한다는 점이다.
이 말은 곧, 벽 내부 제거 전략은 벽 내부 오브젝트를 어떻게 찾는지에 대해 배치 전략과 동일한 선택 구조를 사용한다는 뜻이다.
벽 내부 오브젝트는 일반 가구처럼 아무 셀에서나 선택되는 것이 아니라, 기존 벽 위에 놓일 수 있는 위치인지, 현재 위치가 벽과 연결된 위치인지 같은 규칙을 따라 선택된다.
제거할 때도 그 선택 규칙은 그대로 필요하다.
예를 들어 벽 위에 없는 위치를 클릭했을 때는 애초에 문이나 창문 선택 자체가 이루어지지 않아야 하므로, 제거라고 해서 선택 구조를 단순화할 수 있는 것은 아니다.
그래서 이 클래스는 InWallPlacementStrategy를 부모로 두고, 그 안에 구현된 Grid 좌표 해석과 선택 데이터 구성 방식을 그대로 물려받는다.
생성자도 같은 맥락에서 봐야 한다.
inWallObjectsPlacementData, placementData, gridManager를 받아서 그대로 부모 생성자에 넘기고, 자신은 아무 필드도 추가로 초기화하지 않는다.
이건 단순히 코드가 짧아서 그런 것이 아니다.
벽 내부 제거 전략이 필요로 하는 데이터 구조와 초기화 방식이 이미 InWallPlacementStrategy 안에 모두 들어 있기 때문이다.
제거 전략이 새로 해야 하는 것은 선택 방식의 변경이 아니라 검증 규칙의 변경뿐이고, 좌표 해석이나 SelectionData 구성은 부모 전략에 완전히 맡기는 것이 더 자연스럽다.
C# 상속 구조에서 이런 식의 생성자 forwarding은 새 상태를 추가하지 않고 부모 초기화만 그대로 따른다는 표현이기도 하다.
이 클래스에서도 실제로 오버라이드한 함수는 ValidatePlacement 하나뿐이다.
그리고 그 구현은 역시 return true이다.
부모인 InWallPlacementStrategy에서는 선택된 위치가 실제로 벽 위에 존재하는지, 현재 위치가 벽 내부 오브젝트를 놓을 수 있는 자리인지 검사했었다.
하지만 제거 전략에서는 그 위치에 이미 선택 가능한 벽 내부 오브젝트가 있다는 것만 확인되면 충분하다.
더 이상 여기에 새로 놓을 수 있는지를 검사할 필요가 없으므로, 배치 전략에 있던 제약을 모두 제거하고 무조건 true를 반환한다.
이 말은 곧, InWallRemovalStrategy의 설계 핵심도 WallRemovalStrategy와 완전히 동일하다는 뜻이다.
선택 구조는 부모 전략을 그대로 쓰고, 최종 검증만 제거에 맞게 완화한다.
3.3. 자유 배치 오브젝트 제거 전략
public class ObjectRemovalStrategy : FreeObjectPlacementStrategy
{
public ObjectRemovalStrategy(PlacementGridData placementData, PlacementGridData wallPlacementData, PlacementGridData inWallPlacementData, GridManager gridManager) : base(placementData, wallPlacementData, inWallPlacementData, gridManager)
{
}
protected override bool ValidatePlacement(SelectionData selectionData)
{
return true;
}
}ObjectRemovalStrategy는 가구나 일반 오브젝트처럼 자유 배치 전략을 따르는 구조물을 제거하기 위한 전략이다.
이 클래스가 FreeObjectPlacementStrategy를 상속하고 있다는 점이 가장 중요하다.
제거 시에도 현재 마우스 위치를 Grid 좌표로 해석하고, lastDetectedPosition을 이용해 같은 셀에서 불필요한 갱신을 막고, 현재 위치 하나를 기준으로 SelectionData를 다시 구성하는 흐름은 모두 자유 배치 전략과 동일하게 가져간다는 뜻이다.
이건 단순 재사용이 아니라, 배치와 제거가 같은 방식으로 선택된다는 일관성을 만들어 준다.
예를 들어 어떤 가구를 배치할 때 1x1 단일 위치 선택 구조를 썼다면, 제거할 때도 उसी 기준으로 선택되어야 플레이어 입장에서 동작이 일관되다.
이 전략은 바로 그 일관성을 상속 구조로 확보하고 있다.
생성자도 부모 생성자에 모든 의존성을 넘기고 본문은 비어 있다.
여기서는 placementData, wallPlacementData, inWallPlacementData, gridManager를 모두 부모에 전달한다.
FreeObjectPlacementStrategy는 일반 오브젝트 레이어뿐 아니라 벽과 벽 내부 구조물을 가로지르지 않는지도 함께 검사하는 구조이기 때문에, 제거 전략에서도 같은 데이터 구성을 그대로 가져간다.
이 점이 중요하다.
제거 전략이라고 해서 데이터 구조가 단순해지는 것이 아니라, 선택 구조를 부모와 완전히 동일하게 유지하기 위해 같은 데이터 집합이 그대로 필요하다.
본문이 비어 있다는 것은 이 클래스가 자기만의 상태를 추가하지 않는다는 뜻이며, 제거 전략의 차별점이 데이터 추가가 아니라 검증 규칙 변경에만 있다는 점을 보여준다.
이 클래스 역시 오버라이드한 것은 ValidatePlacement 하나뿐이다.
부모인 FreeObjectPlacementStrategy에서는 이 함수가 매우 많은 검사를 한다.
선택한 위치가 Grid 범위 안에 있는지, 현재 오브젝트 레이어가 비어 있는지, 벽 내부 오브젝트를 가로지르지 않는지, 벽을 가로지르지 않는지까지 모두 검사한다.
그런데 제거 전략에서는 이런 검사가 필요하지 않다.
이미 배치되어 있는 오브젝트를 선택해서 제거하는 상황에서, 그 자리가 비어 있어야 한다는 식의 배치 조건은 오히려 의미가 없다.
그래서 여기서는 단순히 true를 반환한다.
이 한 줄은 선택은 부모 전략과 똑같이 하고, 단지 배치 가능 여부를 막지 않는다는 의미이다.
다시 말해 ObjectRemovalStrategy는 자유 배치 오브젝트의 제거를 위해 선택 메커니즘은 그대로 유지하고, 배치 검증만 무력화한 클래스다.
3.4. 바닥 제거 전략
public class FloorRemovalStrategy : BoxSelection
{
public FloorRemovalStrategy(PlacementGridData placementData, GridManager gridManager) : base(placementData, gridManager)
{
}
protected override bool ValidatePlacement(SelectionData selectionData)
{
return true;
}
}FloorRemovalStrategy는 바닥처럼 여러 셀을 박스 형태로 선택하는 구조물을 제거하기 위한 전략이다.
이 클래스가 BoxSelection을 상속한다는 점이 핵심이다. 즉 바닥 제거는 자유 배치처럼 단일 셀을 선택하는 구조가 아니라, 배치할 때와 마찬가지로 드래그를 통해 여러 셀을 한 번에 선택하는 흐름을 그대로 사용한다는 뜻이다.
이것도 매우 중요한 설계 포인트다.
바닥은 보통 넓은 영역을 한 번에 깔고, 제거할 때도 넓은 영역을 한 번에 지우는 것이 자연스럽다.
그래서 제거 전략도 단일 선택이 아니라 박스 선택을 그대로 물려받아야 한다.
이 상속 구조는 바닥 제거는 바닥 배치와 같은 영역 선택 방식을 사용한다는 것을 아주 명확하게 보여준다.
생성자는 placementData와 gridManager만 받아서 부모 생성자에 그대로 넘긴다.
바닥 전략은 벽이나 벽 내부 레이어와의 관계를 직접 다루지 않기 때문에, 자유 배치 전략보다 필요한 데이터 종류가 적다.
BoxSelection은 선택 시작점, 현재 위치, 모서리 기준 순회 규칙, Grid/World 좌표 동시 기록 같은 박스 선택용 구조를 이미 갖고 있으므로, 제거 전략 쪽에서는 이를 그대로 재사용하기만 하면 된다.
본문이 비어 있는 이유도 마찬가지다.
FloorRemovalStrategy는 바닥 제거를 위해 새로운 상태나 추가 필드를 만들지 않고, 기존 박스 선택 시스템을 그대로 활용한다.
마지막 ValidatePlacement 역시 true를 반환한다.
부모인 BoxSelection에서는 이 함수가 선택된 모든 Grid 위치가 유효한지, 비어 있는지 등을 검사한다.
하지만 제거 전략에서는 그 자리가 비어 있으면 안 되는 것이 아니라, 선택만 되면 제거 대상으로 넘기면 된다.
그래서 여기도 모든 배치 조건을 제거하고 true를 반환한다.
이때 중요한 것은, 박스 선택 자체는 여전히 살아 있다는 점이다.
드래그 방식으로 범위를 구성하고, 그 범위를 SelectionData에 채워 넣는 흐름은 부모인 BoxSelection이 그대로 담당한다.
단지 그 결과를 배치 가능한 영역으로 볼지, 제거 가능한 영역으로 볼지만 ValidatePlacement 하나가 바꿔 주는 것이다.
4. 개발 의도
이 제거 전략 시스템의 핵심 설계 의도는, 제거를 별도의 선택 시스템으로 다시 구현하지 않고 기존 배치 전략을 그대로 재사용하는 것이다.
코드를 보면 네 클래스 모두 공통적으로 부모 배치 전략을 상속하고, 생성자는 부모 생성자만 호출하며, ValidatePlacement만 true로 바꾼다는 패턴을 가진다.
이건 단순히 코드를 짧게 쓰기 위한 트릭이 아니라, 선택 방식과 검증 방식을 분리해 두었기 때문에 가능한 설계다.
배치 전략은 본래 어떻게 선택할 것인가와 그 선택이 유효한가, 이 두 가지를 동시에 가지고 있었고, 제거 전략은 그중 첫 번째는 그대로 유지하고 두 번째만 바꾼다.
그래서 벽 제거는 WallPlacementStrategy의 선택 흐름을, 벽 내부 제거는 InWallPlacementStrategy의 선택 흐름을, 일반 오브젝트 제거는 FreeObjectPlacementStrategy의 선택 흐름을, 바닥 제거는 BoxSelection의 선택 흐름을 각각 그대로 상속받는다.
그 위에 ValidatePlacement만 return true로 바꾸면서 배치 제약은 무시하고 제거 대상으로 선택할 수 있게 한다는 의미를 부여한다.
