GridData 상위 통합 구조 (Grid 계층 분리와 공간 확장 설계)

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. GridData 클래스 구조

       3.2. Grid 확장 구조와 생성자

       3.3. 전체 Grid 초기화

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서 가장 중요한 문제 중 하나는 '같은 위치를 여러 종류의 구조물이 어떻게 공유할 것인가' 이다.

예를 들어 바닥 타일이 존재하는 위치에 가구를 배치하는 것은 정상적인 상황이다.

또한 벽은 셀 내부가 아니라 셀과 셀 사이의 경계에 배치되며, 벽 내부 구조물은 일반 오브젝트와 다른 규칙을 가진다.

이러한 서로 다른 특성을 가진 구조물들을 하나의 Grid 데이터 구조로 관리하면, 정상적인 배치조차 충돌로 처리되는 문제가 발생한다.

따라서 시스템은 단일 Grid가 아니라, 배치 유형에 따라 분리된 여러 Grid 계층을 가져야 한다.

바닥은 바닥끼리, 벽은 벽끼리, 일반 오브젝트는 일반 오브젝트끼리, 벽 내부 구조물은 별도의 계층에서 관리되어야 한다.

이 구조를 통해 동일한 월드 좌표에서도 서로 다른 의미의 데이터가 공존할 수 있으며, 각 계층은 자신에게 맞는 충돌 규칙과 제거 규칙을 독립적으로 유지할 수 있다.

또한 벽은 셀 기반 구조물이 아니라 Edge 기반 구조물이기 때문에, 일반 셀보다 더 넓은 좌표 범위를 가져야 한다.

Grid는 단순히 크기를 지정하는 것이 아니라, 배치 유형에 따라 서로 다른 공간 규칙을 가져야 한다.

이 요구사항을 만족하기 위해 GridData는 여러 PlacementGridData를 통합 관리하는 상위 계층으로 설계되었으며, 각 계층의 역할과 공간 범위를 명확히 구분한다.

2. 흐름도

Grid 시스템 초기화

   ↓

GridData 생성

   ↓

배치 유형별 Grid 계층 생성

   ├─ FloorPlacementData

   ├─ WallPlacementData

   ├─ ObjectPlacementData

   └─ InWallPlacementData

   ↓

각 시스템이 필요한 Grid 계층에 접근

   ↓

배치 / 제거 / 조회 수행

   ↓

전체 초기화 시 GridData.Clear()

이 구조에서 핵심은 Grid를 하나로 보지 않고, 의미에 따라 여러 계층으로 나누어 관리한다는 점이다.

상위 시스템은 Grid 전체를 직접 다루지 않고, 자신에게 필요한 계층만 선택해서 사용한다.

3. 구현

3.1. GridData 클래스 구조
public class GridData
{
    public PlacementGridData WallPlacementData { private set; get; }
    public PlacementGridData FloorPlacementData { private set; get; }
    public PlacementGridData ObjectPlacementData { private set; get; }
    public PlacementGridData InWallPlacementData { private set; get; }

    ...
}

GridData는 건축 시스템에서 사용하는 모든 Grid 데이터를 통합 관리하는 상위 클래스다.

이 클래스의 핵심은 네 개의 PlacementGridData를 별도로 가지고 있다는 점이다.

타입은 동일하지만, 각각이 담당하는 의미가 다르다.

이 코드에서 가장 먼저 봐야 할 것은 왜 필드가 아니라 프로퍼티를 사용했는가이다.

C#에서 프로퍼티는 내부적으로 getter/setter 메서드를 감싸는 구조이며, { private set; get; } 형태를 사용하였다.

이는 외부에서는 값을 읽을 수 있지만, 외부에서 값을 변경하는 것은 막겠다는 의미다.

이 코드는 단순히 데이터를 노출하는 것이 아니라, '읽기는 허용하지만, 구조 자체는 보호한다' 는 의도를 가진다.

GridData는 시스템의 핵심 데이터이기 때문에, 외부에서 PlacementGridData를 교체할 수 있다면 전체 Grid 상태가 쉽게 깨질 수 있다.

따라서 setter를 private으로 제한하여, GridData 내부에서만 초기화가 가능하도록 만든 것이다.

다음으로 중요한 것은 같은 타입을 네 번 선언했다는 점이다.

PlacementGridData WallPlacementData
PlacementGridData FloorPlacementData
PlacementGridData ObjectPlacementData
PlacementGridData InWallPlacementData

