공간 검사 시스템

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. Grid 범위의 가장 작은 단위 검사

       3.2. 전체 점유 영역의 유효성 검사

       3.3. 공간이 비어 있는지 검사

       3.4. 공간이 이미 점유되어 있는지 검사

       3.5. 멀티타일 구조물 점유 검사

       3.6. Edge 구조물 교차 검사

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서 가장 중요한 순간은 플레이어가 구조물을 실제로 배치하는 순간이 아니라, 그 이전에 이 위치에 배치해도 되는지를 정확하게 판단하는 순간이다.

건축 시스템은 플레이어가 마우스로 선택한 하나의 좌표만 보면 안 되고, 구조물의 크기와 회전을 반영해 실제로 점유하게 될 전체 영역을 기준으로 배치 가능 여부를 판단해야 한다.

예를 들어 2x2 가구는 클릭한 한 칸에만 존재하는 것이 아니라 네 개의 셀을 동시에 차지하고, 벽이나 벽 내부 구조물은 셀이 아니라 셀과 셀 사이의 Edge를 차지한다.

따라서 공간 검사는 단순한 단일 좌표 검사로 끝날 수 없다.

또한 건축 시스템에서는 비어 있는지와 이미 점유되어 있는지가 서로 다른 의미를 가진다.

일반 오브젝트를 놓을 때는 해당 공간이 비어 있어야 하지만, 벽 내부 구조물처럼 기존 벽이 있는 위치에만 놓여야 하는 경우는 오히려 해당 공간이 이미 점유되어 있어야 한다.

배치 규칙이 구조물 종류에 따라 달라지므로, 공간 검사 시스템은 하나의 단순 함수가 아니라 여러 종류의 검사 함수로 분리되어 있어야 한다.

이와 함께 멀티타일 구조물과 Edge 기반 구조물은 일반적인 셀 점유 검사만으로는 처리할 수 없는 특수 상황을 만든다.

멀티타일 구조물은 하나의 구조물이 여러 셀을 공유하는 방식으로 저장되기 때문에, 특정 좌표 하나만 보고도 해당 구조물 전체를 추적할 수 있어야 한다.

반대로 셀 기반 오브젝트를 배치할 때는 주변 Edge에 이미 벽이 있는지도 검사해야 할 수 있다.

공간 검사 시스템은 단순히 Dictionary에 키가 있는지 확인하는 수준을 넘어서, Grid 공간 전체를 해석하는 논리 계층으로 동작해야 한다.

따라서 이 시스템은 구조물이 차지할 전체 좌표를 기준으로 Grid 범위가 유효한지 검사할 수 있어야 하고, 셀 또는 Edge 기준으로 공간이 비어 있는지 혹은 이미 점유되어 있는지 판단할 수 있어야 하며, 멀티타일 구조물과 Edge 구조물처럼 특별한 충돌 케이스도 다룰 수 있어야 한다.

이 검사 결과는 이후 SelectionStrategy, PlacementValidator, PlacementManager가 그대로 신뢰하고 사용하게 되므로, 이 계층의 정확성이 곧 배치 시스템 전체의 안정성을 결정한다.

2. 흐름도

구조물 배치 시도

   ↓

현재 선택 위치, 크기, 회전, 배치 타입 전달

   ↓

PlacementGridData 공간 검사 함수 호출

   ↓

셀 기반인지 Edge 기반인지 분기

   ↓

GetCellPositions 또는 GetEdgePositions로 실제 점유 좌표 계산

   ↓

IsCellAt / IsSpaceValid로 Grid 범위 검사

   ↓

IsSpaceFree 또는 IsSpaceOccupied로 기본 점유 상태 검사

   ↓

필요한 경우

IsSpaceOccupiedByMultitileObject / IsSpaceOccupiedByEdgeObject 추가 검사

   ↓

최종 배치 가능 여부 결정

이 흐름에서 중요한 점은 공간 검사가 현재 클릭한 좌표 하나만 보고 수행되지 않는다는 것이다.

