Grid 데이터 구조 시스템

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. PlacementGridData 클래스 구조

       3.2. 생성자와 Grid 경계 설정

       3.3. Clear 함수와 상태 초기화

       3.4. Edge 구조와 record 사용

       3.5. PlacementGridSaveData 구조

       3.6. namespace System.Runtime.CompilerServices

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서 가장 먼저 해결해야 하는 문제는 구조물이 현재 맵의 어디를 차지하고 있는지를 논리적으로 정확하게 표현하는 것이다.
이 프로젝트에서는 바닥, 가구, 일반 오브젝트처럼 셀 중심을 기준으로 배치되는 구조물과, 벽처럼 셀과 셀 사이의 경계선인 Edge를 기준으로 배치되는 구조물이 함께 존재한다.

이 둘은 겉보기에는 모두 맵 위에 놓이는 구조물이지만, 논리적으로는 전혀 다른 좌표 체계를 사용한다.

셀 기반 구조물은 Grid의 칸을 점유하고, Edge 기반 구조물은 칸이 아니라 칸과 칸 사이의 선분을 점유한다.

따라서 이 둘을 같은 방식으로 저장하려 하면, 벽의 위치를 정확하게 표현할 수 없고, 충돌 검사나 제거, 저장/복원 단계에서 예외 처리가 급격히 늘어나게 된다.

또한 건축 시스템은 단순히 저장만 하면 되는 구조가 아니다.

이후 단계에서 특정 공간이 비어 있는지, 이미 점유되었는지, 유효한 Grid 범위 안에 있는지, 멀티타일 구조물이 기존 벽이나 다른 구조물을 가로지르지 않는지 등을 매우 자주 계산해야 한다.

데이터 구조는 기록용 저장소이면서 동시에 공간 계산의 기준점 역할도 수행해야 한다.

따라서 PlacementGridData의 핵심 요구사항은 분명하다.

셀 기반 구조물과 Edge 기반 구조물을 분리해서 관리할 수 있어야 하고, 특정 위치에 대한 조회가 빨라야 하며, 구조물 하나가 여러 셀 또는 여러 Edge를 점유하더라도 이를 하나의 구조물로 다룰 수 있어야 한다.

더 나아가, 나중에 저장 데이터를 만들 때는 중복 없이 구조물 단위로 다시 재구성할 수 있어야 한다.

이 클래스는 단순한 데이터 클래스가 아니라, 건축 시스템 전체의 논리적 공간 상태를 책임지는 핵심 데이터 계층이어야 한다.

2. 흐름도

건축 시스템 초기화

   ↓

PlacementGridData 생성

   ↓

셀 기반 / Edge 기반 Dictionary 초기화

   ↓

구조물 배치 시

   ├─ 셀 기반 → gridCellsDictionary 저장

   └─ Edge 기반 → gridEdgesDictionary 저장

   ↓

조회 / 선택 / 제거 / 저장 시스템에서 사용

이 구조에서 핵심은 PlacementGridData가 단순히 데이터를 들고만 있는 클래스가 아니라는 점이다.
상위 시스템에서 구조물 배치나 제거 요청이 들어오면, PlacementGridData는 현재 구조물의 크기와 회전을 기준으로 실제 점유 좌표를 계산하고, 그 좌표를 기준으로 현재 상태를 조회하거나 수정한다. 즉 “구조물이 실제로 어디를 차지하는가”를 계산하는 단계와, “그 결과를 데이터 구조에 반영하는 단계”가 이 클래스 안에서 자연스럽게 연결된다. 결국 Selection, Validator, PlacementManager, Command, Save 시스템은 모두 PlacementGridData가 계산하고 저장한 결과를 신뢰하고 동작하게 된다. 그래서 이 클래스는 데이터 저장소이면서 동시에 공간 논리의 기준점이다.

3. 구현

3.1. PlacementGridData 클래스 구조
public class PlacementGridData
{
    Dictionary<Vector3Int, PlacedCellObjectData> gridCellsDictionary;
    Dictionary<Edge, PlacedEdgeObjectData> gridEdgesDictionary;

    int xGridBoundMin, xGridBoundMax, zGridBoundMin, zGridBoundMax;

    ...
}

이 클래스의 핵심은 두 개의 Dictionary다.

gridCellsDictionary는 셀 기반 구조물을 저장하고, gridEdgesDictionary는 Edge 기반 구조물을 저장한다.

이 코드가 실제로 의미하는 것은 다음과 같다.

gridCellsDictionary는 특정 Vector3Int 좌표를 키로 사용한다.

이 셀 위치에 어떤 구조물이 존재하는지를 바로 찾기 위한 구조다.

예를 들어 (3, 0, 5)라는 좌표가 키로 들어가 있다면, 그 좌표에는 반드시 어떤 구조물이 존재한다는 의미다.

