벽 내부 배치 전략

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. 클래스 구조와 상태 필드

       3.2. 생성자 및 의존성 주입

       3.3. 선택 시작 처리

       3.4. 선택 진행 처리

       3.5. 배치 검증

       3.6. 선택 종료 처리

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서 문이나 창문과 같은 구조물은 일반적인 바닥이나 가구와는 다른 배치 규칙을 가진다.

이러한 구조물은 독립적으로 배치되는 것이 아니라 반드시 기존에 존재하는 벽 위에 배치되어야 한다.

즉, 아무 위치에나 설치할 수 없고, 반드시 “벽이 존재하는 위치”라는 조건을 만족해야 한다.

이때 중요한 점은 단순히 위치가 비어있는지를 검사하는 것이 아니라, 이미 존재하는 벽 데이터 위에 정확하게 겹쳐서 배치되어야 한다는 점이다.

따라서 일반적인 배치 시스템에서 사용하는 빈 공간 검사 로직이 아니라, 특정 구조물 위에 존재하는지를 검사하는 별도의 검증 로직이 필요하다.

또한 벽 내부 구조물은 선택 방식 자체도 단순하다.

박스 드래그와 같은 다중 선택이 아닌, 단일 셀 기반 선택으로 동작하며, 마우스 위치에 따라 즉시 선택 위치가 결정되어야 한다.

따라서 선택 변경은 매 프레임 발생할 수 있으며, 불필요한 연산을 줄이기 위해 위치 변화가 있을 때만 갱신하는 구조가 필요하다.

이러한 요구사항을 만족하기 위해 InWallPlacementStrategy는 다음과 같은 역할을 수행한다.

마우스 위치를 Grid 좌표로 변환하고, 선택 위치를 갱신하며, 해당 위치에 벽이 존재하는지를 검사하여 배치 가능 여부를 판단한다.

그리고 이 모든 과정을 SelectionStrategy 기반 구조 안에서 처리하여 다른 배치 전략과 동일한 인터페이스를 유지한다.

2. 흐름도

마우스 입력

GridManager → 월드 좌표 → Grid 좌표 변환

LastDetectedPosition → 위치 변화 감지

SelectionData 초기화 및 갱신

ValidatePlacement 실행

벽 데이터(wallPlacementData) 기반 점유 검사

배치 가능 여부 결정

이 구조에서 핵심은 위치 변화 감지와 벽 위에만 배치 가능이라는 두 가지 조건이다.

일반 배치와 다르게 공간이 비어있는지를 보는 것이 아니라, 특정 데이터가 존재하는지를 검사하는 방향으로 흐름이 구성되어 있다.

3. 구현

3.1. 클래스 구조와 상태 필드
public class InWallPlacementStrategy : SelectionStrategy
{
    protected PlacementGridData wallPlacementData;

    ...
  }

이 클래스는 SelectionStrategy를 상속받아 선택 방식을 정의하는 전략 객체이다.

여기서 핵심은 이 클래스가 배치 자체를 하는 클래스가 아니라, 어디에 배치할 수 있는지를 결정하는 클래스라는 점이다.

wallPlacementData는 현재 맵에 존재하는 벽의 Grid 점유 데이터를 참조한다.

이 데이터는 단순한 리스트가 아니라, 특정 Grid 좌표에 어떤 구조물이 존재하는지를 빠르게 조회할 수 있도록 구성된 구조다.

이 변수를 별도로 들고 있는 이유는 매우 명확하다.

일반 배치 전략은 빈 공간인지만 보면 되지만, 이 전략은 벽이 있는 위치인지를 판단해야 하기 때문이다.

즉, 이 클래스는 빈 공간을 찾는 전략이 아니라, 특정 구조물 위를 찾는 전략이다.

3.2. 생성자 및 데이터 구조
public InWallPlacementStrategy(PlacementGridData wallPlacementData, PlacementGridData placementData, GridManager gridManager) 
    : base(placementData, gridManager)
{
    this.wallPlacementData = wallPlacementData;
}

이 생성자는 이 전략이 동작하기 위해 필요한 외부 데이터를 주입받는다.

여기서 중요한 포인트는 두 가지 PlacementGridData가 존재한다는 점이다.

하나는 base로 전달되는 placementData이고, 다른 하나는 wallPlacementData이다.

placementData는 일반 구조물 데이터이고, wallPlacementData는 벽 데이터이다.

즉 이 전략은 두 개의 데이터 계층을 동시에 참조하는 구조다.

이렇게 외부에서 데이터를 주입하는 방식은 Unity에서 흔히 사용하는 패턴인데, 장점은 테스트와 확장성이 높다는 것이다.

단점은 의존성이 많아질수록 생성자 관리가 복잡해진다는 점이다.

3.3. 선택 시작 처리
public override void StartSelection(Vector3 mousePosition, SelectionData selectionData)
{
    this.lastDetectedPosition.Reset();
    ModifySelection(mousePosition, selectionData);
}