먼저 구조물의 크기와 회전을 반영해 실제로 점유할 모든 셀 또는 Edge를 계산하고, 그 결과를 기준으로 여러 단계의 검사를 수행한다.

가장 먼저 해야 하는 것은 Grid 범위 유효성 검사다.

존재하지 않는 공간을 대상으로 점유 여부를 검사하는 것은 의미가 없기 때문이다.

그 다음에는 현재 공간이 비어 있는지, 혹은 이미 점유되어 있어야 하는지 같은 기본 검사로 들어간다.

마지막으로 구조물 종류에 따라 멀티타일 구조물 충돌이나 벽 교차 같은 특수 검사가 추가된다.

공간 검사 시스템은 단일 boolean 함수가 아니라, 좌표 계산을 전제로 여러 종류의 검사를 조합해 최종 배치 가능 여부를 판단하는 단계적 판정 구조라고 볼 수 있다.

이 구조 덕분에 일반 가구 배치, 벽 배치, 벽 내부 구조물 배치처럼 규칙이 다른 시스템도 같은 데이터 계층 위에서 안정적으로 처리할 수 있다.

3. 구현

3.1. Grid 범위의 가장 작은 단위 검사
internal bool IsCellAt(Vector3Int tempPos)
{
    if (tempPos.x >= xGridBoundMin && tempPos.x <= xGridBoundMax 
        && tempPos.z >= zGridBoundMin && tempPos.z <= zGridBoundMax)
        return true;
    return false;
}

이 함수는 특정 셀이 현재 PlacementGridData가 관리하는 범위 안에 존재하는지를 확인하는 가장 기본적인 검사 함수다.

코드는 매우 짧지만, 공간 검사 시스템 전체에서 가장 아래에 있는 기초 함수라고 볼 수 있다.

이 함수가 하는 일은 단순하다.

전달받은 tempPos의 x 좌표가 최소/최대 범위 안에 있는지, 그리고 z 좌표도 최소/최대 범위 안에 있는지를 검사한다.

이 프로젝트의 Grid는 실질적으로 평면 기준으로 동작하기 때문에 y 좌표는 검사 대상에서 제외된다.

이 함수는 3차원 좌표 타입인 Vector3Int를 쓰지만, 실제로는 XZ 평면상의 유효 영역을 검사하고 있는 셈이다.

여기서 사용된 Vector3Int는 Unity가 제공하는 정수 좌표 구조체다.

Grid 시스템에서는 부동소수점 위치보다 정수 셀 좌표가 더 적합하기 때문에, 좌표 데이터를 Vector3가 아니라 Vector3Int로 관리하는 것이 자연스럽다.

장점은 셀 단위 계산이 명확해지고 부동소수점 오차를 피할 수 있다는 점이다.

단점은 연속적인 공간 표현에는 적합하지 않지만, 현재 시스템은 셀 기반 배치이므로 오히려 적합하다.

이 함수가 중요한 이유는 이후의 모든 검사 함수가 이 판정을 전제로 하기 때문이다.

예를 들어 구조물이 차지할 셀 목록을 계산했더라도, 그 좌표가 실제 Grid 범위 밖이라면 더 이상의 충돌 검사나 Dictionary 조회는 의미가 없다.

IsCellAt은 단순한 범위 비교가 아니라, 공간 검사 로직이 잘못된 좌표를 대상으로 동작하지 않도록 막는 첫 번째 안전 장치다.

코드는 짧지만 시스템 전체의 기반 조건을 보장하는 함수라는 점에서 매우 중요하다.

3.2. 전체 점유 영역의 유효성 검사
internal bool IsSpaceValid(Vector3Int currentTilePosition, Vector2Int objectSize, int rotation, bool edgePlacement)
{
    if (edgePlacement)
    {
        List<Edge> edges = GetEdgePositions(currentTilePosition, objectSize, rotation);
        return edges.Any(edgePos => IsCellAt(edgePos.smallerPoint) == false) == false;
    }
    else
    {
        List<Vector3Int> positionsToOccupy = GetCellPositions(currentTilePosition, objectSize, rotation);
        return positionsToOccupy.Any(pos => IsCellAt(pos) == false) == false;
    }
}