반면 gridEdgesDictionary는 Edge를 키로 사용한다.

Edge는 두 개의 좌표로 구성된 선분이다.

즉 이 Dictionary는 이 선분 위치에 벽이 존재하는지를 표현한다.

이렇게 두 개로 나눈 이유는 코드 구조에서 명확하게 드러난다.

셀과 Edge는 완전히 다른 타입을 키로 사용한다.

이건 단순 분리가 아니라, 아예 좌표 개념 자체를 분리한 설계다.

C#의 Dictionary를 사용한 이유는 명확하다.

이 클래스는 이후 계속 이 좌표에 뭐 있는지를 묻는 구조다.

Dictionary는 내부적으로 해시 기반이라, 키로 바로 접근이 가능하다.

Dictionary는 평균적으로 O(1)에 가까운 조회 성능을 제공하기 때문에, 이 위치에 무엇이 있는지를 빠르게 판단할 수 있다.

이 구조의 가장 중요한 장점은 셀과 Edge를 완전히 분리함으로써, 각각의 배치 규칙을 단순하게 유지할 수 있다는 것이다.

복잡한 분기 대신, 셀은 셀대로, 벽은 벽대로 처리하는 구조를 만든 것이다.

이 구조에서 중요한 점은 하나의 구조물이 하나의 좌표에만 저장되는 것이 아니라, 구조물이 점유하는 모든 좌표에 동일한 데이터를 공유하는 방식으로 저장된다는 점이다.

예를 들어 2x2 크기의 구조물이 (3,0,5)를 기준으로 배치되었다고 가정하면, 실제로는 (3,0,5), (4,0,5), (3,0,6), (4,0,6) 총 4개의 좌표를 점유하게 된다.

이때 gridCellsDictionary에는 이 4개의 좌표가 각각 키로 등록되지만, 값으로는 동일한 PlacedCellObjectData 인스턴스를 참조하게 된다.

즉, Dictionary는 좌표 단위로 데이터를 저장하지만, 실제 구조물 데이터는 하나만 존재하고 여러 좌표에서 공유되는 구조다.

이 구조 덕분에 특정 좌표 하나만 가지고도 해당 구조물의 전체 정보, 예를 들어 구조물 ID, origin 위치, 점유 범위 등을 역으로 추적할 수 있다.

또한 제거 시에도 특정 좌표 하나를 기준으로 전체 점유 영역을 찾아 한 번에 제거할 수 있다.

이 설계는 Grid 기반 건축 시스템에서 멀티타일 구조물을 효율적으로 관리하기 위한 핵심 구조다.

이 구조에서 중요한 또 하나의 포인트는 Dictionary에 저장되는 값 자체가 단순한 식별자가 아니라, 구조물의 전체 정보를 포함하고 있는 데이터라는 점이다.

gridCellsDictionary의 값인 PlacedCellObjectData에는 해당 구조물의 ID, origin 위치, 점유하고 있는 전체 셀 목록, 그리고 실제 생성된 GameObject의 인덱스 정보 등이 포함된다.

이렇게 설계된 이유는 특정 좌표 하나만 가지고도 해당 구조물의 전체 정보를 역으로 추적할 수 있도록 하기 위함이다.

예를 들어 어떤 셀 하나를 클릭했을 때, 그 셀에 해당하는 구조물이 어떤 구조물인지, 전체 점유 범위가 어디까지인지, 어떤 GameObject와 연결되어 있는지를 바로 알 수 있어야 한다.

즉, Dictionary는 좌표를 키로 사용하지만, 값에는 단순 참조가 아니라 구조물 전체 상태를 담고 있기 때문에, 이 구조 하나만으로 선택, 제거, 이동, 저장과 같은 모든 상위 시스템이 필요한 정보를 얻을 수 있다.

이 구조 덕분에 별도의 매핑 구조 없이도 좌표 하나만으로 구조물 단위의 처리가 가능해진다.

3.2. 생성자와 Grid 경계 설정
public PlacementGridData(int xMin, int xMax, int zMin, int zMax)
{
    gridCellsDictionary = new();
    gridEdgesDictionary = new();
    xGridBoundMax = xMax;
    zGridBoundMax = zMax;
    xGridBoundMin = xMin;
    zGridBoundMin = zMin;
}

이 생성자는 단순 초기화처럼 보이지만, 실제로는 두 가지 역할을 수행한다.

첫 번째는 Dictionary 생성이다.

new() 문법은 타입을 생략하고 객체를 생성하는 C# 문법이다.

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

이 문법은 코드 길이를 줄여주지만, 타입이 불명확한 경우에는 가독성이 떨어질 수 있다.

