구조물 단위 데이터 설계
1. 시스템 요구 사항
Grid 기반 건축 시스템에서는 구조물을 단순히 하나의 좌표로 표현할 수 없다.
하나의 구조물은 여러 셀을 동시에 점유할 수 있으며, 벽과 같은 구조물은 셀이 아니라 셀 사이의 경계인 Edge를 점유한다.
따라서 '좌표 → 구조물' 이 아니라, '여러 좌표 → 하나의 구조물' 이라는 관계를 표현할 수 있는 데이터 구조가 필요하다.
또한 이 시스템은 특정 좌표 하나를 기준으로 구조물 전체를 선택하거나 제거하는 동작을 수행해야 한다.
이를 위해서는 Grid의 각 좌표가 단순히 개별 데이터를 가지는 것이 아니라, 구조물 전체 정보를 참조할 수 있어야 한다.
요구사항은 명확하다.
좌표 단위가 아니라 구조물 단위로 상태를 정의해야 하며, 특정 좌표 하나를 통해 구조물 전체를 역추적할 수 있어야 한다.
또한 구조물의 위치, 방향, 점유 범위를 모두 표현할 수 있어야 한다.
이 요구사항을 만족시키기 위해 도입된 구조가 PlacedCellObjectData와 PlacedEdgeObjectData이다.
이 두 클래스는 Grid 시스템에서 구조물 하나를 표현하는 기준 데이터이며, 모든 좌표는 이 객체를 공유하는 방식으로 연결된다.
2. 흐름도
구조물 배치
↓
점유 좌표 계산 (GetCellPositions / GetEdgePositions)
↓
PlacedObjectData 생성
↓
여러 좌표가 동일 객체 참조
↓
조회 시
↓
좌표 하나 → 구조물 데이터 → 전체 정보 사용
이 구조의 핵심은 좌표가 데이터를 직접 가지는 것이 아니라, 구조물 데이터를 참조한다는 점이다.
따라서 특정 좌표 하나만 알아도, 그 좌표가 속한 구조물의 전체 정보를 가져올 수 있다.
3. 구현
3.1. 셀/Edge 기반 구조물 데이터
public class PlacedCellObjectData
{
public IEnumerable<Vector3Int> PositionsOccupied { get; private set; }
public int gameObjectIndex, structureID;
public Vector3Int origin;
public int rotation, objectRotation;
public PlacedCellObjectData(int gameObjectIndex,int structureID, IEnumerable<Vector3Int> positionsOccupied, Vector3Int origin, int rotation, int objectRotation)
{
this.gameObjectIndex = gameObjectIndex;
this.PositionsOccupied = positionsOccupied;
this.structureID = structureID;
this.origin = origin;
this.rotation = rotation;
this.objectRotation = objectRotation;
}
}
public class PlacedEdgeObjectData
{
public IEnumerable<Edge> PositionsOccupied { get; private set; }
public int gameObjectIndex, structureID;
public Vector3Int origin;
public int rotation;
public int objectRotation;
public PlacedEdgeObjectData(int gameObjectIndex, int structureID, IEnumerable<Edge> positionsOccupied, Vector3Int origin, int rotation, int objectRotation)
{
this.gameObjectIndex = gameObjectIndex;
this.PositionsOccupied = positionsOccupied;
this.structureID = structureID;
this.origin = origin;
this.rotation = rotation;
this.objectRotation = objectRotation;
}
}이 두 클래스는 Grid 시스템에서 구조물 하나를 표현하는 데이터 객체다.
Dictionary의 키는 셀이나 Edge지만, 실제 값으로는 이 객체가 저장된다. 즉 여러 좌표가 하나의 객체를 공유하는 구조다.
이 구조를 이해할 때 가장 중요한 필드는 PositionsOccupied다.
이 필드는 구조물이 실제로 점유하고 있는 모든 좌표를 담고 있다.
셀 기반 구조물에서는 Vector3Int 목록이며, Edge 기반 구조물에서는 Edge 목록이다.
이 값이 있기 때문에 특정 좌표 하나를 기준으로 구조물 전체 범위를 알 수 있다.
이 필드의 타입이 IEnumerable<T>로 선언되어 있다는 점도 중요하다.
IEnumerable은 C#에서 반복 가능한 집합을 의미하는 인터페이스로, 컬렉션의 구체 타입을 제한하지 않는다.
이 구조를 사용하면 List, 배열, 계산 결과 등 다양한 형태의 데이터를 그대로 받을 수 있다.
런타임에서는 좌표 계산 결과를 바로 전달받을 수 있고, 불필요한 컬렉션 복사를 줄일 수 있다.
대신 인덱스 접근이 필요한 상황에서는 별도 변환이 필요하지만, 이 시스템에서는 순회 중심 사용이기 때문에 적절한 선택이다.
origin은 구조물의 기준 좌표다.
이 값은 단순히 시작 위치를 의미하는 것이 아니라, 구조물 전체를 대표하는 기준점이다.
이동, 회전, 재배치, 선택 중심 계산은 모두 이 origin을 기준으로 수행된다.
여기서 중요한 점은 PositionsOccupied와 origin이 반드시 분리되어야 한다는 것이다.
점유 좌표 목록만 있으면 구조물 범위는 알 수 있지만, 기준 위치를 알 수 없다.
반대로 origin만 있으면 구조물의 전체 범위를 알 수 없다.
이 둘을 하나로 합칠 수 없는 이유는 역할이 완전히 다르기 때문이다.
PositionsOccupied는 어디까지 차지하는가를 의미하고, origin은 이 구조물을 어디를 기준으로 해석할 것인가를 의미한다.
이 분리는 구조물 단위 연산을 가능하게 만드는 핵심 설계다.
rotation과 objectRotation도 분리되어 있다.
rotation은 Grid 기준 회전값으로, 좌표 계산에 직접 사용된다.
GetCellPositions나 GetEdgePositions에서 어느 방향으로 좌표를 확장할지를 결정하는 기준이다.
반면 objectRotation은 실제 GameObject의 회전값이다.
이는 시각적인 표현과 관련된 값이다.
이 둘을 분리하지 않으면 문제가 발생한다.
Grid 계산은 90도 단위로 진행되지만, 실제 오브젝트는 더 다양한 회전을 가질 수 있기 때문이다.
따라서 좌표 계산과 렌더링을 분리하기 위해 두 값을 나누어 저장한다.
이 설계 덕분에 Grid 논리와 시각적 표현이 서로 간섭하지 않는다.
structureID는 이 구조물이 어떤 종류인지 식별하는 값이다.
저장이나 복원 시 어떤 prefab을 사용할지 결정하는 기준이 된다.
반면 gameObjectIndex는 현재 씬에 존재하는 GameObject를 찾기 위한 값이다.
구조물 제거, 이동, 상태 변경은 결국 씬 오브젝트를 기준으로 이루어지기 때문에 이 값이 필요하다.
즉 structureID는 무엇인가를 의미하고, gameObjectIndex는 어디에 연결되어 있는가를 의미한다.
PlacedEdgeObjectData는 구조적으로 PlacedCellObjectData와 동일하지만, 점유 대상이 Edge라는 점이 다르다.
Edge는 두 점으로 이루어진 선분이기 때문에 단일 좌표보다 복잡한 구조를 가진다.
그럼에도 불구하고 동일한 데이터 구조를 유지한 이유는, 상위 시스템이 셀 기반 구조물과 Edge 기반 구조물을 동일한 방식으로 처리할 수 있도록 하기 위함이다.
결과적으로 이 클래스들은 단순 데이터 묶음이 아니다.
이 클래스들은 좌표 기반 시스템에서 구조물 단위를 정의하는 핵심 객체이며, 모든 좌표는 이 객체를 통해 구조물 전체와 연결된다.
4. 개발 의도
이 클래스들의 핵심 설계 의도는 구조물을 좌표가 아니라 객체 단위로 다루기 위함이다.
Grid는 좌표를 기준으로 데이터를 저장하지만, 실제 게임 로직은 구조물 단위로 동작한다.
따라서 좌표 하나만으로 구조물 전체를 추적할 수 있는 구조가 필요했고, 그 해결책이 여러 좌표가 하나의 데이터 객체를 공유하는 구조였다.
또한 PositionsOccupied, origin, rotation을 분리한 것도 중요한 설계다.
각각의 필드는 서로 다른 역할을 담당하며, 이 세 가지가 모두 있어야 구조물의 상태를 완전히 정의할 수 있다.
하나라도 빠지면 구조물의 위치, 방향, 범위를 정확하게 표현할 수 없게 된다.
결과적으로 이 클래스는 단순 데이터 클래스가 아니라, Grid 시스템에서 구조물 단위를 정의하고, 모든 연산의 기준이 되는 핵심 데이터 구조다.