이 함수는 마우스를 처음 클릭했을 때 실행된다.

여기서 가장 중요한 건 'Reset → ModifySelection' 순서다.

Reset은 내부적으로 마지막으로 감지된 Grid 좌표를 초기화하는 역할을 한다.

이 값은 ModifySelection에서 이전 위치와 비교할 때 사용되기 때문에, 초기화하지 않으면 첫 입력이 무시될 수 있다.

그 다음 바로 ModifySelection을 호출하는 이유는 클릭 순간에도 선택 결과를 즉시 계산하기 위해서다.

Unity에서는 보통 Update에서 처리하지만, 이렇게 직접 호출하면 프레임을 기다리지 않고 즉시 반응하게 된다.

즉, 입력 딜레이를 줄이기 위한 설계다.

3.4. 선택 진행 처리
public override bool ModifySelection(Vector3 mousePosition, SelectionData selectionData)
{
    Vector3Int tempPos = gridManager.GetCellPosition(mousePosition, selectionData.PlacedItemData.objectPlacementType);
    
    if (lastDetectedPosition.TryUpdatingPositon(tempPos))
    {
        selectionData.Clear();
        selectionData.AddToWorldPositions(gridManager.GetWorldPosition(lastDetectedPosition.GetPosition()));
        selectionData.AddToGridPositions(lastDetectedPosition.GetPosition());
        selectionData.PlacementValidity = ValidatePlacement(selectionData);   
        return true;
    }
    return false;
}

이 함수는 마우스가 움직이거나, Update 루프에서 선택 위치가 갱신될 때마다 반복적으로 호출되는 함수이다.

즉, 선택이 시작된 이후에는 이 함수가 계속 호출되면서 현재 선택 상태를 갱신하는 역할을 한다.

건축 시스템에서는 플레이어가 마우스를 움직일 때마다 프리뷰 위치가 바뀌어야 하기 때문에, 이 함수는 사실상 실시간 선택 갱신 루프의 핵심 처리 함수라고 볼 수 있다.

함수 내부의 첫 줄에서는 현재 마우스 위치를 Grid 좌표로 변환한다.

Vector3Int tempPos = gridManager.GetCellPosition(mousePosition, selectionData.PlacedItemData.objectPlacementType);

GridManager는 Grid 시스템의 핵심 클래스이며, 월드 좌표를 기반으로 어떤 셀에 해당하는지를 계산한다.

여기서 objectPlacementType을 함께 전달하는 이유는 셀 중심 기준인지, 경계 기준인지에 따라 좌표 계산 방식이 달라지기 때문이다.

이 구조는 다양한 배치 타입을 하나의 함수에서 처리할 수 있도록 만든 확장 가능한 설계이다.

다음 조건문은 이 함수의 핵심 제어 흐름이다.

if (lastDetectedPosition.TryUpdatingPositon(tempPos))

이 부분은 성능과 직결되는 핵심 로직이다.

TryUpdatingPosition은 현재 위치와 이전 위치를 비교하여 실제로 위치가 변경되었을 때만 true를 반환한다.

Unity의 Update는 매 프레임 호출되기 때문에, 이 체크가 없다면 동일한 위치에서도 계속 SelectionData를 갱신하게 된다.

이는 불필요한 연산 증가와 GC 발생으로 이어질 수 있다.

따라서 위치 변경 시에만 로직을 수행하도록 제한한 것은 매우 중요한 최적화 설계이다.

다음 조건문은 이 함수의 핵심 제어 흐름이다.

selectionData.Clear();

이 과정은 이전 프레임의 선택 상태를 제거하고, 현재 프레임 기준으로 선택을 다시 구성하기 위한 것이다.

SelectionData는 현재 선택된 Grid 좌표, 월드 좌표, 회전 정보 등을 모두 포함하는 구조체이기 때문에, 이전 데이터를 유지하면 잘못된 선택 상태가 누적될 수 있다.

이후 현재 선택 위치를 기록한다.

selectionData.AddToWorldPositions(gridManager.GetWorldPosition(lastDetectedPosition.GetPosition()));
selectionData.AddToGridPositions(lastDetectedPosition.GetPosition());

이 부분은 선택 결과를 실제 데이터 구조에 기록하는 단계이다.

Grid 좌표와 월드 좌표를 동시에 저장하는 이유는 역할이 다르기 때문이다.

Grid 좌표는 논리적인 위치 판정에 사용되고, 월드 좌표는 실제 오브젝트 생성이나 프리뷰 표시 등에 사용된다.

이 두 데이터를 분리하여 저장하는 구조는 매우 중요한 설계 포인트이다.

마지막으로 배치 가능 여부를 계산한다.

selectionData.PlacementValidity = ValidatePlacement(selectionData);

이 코드는 현재 선택 위치가 실제로 배치 가능한지를 판단하는 단계이다.