이 함수는 현재 구조물이 점유하게 될 전체 공간이 유효한 Grid 안에 있는지를 검사한다.

단일 셀 하나가 유효한지를 보는 것이 아니라, 구조물의 크기와 회전을 반영한 전체 점유 영역을 기준으로 판단한다.

이 프로젝트에서 구조물은 1x1일 수도 있지만, 2x1, 2x2, 혹은 Edge 기반 구조처럼 여러 셀이나 여러 Edge를 동시에 차지할 수도 있다.

따라서 배치 가능 여부를 판단하려면 반드시 전체 범위를 대상으로 검사해야 한다.

코드를 보면 가장 먼저 edgePlacement 값으로 분기한다.

이 값은 현재 구조물이 셀 기반인지, Edge 기반인지를 나타낸다.

셀 기반이면 GetCellPositions를 호출해서 점유할 모든 셀 좌표를 구하고, Edge 기반이면 GetEdgePositions를 호출해서 점유할 모든 Edge를 구한다.

여기서 중요한 점은 공간 검사를 직접 하기 전에 먼저 실제 점유 좌표를 계산하는 단계가 존재한다는 것이다.

IsSpaceValid는 좌표 계산 함수와 강하게 연결된 검사 함수다.

이 함수는 현재 위치 하나가 유효한지를 보는 것이 아니라, 이 구조물이 실제로 점유하게 될 전부가 유효한지를 판단한다.

Edge 분기에서는 edges.Any(edgePos => IsCellAt(edgePos.smallerPoint) == false) == false라는 형태를 사용한다.

이 표현은 처음 보면 복잡해 보이지만 의미는 분명하다.

계산된 Edge들 중 하나라도 유효하지 않은 좌표를 포함하면 false를 반환하겠다는 뜻이다.

여기서 Any는 C# LINQ에서 제공하는 함수로, 컬렉션 안에 조건을 만족하는 원소가 하나라도 있는지를 검사한다.

장점은 하나라도 잘못된 값이 있으면 실패라는 의도를 아주 짧게 표현할 수 있다는 점이다.

단점은 익숙하지 않은 사람에게는 중첩된 람다식이 직관적으로 보이지 않을 수 있다는 점이다.

그러나 현재 구조에서는 코드 의도를 간결하게 표현하는 데 적절하다.

셀 기반 분기에서도 같은 구조를 사용한다.

GetCellPositions로 구한 모든 좌표 중 하나라도 IsCellAt를 통과하지 못하면 false다.

이 함수는 부분적으로만 유효한 공간을 허용하지 않는다.

2x2 구조물이라면 네 칸이 모두 유효해야만 배치 가능하다.

멀티타일 구조물은 일부만 Grid 안에 있고 일부는 Grid 밖인 상태를 허용하면 안 되기 때문이다.

따라서 IsSpaceValid는 단순한 범위 검사라기보다, 멀티타일 구조물의 원자성을 보장하는 함수라고 볼 수 있다.

구조물은 전체가 유효하든지, 아니면 전체가 실패해야 한다는 규칙을 코드로 구현한 셈이다.

3.3. 공간이 비어 있는지 검사
public bool IsSpaceFree(Vector3Int currentTilePosition, Vector2Int objectSize, int rotation, bool edgePlacement)
{
    if (edgePlacement)
    {
        List<Edge> edges = GetEdgePositions(currentTilePosition, objectSize, rotation);
        return gridEdgesDictionary.Keys.Any(edgePos => edges.Any(x => x == edgePos)) == false;
    }
    else
    {
        List<Vector3Int> positionsToOccupy = GetCellPositions(currentTilePosition, objectSize, rotation);
        return gridCellsDictionary.Keys.Any(pos => positionsToOccupy.Any(x => x == pos)) == false;
    }
}

이 함수는 특정 공간이 비어 있는지 검사한다.

배치 시스템에서 가장 자주 사용되는 판정 중 하나이며, 일반 가구나 바닥처럼 빈 공간에만 놓일 수 있는 구조물에 대해 핵심 역할을 한다.