여기서는 필드 타입이 명확하기 때문에 문제 없다.

두 번째는 Grid 경계 설정이다.

이 코드에서 중요한 부분은 이 네 줄이다.

xGridBoundMax = xMax;
zGridBoundMax = zMax;
xGridBoundMin = xMin;
zGridBoundMin = zMin;

이건 단순 변수 저장이 아니라, 이 PlacementGridData가 관리할 수 있는 영역을 정의하는 코드다.

이 객체는 무한 공간이 아니라, 정해진 범위 안에서만 유효한 데이터를 가진다.

이 값들은 이후 IsCellAt, IsSpaceValid 같은 함수에서 이 좌표가 유효한 Grid 안인지를 판단할 때 사용된다.

3.3. Clear 함수와 상태 초기화
public void Clear()
{
    gridCellsDictionary.Clear();
    gridEdgesDictionary.Clear();
}

Clear 는 현재 저장된 셀 데이터와 Edge 데이터를 모두 지우는 함수다.

이 함수는 짧지만 의미가 크다.

건축 시스템에서 전체 맵을 리셋하거나 저장 데이터를 새로 불러오기 전에 기존 상태를 비울 때 필요하다.

여기서 중요한 점은 셀과 Edge를 함께 지운다는 것이다.

둘 중 하나만 남으면 시스템 상태가 불일치하게 된다.

예를 들어 셀 기반 오브젝트는 지워졌는데 벽 정보만 남아 있거나, 반대로 벽은 지워졌는데 오브젝트만 남아 있으면 이후 충돌 검사와 선택 결과가 전부 흔들릴 수 있다.

따라서 Clear는 단순 삭제가 아니라, PlacementGridData를 다시 완전히 빈 상태로 되돌리는 함수다.

Dictionary.Clear()를 쓰는 이유는 내부 원소만 제거하고 컬렉션 객체 자체는 재사용할 수 있기 때문이다.

새 Dictionary를 다시 만드는 것보다 더 직접적이고 의도도 명확하다.

3.4. Edge 구조와 record 사용
public record Edge(Vector3Int smallerPoint, Vector3Int biggerPoint);

Edge는 이 프로젝트의 핵심 보조 자료형이다.

벽이나 벽 내부 구조물은 셀 하나가 아니라 선분을 점유하기 때문에, 그 선분을 표현할 전용 타입이 필요하다.

Edge는 단순히 두 점을 저장하는 구조가 아니라, 실제로 Dictionary의 키로 사용되기 때문에 항상 동일한 선분이 동일한 형태로 표현되도록 보장해야 한다.

예를 들어 (A, B)와 (B, A)는 동일한 선분이지만, 그대로 저장하면 서로 다른 키로 인식될 수 있다.

이를 방지하기 위해 Edge는 생성 시 항상 smallerPoint와 biggerPoint로 정렬된 상태를 유지한다.

이 정규화 과정을 통해 동일한 선분은 언제 생성되더라도 항상 동일한 키로 생성되며, Dictionary에서 정확하게 조회될 수 있다.

C#의 record는 값 기반 동등성을 제공한다.

즉, 내용이 같으면 같은 객체처럼 비교된다.

이건 Dictionary 키로 사용할 때 특히 중요하다.

같은 선분을 표현하는 Edge가 두 번 계산되더라도 smallerPoint와 biggerPoint가 같으면 같은 키로 인식되어야 하기 때문이다.

만약 일반 class를 썼다면 참조 비교가 기본이어서, 내용이 같아도 서로 다른 객체면 다른 키처럼 동작할 위험이 있다.

record를 사용함으로써 같은 선분은 항상 같은 Edge라는 도메인 규칙을 코드 수준에서 자연스럽게 보장하게 된다.

여기서 smallerPoint와 biggerPoint로 필드를 나눈 것도 중요하다.

선분은 본질적으로 양방향이지만, 데이터 저장과 조회에서는 항상 같은 정규화 기준이 필요하다.

(A, B)와 (B, A)를 같은 선분으로 취급해야 한다.

그래서 Edge를 생성할 때 항상 더 작은 쪽을 smallerPoint, 더 큰 쪽을 biggerPoint로 정렬해서 저장한다.

이렇게 해야 Edge를 Dictionary 키로 쓸 수 있고, 조회할 때도 일관성이 유지된다.

다시 말해 Edge는 단순 DTO가 아니라, 선분을 키로 사용하기 위해 정규화된 값 객체다.

3.5. PlacementGridSaveData 구조
[Serializable]
public struct PlacementGridSaveData
{
    public EdgesData edgesData;
    public List<EdgeObjectSaveData> edgesObjectData;
    public List<Vector3Int> cellsData;
    public List<CellObjectSaveData> cellObjectData;
}