PlacementGridData는 셀과 Edge 점유를 관리하는 공통 자료구조지만, GridData는 이 자료구조를 네 개의 서로 다른 계층으로 나누어 사용한다.

이는 자료구조를 분리한 것이 아니라, 같은 자료구조를 서로 다른 의미의 공간으로 분리한 것이다.

이 구조에서 가장 중요한 부분은 왜 Grid를 네 개로 나누었는지다.

이 구조를 이해하기 위해서는 먼저 PlacementGridData 내부 동작을 봐야 한다.

PlacementGridData는 Dictionary 기반으로 동작하며, 일반적으로 Dictionary<Vector3Int, PlacedObjectData> 형태로 데이터를 관리한다.

이 구조는 하나의 좌표가 구조물 데이터를 직접 가지는 것이 아니라, 해당 좌표가 구조물 객체를 참조하는 구조다.

그런데 이 Dictionary를 하나만 쓰면 다음과 같은 문제가 발생한다.

(같은 좌표)
- 바닥 있음
- 가구 있음
- 벽 있음

이 상태에서 충돌 검사는 보통 다음과 같이 이루어진다.

if (dictionary.ContainsKey(position))
    return false;

이 코드는 해당 좌표에 무엇이든 존재하면 배치 불가능으로 처리한다.

즉, 좌표 단위로만 점유 여부를 판단하는 구조다.

하지만 실제 게임에서는 동일한 좌표에 서로 다른 의미의 구조물이 동시에 존재할 수 있다.

바닥 위에 가구를 배치하는 것은 정상이며, 벽과 바닥 역시 서로 다른 계층으로 존재해야 한다.

결국 문제는 서로 다른 계층의 데이터를 하나의 Dictionary에서 동일한 기준으로 검사하고 있다는 점이다.

이 문제를 해결하기 위해 GridData는 구조 자체를 변경한다.

하나의 Dictionary를 확장하는 대신, PlacementGridData 자체를 여러 개로 분리하는 방식을 선택한다.

즉 코드 흐름은 다음과 같이 바뀐다.

FloorPlacementData.IsOccupied(position)
ObjectPlacementData.IsOccupied(position)
WallPlacementData.IsOccupied(edge)

각 계층은 독립적인 Dictionary를 가지며, 충돌 검사 역시 계층별로 수행된다.

이 구조를 통해 동일 좌표라도 서로 다른 의미의 데이터는 충돌하지 않게 된다.

결과적으로 이 설계는 바닥 위에 가구 배치를 가능하게 하고, 벽과 가구 충돌 분리하며, 벽 내부 오브젝트를 독립 처리한다.

이 코드는 단순히 변수를 나눈 것이 아니라, 충돌 판정 로직 자체를 분리하기 위해 데이터 구조를 분리한 설계다.

3.2. Grid 확장 구조와 생성자
public GridData(Vector2Int gridSize)
{
    FloorPlacementData = new(-gridSize.x, gridSize.x - 1, -gridSize.y, gridSize.y - 1);
    WallPlacementData = new(-gridSize.x, gridSize.x, -gridSize.y, gridSize.y);
    ObjectPlacementData = new(-gridSize.x, gridSize.x - 1, -gridSize.y, gridSize.y - 1);
    InWallPlacementData = new(-gridSize.x, gridSize.x, -gridSize.y, gridSize.y);
}

이 생성자는 단순히 Grid를 초기화하는 코드처럼 보이지만, 실제로는 각 계층의 좌표 해석 방식 자체를 정의하는 코드다.

먼저 생성자 호출 방식이다.

new(minX, maxX, minZ, maxZ)

이건 C#의 target-typed new 문법으로, 타입을 생략하고 생성자를 호출하는 방식이다.

컴파일러가 좌측 타입을 보고 자동으로 PlacementGridData로 추론한다.

이 문법은 타입이 명확하지 않은 경우 가독성이 떨어질 수 있지만, 여기서는 프로퍼티 타입이 명확하기 때문에 문제 없다고 생각했다.

다음으로 핵심 로직을 보면,

FloorPlacementData = new(-gridSize.x, gridSize.x - 1, ...)
WallPlacementData  = new(-gridSize.x, gridSize.x, ...)

각 계층의 범위 설정이 다르다.

Floor / Object => gridSize - 1  
Wall / InWall  => gridSize

이 차이는 좌표 시스템의 차이를 반영한 것이다.