여기서도 먼저 edgePlacement 값으로 분기한다.

셀 기반 구조물이라면 GetCellPositions로 전체 점유 셀을 계산하고, Edge 기반 구조물이라면 GetEdgePositions로 전체 점유 Edge를 계산한다.

즉 IsSpaceFree는 여전히 현재 좌표 하나를 검사하는 함수가 아니라, 구조물이 실제로 차지할 전체 공간을 기준으로 동작한다.

셀 기반 분기에서 핵심 코드는 gridCellsDictionary.Keys.Any(pos => positionsToOccupy.Any(x => x == pos)) == false다.

이 표현은 기존에 점유된 셀 중 하나라도 내가 새로 점유하려는 셀 목록과 겹치면 false라는 뜻이다.

다시 말해, 기존 점유 셀과 새 점유 셀 사이에 교집합이 전혀 없을 때만 true를 반환한다.

Edge 기반 분기도 정확히 같은 논리를 Edge에 대해 수행한다.

이 함수의 핵심은 겹치는 좌표가 하나라도 존재하는지를 찾는 것이다.

이 함수에서 주목할 점은 Dictionary의 값을 보지 않고 Keys만 사용한다는 것이다.

여기서는 어떤 구조물이 있는지가 아니라, 해당 좌표가 이미 점유되어 있는지만 알면 되기 때문이다.

점유 여부 판정은 키 수준에서 충분히 끝난다.

값에 들어 있는 구조물 전체 데이터를 불러오지 않아도 되므로, 검사 비용을 최소화할 수 있다.

또한 Any를 중첩해서 사용하는 방식은 하나라도 겹치면 실패라는 로직을 그대로 반영한다.

이 구조의 장점은 의도가 명확하다는 것이다.

반면 성능만 놓고 보면 새 좌표 목록을 돌면서 ContainsKey를 하는 방식이 더 직접적일 수 있다.

하지만 현재 구조는 점유 좌표 수가 제한적이고, 코드 해석 면에서는 훨씬 읽기 쉽다.

결국 IsSpaceFree는 단순히 비었지를 묻는 함수가 아니라, 현재 PlacementGridData 전체 상태와 새 구조물의 점유 영역을 비교해 충돌 여부를 판단하는 핵심 충돌 검사 함수다.

3.4. 공간이 이미 점유되어 있는지 검사
public bool IsSpaceOccupied(Vector3Int currentTilePosition, Vector2Int objectSize, int rotation, bool edgePlacement)
{
    if (edgePlacement)
    {
        List<Edge> edges = GetEdgePositions(currentTilePosition, objectSize, rotation);
        return edges.All(edgePos => gridEdgesDictionary.Keys.Contains(edgePos));
    }
    else
    {
        List<Vector3Int> positionsToOccupy = GetCellPositions(currentTilePosition, objectSize, rotation);
        return positionsToOccupy.All(pos => gridCellsDictionary.Keys.Contains(pos));
    }
}

IsSpaceOccupied는 이름상 IsSpaceFree의 반대처럼 보이지만, 실제 의미는 조금 다르다.

IsSpaceFree는 겹치는 것이 하나라도 없는지를 검사했다면, IsSpaceOccupied는 내가 차지하려는 모든 공간이 이미 점유되어 있는지를 검사한다.

하나만 존재한다고 true가 되는 것이 아니라, 전체 영역이 전부 채워져 있어야 true를 반환한다.

그래서 여기서는 Any가 아니라 All을 사용한다.

All 역시 LINQ 함수로, 컬렉션의 모든 원소가 조건을 만족해야 true를 반환한다.

셀 기반 분기에서는 positionsToOccupy의 모든 좌표가 gridCellsDictionary.Keys.Contains(pos)를 만족해야 하고, Edge 기반 분기에서는 edges의 모든 원소가 gridEdgesDictionary 안에 있어야 한다.

이 함수는 현재 선택 공간 위에 이미 기반 구조가 모두 깔려 있는지를 판단하는 함수다.

이 함수가 왜 필요한지도 중요하다.