이 값은 이후 프리뷰 색상 변경, 배치 허용 여부, 입력 처리 흐름 등에 직접적인 영향을 준다.

결과적으로 ModifySelection 함수는'입력 → 좌표 변환 → 위치 변화 감지 → 선택 데이터 갱신 → 배치 가능 여부 판단' 이라는 전체 흐름을 수행하는 핵심 함수이다.

3.5. 배치 검증
protected override bool ValidatePlacement(SelectionData selectionData)
{
    bool valid = PlacementValidator.CheckIfPositionsAreOccupied(
        selectionData.GetSelectedGridPositions(), 
        this.wallPlacementData, 
        selectionData.PlacedItemData.size, 
        selectionData.GetSelectedPositionsGridRotation(), 
        selectionData.PlacedItemData.objectPlacementType.IsEdgePlacement());

    return valid;
}

이 함수는 현재 선택된 위치가 실제로 배치 가능한 위치인지 판단할 때 호출되는 함수이다.

이 함수는 ModifySelection 내부에서 호출되며, 선택 위치가 갱신될 때마다 함께 실행된다.

즉, 이 함수는 단발성 검사가 아니라, 실시간으로 반복 실행되는 검증 함수이다.

함수 내부에서는 PlacementValidator를 통해 실제 검증을 수행한다.

bool valid = PlacementValidator.CheckIfPositionsAreOccupied(...)

이 코드에서 가장 중요한 점은 일반 배치와 달리 비어있는지가 아니라 이미 점유되어 있는지를 검사한다는 점이다.

일반적인 배치 시스템에서는 CheckIfPositionsAreFree를 사용하여 빈 공간을 찾는다.

하지만 InWallPlacementStrategy에서는 CheckIfPositionsAreOccupied를 사용한다.

이 차이는 단순한 함수 선택이 아니라, 배치 규칙 자체가 반대라는 것을 의미한다.

이 함수가 wallPlacementData를 사용하는 이유는 현재 선택 위치에 벽이 존재하는지를 확인하기 위해서이다.

이 검증에서 valid가 true면 '해당 위치에 벽이 존재하여 배치가 가능', false면 '벽이 없음으니 배치가 불가능' 의미이다.

여기서 PlacementValidator는 Grid 데이터를 기반으로 충돌이나 점유 상태를 검사하는 유틸리티 클래스이다.

이 구조를 사용하면 검증 로직을 전략 클래스에서 분리할 수 있고, 다른 배치 전략에서도 동일한 검증 로직을 재사용할 수 있다.

결과적으로 ValidatePlacement 함수는 단순한 boolean 반환 함수가 아니라, 벽 위에만 배치 가능하도록 만드는 핵심 규칙 정의 함수이다.

3.6. 선택 종료 처리
public override void FinishSelection(SelectionData selectionData)
{
    lastDetectedPosition.Reset();
}

이 함수는 선택이 끝났을 때 호출된다.

여기서 Reset을 다시 호출하는 이유는 다음 선택 과정에서 이전 위치가 영향을 주지 않도록 하기 위함이다.

만약 이 초기화를 하지 않으면 다음 선택에서 위치 변화가 감지되지 않아 ModifySelection이 호출되지 않을 가능성이 있다.

이는 입력 반응이 끊기는 문제로 이어질 수 있다.

4. 개발 의도

InWallPlacementStrategy의 핵심 설계 의도는 “일반 배치와 완전히 다른 검증 기준을 동일한 인터페이스 안에서 처리하는 것”이다.

일반 배치 전략은 빈 공간을 찾는 문제이지만, 벽 내부 배치는 특정 구조물 위에 존재해야 하는 문제이다. 이 두 문제는 본질적으로 다르지만, SelectionStrategy라는 공통 인터페이스를 사용함으로써 시스템 전체 구조를 일관되게 유지할 수 있도록 설계하였다.

또한 LastDetectedPosition을 활용하여 불필요한 연산을 제거한 점은 실시간 입력 시스템에서 매우 중요한 최적화 포인트이다. Unity의 Update 기반 구조에서는 매 프레임 동일한 로직이 반복되기 때문에, 이러한 위치 변화 기반 필터링은 성능 안정성에 직접적인 영향을 준다.

PlacementValidator를 활용한 검증 구조 역시 중요한 설계이다. 검증 로직을 전략 내부에 직접 작성하지 않고 외부 유틸리티로 분리함으로써, 검증 규칙을 재사용 가능하게 만들고 전략 클래스의 책임을 명확하게 유지하였다.

결과적으로 이 전략은 단순한 선택 처리 로직이 아니라, “특정 구조물 위에만 배치 가능한 제약 조건을 가진 시스템”을 기존 아키텍처 안에서 자연스럽게 통합한 설계이며, 확장성과 유지보수성을 동시에 고려한 구조라고 볼 수 있다.