PlacementGridData 내부에서는 좌표 유효성 검사를 다음과 같이 수행한다.

if (x < min || x > max) return false;

여기서 max 값이 실제 사용 가능한 마지막 좌표이다.

셀 기반 구조물은 셀 내부에 배치되므로 마지막 셀까지만 필요하다.

그래서 gridSize - 1을 사용한다.

하지만 벽은 다르다.

벽은 다음과 같은 형식으로 배치된다.

[셀][셀][셀]
  |   |   |

벽은 셀 사이의 경계, 즉 Edge에 존재한다.

따라서 마지막 셀 바깥쪽 경계까지 포함해야 한다.

그래서 코드에서 일부러 다음과 같이 설정한다.

WallPlacementData = new(-gridSize.x, gridSize.x, ...)

이는 단순한 보정이 아니라, Edge 기반 좌표 시스템을 반영한 코드이다.

이 처리가 없으면 맵의 가장 바깥 벽을 배치할 수 없게 된다.

또 하나 중요한 부분은 ObjectPlacementData가 Floor와 같은 범위를 가진다는 점이다.

ObjectPlacementData = new(... gridSize - 1 ...)

‍이는 오브젝트는 항상 셀 위에 존재한다는 의미이다.

이는 Floor와 같은 좌표 체계를 공유한다.

이 생성자는 단순 초기화 코드가 아닌, 좌표 유효 범위 정의, 셀/Edge 차이 반영, 계층별 좌표 체계 분리를 동시에 수행한다.

3.3. 전체 Grid 초기화
public void Clear()
{
    FloorPlacementData.Clear();
    WallPlacementData.Clear();
    ObjectPlacementData.Clear();
    InWallPlacementData.Clear();
}

이 함수는 GridData 전체 초기화한다.

PlacementGridData 내부의 Clear는 'dictionary.Clear()'로, 모든 점유 데이터를 제거한다.

Dictionary.Clear는 내부 버킷 구조를 유지한 채, 저장된 Key-Value만 제거한다.

이는 새로운 Dictionary를 생성하는 것이 아니라 기존 구조를 재사용하면서 데이터를 제거한다.

이 방식은 반복적으로 초기화가 발생하는 상황에서 메모리 재할당을 줄이고 성능적으로 유리하다.

GridData.Clear는 이 Clear들을 묶어서 호출한다.

바닥, 벽, 오브젝트, 벽 내부 오브젝트를 모두 동시에 초기화(제거)한다.

여기서 중요한 점은 부분 초기화를 허용하지 않는다는 것이다.

만약 Floor만 Clear하고 Object를 안 지우면, 바닥은 없고 가구만 남는 것과 같은 비정상 상태가 발생한다.

따라서 이 함수는 단순 반복 호출이 아니라, Grid 상태를 항상 일관된 상태로 유지하기 위한 강제 초기화 구조다.

또한 구조적으로 보면, GridData는 전체 흐름 관리를 하고, PlacementGridData는 실제 데이터 삭제로 역할이 분리되어 있다.

4. 개발 의도

GridData의 핵심 설계 의도는 하나의 Grid를 여러 의미 계층으로 나누고, 이를 통합 관리하는 것이다.

바닥, 벽, 오브젝트, 벽 내부 구조물은 같은 공간에 존재할 수 있지만, 같은 규칙으로 처리할 수는 없다.

따라서 이들을 하나의 Grid에 넣지 않고, 서로 다른 계층으로 분리했다.

이 분리를 통해 충돌 판정, 배치 조건, 제거 로직을 단순하게 유지할 수 있다.

또한 GridData는 이 분리된 계층을 하나의 시스템으로 통합한다.

생성자에서는 각 계층의 공간 규칙을 정의하고, Clear에서는 전체 상태를 초기화한다.

GridData는 단순 데이터 컨테이너가 아니라, Grid 시스템 전체의 구조와 규칙을 정의하는 상위 계층이다.

이 구조는 확장성도 고려하고 있다.

새로운 배치 유형이 필요해지면 PlacementGridData를 추가하고, GridData에 새로운 계층을 정의하면 된다.

기존 시스템을 수정하지 않고도 확장이 가능하다.

결과적으로 GridData는 단순한 데이터 묶음이 아니라, 공간을 계층적으로 분리하고, 각 계층의 규칙을 정의하며, 전체 Grid를 하나의 시스템으로 통합하는 핵심 구조다.