일반 가구는 빈 공간이 필요하지만, 벽 내부 구조물이나 특정 교체 로직은 오히려 해당 공간이 이미 채워져 있어야만 실행할 수 있다.

예를 들어 창문이나 문은 벽이 없는 곳에는 놓을 수 없고, 이미 벽이 있는 위치에서만 교체 형태로 들어가야 한다.

이런 경우에는 공간이 비어 있는지가 아니라 전체 공간이 이미 점유되어 있는지를 검사해야 한다.

그래서 IsSpaceFree와 IsSpaceOccupied는 서로 비슷한 함수가 아니라, 완전히 다른 배치 규칙을 담당하는 함수다.

코드 관점에서 보면 둘 다 GetCellPositions / GetEdgePositions와 Dictionary 키 비교를 사용하지만, 비교 방식이 Any냐 All이냐에 따라 의미가 완전히 달라진다.

결국 IsSpaceOccupied는 배치 기반이 충분히 존재하는지를 판단하는 함수라고 보는 것이 맞다.

일반 충돌 방지용 함수라기보다, 기존 구조물 위에만 설치 가능한 배치 타입을 지원하기 위한 기반 존재 검사 함수다.

3.5. 멀티타일 구조물 점유 검사
internal bool IsSpaceOccupiedByMultitileObject(Vector3Int currentTilePosition, Vector2Int objectSize, int rotation, bool edgePlacement)
{
    if (edgePlacement)
    {
        List<Edge> edges = GetEdgePositions(currentTilePosition, objectSize, rotation);
        Vector3Int offset;
        if (rotation == 0 || rotation == 180)
            offset = Vector3Int.back;
        else
            offset = Vector3Int.left;
        foreach (Edge edge in edges)
        {
            if (gridCellsDictionary.ContainsKey(edge.smallerPoint) == false || gridCellsDictionary.ContainsKey(edge.smallerPoint + offset) == false)
                continue;
            if (gridCellsDictionary[edge.smallerPoint].gameObjectIndex == gridCellsDictionary[edge.smallerPoint + offset].gameObjectIndex)
                return true;
        }
        return false;
    }
    throw new NotImplementedException();
}

이 함수는 일반적인 빈 공간 검사보다 훨씬 특수한 문제를 다룬다.

주석에도 적혀 있듯이, 특정 Edge의 양쪽 셀이 동일한 멀티타일 구조물에 의해 점유되어 있는지를 확인하는 함수다.

현재 Edge가 사실상 하나의 큰 구조물 내부를 가로지르고 있는지를 검사하는 것이다.

이건 일반적인 ContainsKey만으로는 알 수 없는 정보다.

왜냐하면 단순 점유 여부가 아니라, 양쪽 셀이 같은 구조물에 속하는가를 확인해야 하기 때문이다.

코드를 보면 이 함수는 현재 edgePlacement == true인 경우만 구현되어 있다.

그렇지 않은 경우에는 NotImplementedException을 던진다.

이건 아직 셀 기반 케이스는 구현하지 않았고, 현재 시스템에서 필요한 Edge 기반 특수 판정만 먼저 구현했다는 의미다.

이 예외를 명시적으로 던지는 것은 좋은 습관이다.

조용히 false를 반환하는 것보다, 아직 지원하지 않는 분기라는 사실을 분명히 드러내기 때문이다.

먼저 GetEdgePositions를 통해 현재 구조물이 점유할 Edge들을 구한다.

그 다음 회전에 따라 offset을 다르게 잡는다.

회전이 0 또는 180이면 Vector3Int.back, 나머지인 90 또는 270이면 Vector3Int.left를 쓴다.

이 계산이 중요한 이유는 Edge의 방향에 따라 양옆 셀의 기준이 달라지기 때문이다.

수평 방향 Edge라면 위/아래 셀을 봐야 하고, 수직 방향 Edge라면 좌/우 셀을 봐야 한다.

이 offset은 바로 그 양쪽 셀 중 두 번째 셀을 찾기 위한 방향 벡터다.

루프 안에서는 각 Edge에 대해 edge.smallerPoint와 edge.smallerPoint + offset 두 좌표를 검사한다.