PlacementGridSaveData는 PlacementGridData 내부 상태를 저장용 구조로 옮길 때 사용하는 구조체다.

여기서 struct를 쓴 이유는 이 타입을 행동하는 객체가 아니라 데이터 묶음으로 쓰기 때문이다.

C#의 struct는 값 형식이므로, 개념적으로는 상태 전달용 그릇에 더 가깝다.

물론 내부에 List 같은 참조형 필드가 있으므로 완전한 깊은 복사가 자동으로 보장되는 것은 아니지만, 이 코드의 의도는 분명하다.

PlacementGridSaveData는 로직을 가지지 않고, 저장 시점에 만들어지는 결과 데이터를 담는 역할에 집중한다.

PlacementGridData 내부에서는 하나의 구조물이 여러 좌표에 중복 저장되어 있기 때문에, 이를 그대로 저장하면 동일 구조물이 여러 번 저장되는 문제가 발생한다.

따라서 저장 단계에서는 origin을 기준으로 동일 구조물을 하나로 묶고, 중복 데이터를 제거한 뒤 구조물 단위로 재구성하는 과정이 필요하다.

PlacementGridSaveData는 이러한 변환 결과를 담는 구조이며, 런타임 데이터 구조와 저장 데이터 구조를 분리하기 위한 설계다.

필드를 보면 셀 데이터와 Edge 데이터가 또 한 번 분리되어 있다.

cellsData와 cellObjectData는 셀 기반 구조물용이고, edgesData와 edgesObjectData는 Edge 기반 구조물용이다.

런타임 내부에서는 Dictionary를 사용해 빠른 조회를 수행하지만, 저장 단계에서는 Dictionary를 그대로 직렬화하기보다 별도 구조체로 변환하는 편이 더 안정적이고 명시적이다.

즉, PlacementGridSaveData는 런타임용 내부 구조를 저장/복원에 적합한 형태로 변환한 결과물이라고 볼 수 있다.

3.6. namespace System.Runtime.CompilerServices
namespace System.Runtime.CompilerServices
{
    internal static class IsExternalInit { }
}

이 코드는 기능 로직이라기보다 환경 보정 코드에 가깝다.

record를 사용하려면 C# 컴파일 환경에서 IsExternalInit 타입이 필요할 수 있는데, Unity 환경에서는 프로젝트 설정이나 버전에 따라 이 타입이 기본 제공되지 않는 경우가 있다.

그래서 직접 정의해 record 문법이 정상 작동하도록 맞춘 것이다.

이 네임스페이스 선언은 건축 시스템의 비즈니스 로직을 위한 것이 아니라, 언어 기능인 record를 현재 프로젝트 환경에서도 문제없이 사용할 수 있도록 하는 호환성 코드다.

이 부분을 넣었다는 것은 단순히 문법을 아는 수준을 넘어서, Unity/C# 버전 차이와 컴파일 환경까지 고려해 코드를 안정적으로 굴리기 위한 조치라고 볼 수 있다.

4. 개발 의도

Grid 데이터 구조 시스템의 핵심 설계 의도는 건축 공간을 셀과 경계선이라는 두 개의 서로 다른 논리 단위로 정확하게 분리해 표현하는 것이다.

이 프로젝트에서는 바닥, 가구, 일반 오브젝트와 벽을 같은 방식으로 저장하면 오히려 시스템이 더 복잡해진다.

그래서 셀 기반 구조물은 셀 기준 Dictionary로, 벽 기반 구조물은 Edge 기준 Dictionary로 저장하는 구조를 선택했다.

이 덕분에 이후 배치 검사, 제거, 선택, 저장/복원 로직이 훨씬 명확해졌다.

또한 PlacementGridData를 단순 저장용 클래스가 아니라 공간의 단일 진실 소스로 두려는 의도도 강하다.

위에 있는 시스템들은 모두 이 클래스가 계산하고 보관하는 결과를 신뢰한다.

그래서 데이터 구조 자체가 정확하고, 좌표 표현 방식이 일관되며, 경계와 키 규칙이 명확해야 한다.

Edge를 record로 두고 smaller/bigger 기준을 강제한 것, 저장 구조를 별도의 struct로 분리한 것, 환경 호환을 위해 IsExternalInit까지 보정한 것은 모두 같은 방향의 선택이다.

결과적으로 Grid 데이터 구조 시스템은 단순히 구조물을 저장하는 클래스 모음이 아니라, 건축 시스템 전체가 의존하는 공간 표현의 기반 계층이라고 보는 것이 맞다.

이 구조가 정확하게 설계되어 있기 때문에, 그 위에 올라가는 좌표 계산, 공간 검사, 추가/제거, 조회, 저장/복원 로직도 안정적으로 쌓일 수 있다.