먼저 두 셀 모두 gridCellsDictionary에 존재하는지를 확인한다.

둘 중 하나라도 비어 있으면 같은 멀티타일 구조물이 양옆에 존재한다고 볼 수 없으므로 continue로 넘어간다.

두 셀이 모두 존재한다면, 그 다음에는 두 셀에 들어 있는 PlacedCellObjectData의 gameObjectIndex를 비교한다.

이 값이 같다는 것은 곧 두 셀이 같은 실제 구조물에 속한다는 뜻이다.

여기서 gameObjectIndex를 비교하는 이유는 같은 origin이나 ID보다도 실제로 동일한 구조물 인스턴스에 연결된 셀인지를 가장 직접적으로 판별할 수 있기 때문이다.

이 함수는 단순히 주변에 구조물이 있나를 보는 것이 아니라, 이 Edge 양쪽이 동일한 멀티타일 구조물로 연결되어 있는가를 판정하는 특수 충돌 검사다.

이런 함수가 필요한 이유는 배치 시스템이 단순 셀 점유만으로는 표현되지 않는 구조적 관계까지 고려해야 하기 때문이다.

특히 벽 근처 배치나 벽 내부 구조물 판정에서 매우 유용한 보조 검사 역할을 한다.

3.6. Edge 구조물 교차 검사
internal bool IsSpaceOccupiedByEdgeObject(Vector3Int currentTilePosition, Vector2Int objectSize, int rotation, bool edgePlacement)
{
    HashSet<Edge> edges = new();
    List<Vector3Int> cellsToOccupy = GetCellPositions(currentTilePosition, objectSize, rotation);
    foreach (var cellPosition in cellsToOccupy)
    {
        Vector3Int offset = cellPosition - currentTilePosition;

        if (offset.x == 0 && offset.z == 0)
            continue;
        if (offset.z == 0 && offset.x <= objectSize.x)
            edges.UnionWith(GetEdgePositions(cellPosition, Vector2Int.one, 270));
        else if (offset.x == 0 && offset.z <= objectSize.y)
            edges.UnionWith(GetEdgePositions(cellPosition, Vector2Int.one, 0));
        else
        {
            edges.UnionWith(GetEdgePositions(cellPosition, Vector2Int.one, 0));
            edges.UnionWith(GetEdgePositions(cellPosition, Vector2Int.one, 270));
        }
    }
    foreach (var edgePos in edges)
    {
        if (IsEdgeObjectAt(edgePos))
        {
            return true;
        }
    }
    return false;
}

이 함수는 셀 기반 구조물이 배치될 때, 그 구조물이 지나가는 영역에 Edge 기반 구조물이 존재하는지를 검사한다.

즉 일반 오브젝트나 가구가 벽이나 경계 구조물을 가로지르는지를 판정하는 함수다.

이 검사가 필요한 이유는 셀 기반 오브젝트와 Edge 기반 오브젝트가 서로 다른 Dictionary에 저장되기 때문이다.

셀 점유만 검사해서는 벽과의 교차를 알 수 없다.

따라서 셀 기반 구조물의 주변에서 가능한 모든 교차 Edge를 계산한 뒤, 그 Edge들 중 실제로 점유된 것이 있는지 확인해야 한다.

처음에 HashSet<Edge> edges = new()를 사용하는 부분이 중요하다.

이 함수는 여러 셀을 돌면서 각 셀 주변의 Edge를 수집하게 되는데, 같은 Edge가 여러 번 나올 수 있다.

HashSet은 중복 없는 집합을 표현하는 C# 컬렉션이고, UnionWith를 통해 여러 Edge를 합치더라도 같은 Edge는 한 번만 유지된다.

장점은 중복 제거가 자동이라는 점이고, 단점은 순서가 보장되지 않는다는 점이다.

하지만 이 함수는 순서가 중요하지 않고 중복 없이 교차 후보 Edge를 모으는 것이 목적이므로 HashSet이 적합하다.

그 다음 GetCellPositions로 현재 구조물이 점유할 셀 목록을 구한다.

이 자체는 셀 기반 구조물의 점유 셀을 계산하는 표준 로직이다.

각 셀에 대해 offset = cellPosition - currentTilePosition을 계산한다.

이 offset은 현재 셀이 원점 셀로부터 얼마나 떨어져 있는지를 의미한다.

이 정보를 바탕으로 어떤 방향의 Edge를 검사해야 하는지를 결정한다.

if (offset.x == 0 && offset.z == 0)인 경우는 현재 원점 셀 자체이므로 건너뛴다.

그 다음 조건들은 각 셀이 구조물의 어느 가장자리에 해당하는지를 해석하는 부분이다.

offset.z == 0 && offset.x <= objectSize.x이면 해당 셀은 가로 방향 가장자리이므로 270도 방향의 Edge를 검사하고, offset.x == 0 && offset.z <= objectSize.y이면 세로 방향 가장자리이므로 0도 방향의 Edge를 검사한다.

마지막 else는 내부 또는 코너 셀에 해당하므로 두 방향 Edge를 모두 검사한다.

이 함수는 단순히 모든 셀 주변의 벽을 무식하게 보는 것이 아니라, 현재 구조물의 형태와 셀의 상대 위치를 이용해 실제로 교차 가능성이 있는 Edge만 수집하는 구조다.

마지막 루프에서는 수집된 모든 Edge에 대해 IsEdgeObjectAt(edgePos)를 호출한다.

이 함수는 gridEdgesDictionary에 해당 Edge가 존재하는지를 확인한다.

하나라도 존재하면 true를 반환하고, 모두 비어 있으면 false다.

최종적으로는 이 셀 기반 구조물이 점유할 영역이 어떤 벽 또는 Edge 구조물과라도 교차하는지를 판정하는 함수다.

이 함수가 있기 때문에 셀 기반 구조물 배치 시에도 벽과의 충돌을 놓치지 않을 수 있다.

결국 IsSpaceOccupiedByEdgeObject는 셀 기반 공간 검사와 Edge 기반 공간 검사를 연결하는 다리 역할을 하는 특수 검사 함수라고 볼 수 있다.

4. 개발 의도

이 공간 검사 시스템의 핵심 설계 의도는 배치 가능 여부를 하나의 거대한 조건문으로 처리하지 않고, 서로 다른 책임을 가진 작은 검사 함수들로 분리하는 것이다.

Grid 범위 검사는 IsCellAt와 IsSpaceValid가 맡고, 기본 점유 여부는 IsSpaceFree와 IsSpaceOccupied가 맡으며, 멀티타일 구조물과 Edge 구조물처럼 일반 규칙으로 처리할 수 없는 특수한 경우는 IsSpaceOccupiedByMultitileObject와 IsSpaceOccupiedByEdgeObject가 맡는다.

이런 구조 덕분에 각 함수의 의미가 명확해지고, 특정 배치 전략에서 필요한 검사만 골라서 조합할 수 있게 된다.

또한 이 시스템은 PlacementGridData의 Dictionary 구조를 최대한 활용하도록 설계되어 있다.

셀은 셀 Dictionary에서, Edge는 Edge Dictionary에서 검사하고, 필요할 때만 두 체계를 연결한다.

이 방식 덕분에 좌표 표현 체계를 섞지 않고도 정확한 충돌 판정이 가능해진다.

코드 차원에서는 GetCellPositions, GetEdgePositions, ContainsKey, Any, All, HashSet, UnionWith 같은 C# 컬렉션 기능과 LINQ를 적극적으로 사용해 검사 의도를 짧고 명확하게 표현하고 있다.

결과적으로 이 공간 검사 시스템은 단순 boolean 함수들의 모음이 아니라, 건축 시스템의 배치 규칙을 코드로 분해한 핵심 판정 엔진이다.

이후의 SelectionStrategy나 PlacementValidator는 이 함수들을 조합해서 각 배치 타입에 맞는 규칙을 만들고, PlacementManager는 그 결과를 바탕으로 실제 배치를 수행한다.

이 계층의 정확성이 곧 건축 시스템 전체의 신뢰도를 결정한다고 볼 수 있다.