조회 및 탐색 시스템

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. 셀과 Edge 존재 여부 확인

       3.2. 실제 씬 오브젝트 연결 정보 조회

       3.3. 구조물 전체 점유 범위 조회

       3.4. 구조물 기준점 조회

       3.5. 구조물 식별자 조회

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서는 구조물을 배치하고 제거하는 것만큼이나, 현재 Grid 상태를 정확하게 읽어오는 기능이 중요하다.

플레이어가 특정 위치를 클릭했을 때 그 위치에 구조물이 있는지 알아야 하고, 있다면 그것이 어떤 구조물인지, 구조물 전체가 어느 범위를 점유하고 있는지, 실제 씬에서 연결된 GameObject가 무엇인지까지 역으로 추적할 수 있어야 한다.

그렇지 않으면 선택, 제거, 이동, 교체, 저장/복원 같은 상위 시스템이 동작할 수 없다.

이 프로젝트의 Grid 데이터는 좌표 하나마다 구조물의 일부 조각을 따로 저장하는 방식이 아니라, 구조물 전체를 대표하는 데이터를 여러 좌표가 공유하는 방식으로 설계되어 있다.

따라서 조회 시스템은 단순히 '해당 좌표에 뭔가 있다' 를 반환하는 수준을 넘어서, 좌표 하나를 통해 구조물 전체 정보를 다시 찾아낼 수 있어야 한다.

예를 들어 셀 하나를 클릭했을 때, 그 셀이 속한 구조물의 origin, structureID, 점유 범위, 실제 GameObject 인덱스를 가져올 수 있어야 한다.

Edge 기반 구조물도 마찬가지로, 특정 Edge 하나를 기준으로 구조물 전체를 추적할 수 있어야 한다.

또한 셀 기반 구조물과 Edge 기반 구조물은 저장 방식이 다르기 때문에 조회 방식도 분리되어야 한다.

셀은 Vector3Int를 기준으로 바로 조회할 수 있지만, Edge는 회전값과 기준 좌표를 통해 실제 Edge를 다시 계산해 찾아야 하는 경우가 있다.

따라서 조회 및 탐색 시스템은 셀과 Edge라는 두 좌표 체계를 각각 이해한 상태에서, 상위 시스템이 필요로 하는 정보를 안전하고 일관되게 반환해야 한다.

이 계층은 단순 조회 함수 모음이 아니라, Grid에 저장된 구조물 상태를 읽어서 상위 시스템이 사용할 수 있는 의미 있는 정보로 변환해 주는 탐색 계층이어야 한다.

이 계층이 정확해야만 PlacementManager, RemovingState, Move 시스템, Command 시스템이 모두 같은 기준 위에서 동작할 수 있다.

2. 흐름도

플레이어 입력 또는 시스템 요청

   ↓

특정 셀 좌표 또는 기준 좌표 + 회전값 전달

   ↓

조회 및 탐색 함수 호출

   ↓

셀 기반인가?

   ├─ YES → gridCellsDictionary 기준 조회

   │        ↓

   │       존재 여부 / origin / structureID / 점유 범위 / gameObjectIndex 반환

   │

   └─ NO → GetEdgePositions 또는 Edge 키 기준 조회

            ↓

           gridEdgesDictionary 기준 조회

            ↓

           존재 여부 / origin / structureID / 점유 범위 / gameObjectIndex 반환

   ↓

선택 / 제거 / 이동 / 저장 시스템에서 사용

이 흐름에서 중요한 점은 조회가 단순 확인으로 끝나지 않는다는 것이다.

상위 시스템은 먼저 그 위치에 구조물이 있는지를 묻고, 구조물이 있다면 그 다음에는 어떤 구조물인가, 전체 점유 범위가 어디까지인가, 씬에 연결된 GameObject는 무엇인가를 연속적으로 요구한다.

따라서 조회 시스템은 존재 여부 확인, 구조물 식별, 구조물 범위 조회, origin 조회, 실제 오브젝트 연결 정보 조회까지 모두 제공해야 한다.

또한 셀 기반 구조물은 키가 셀 좌표이기 때문에 바로 조회가 가능하지만, Edge 기반 구조물은 단순 좌표 하나만으로는 충분하지 않다.

현재 기준 위치와 회전에서 실제 어떤 Edge가 생성되는지를 다시 계산한 뒤 그 결과를 키로 사용해야 하는 경우가 있다.

이 차이 때문에 조회 및 탐색 시스템은 셀과 Edge를 동일하게 다루지 않고, 각각의 좌표 체계에 맞는 방식으로 접근한다.

3. 구현

이 조회 함수들은 공통적인 구조를 가진다.

먼저 현재 좌표가 Dictionary에 실제로 등록되어 있는지를 확인하고, 등록되어 있지 않으면 null이나 -1 같은 실패 값을 반환한다.

반대로 등록되어 있으면, 그 좌표가 참조하고 있는 구조물 데이터에서 필요한 필드만 꺼내 반환한다.

이 구조는 단순 getter가 아니라, 좌표 하나를 구조물 전체 데이터로 해석한 뒤 필요한 정보만 추출하는 과정이다.

Grid는 좌표를 키로 저장하지만, 실제로는 구조물 단위 데이터를 공유하고 있기 때문에 가능한 방식이다.

3.1. 셀과 Edge 존재 여부 확인
internal bool IsCellObjectAt(Vector3Int currentTilePosition)
{
    return gridCellsDictionary.ContainsKey(currentTilePosition);
}

internal bool IsEdgeObjectAt(Edge edgePosition)
{
    return gridEdgesDictionary.ContainsKey(edgePosition);
}

이 두 함수는 현재 위치에 구조물이 존재하는지를 가장 먼저 확인하는 함수다.

코드는 짧지만 상위 시스템에서 매우 자주 사용되는 기본 조회 함수다.

셀 기반 구조물은 gridCellsDictionary를, Edge 기반 구조물은 gridEdgesDictionary를 기준으로 검사한다.

각각 ContainsKey를 호출하여 해당 키가 Dictionary 안에 존재하는지를 boolean 값으로 반환한다.

C#의 Dictionary.ContainsKey는 특정 키가 존재하는지를 확인하는 가장 기본적인 API다.

평균적으로 빠른 조회가 가능하기 때문에, Grid 시스템처럼 특정 좌표에 대한 존재 여부를 반복적으로 확인해야 하는 구조에 잘 맞는다.

이 함수들의 장점은 명확하다.

존재 여부만 필요한 상황에서는 구조물 전체 데이터를 꺼낼 필요 없이, 키 존재 여부만으로 즉시 판단할 수 있다.

예를 들어 플레이어가 클릭한 위치에 구조물이 있는지 확인하거나, 특정 위치가 삭제 가능한 대상인지 빠르게 검사할 때 적합하다.

중요한 점은 이 함수들이 단순히 '있다/없다' 만 말하는 것이 아니라, 이후 조회 체인의 시작점 역할을 한다는 것이다.

보통 상위 시스템은 먼저 IsCellObjectAt 또는 IsEdgeObjectAt로 대상이 존재하는지 확인한 뒤, 존재하는 경우에만 origin, structureID, 점유 범위, GameObject 인덱스 같은 더 구체적인 정보를 요청한다.

따라서 이 함수들은 조회 시스템 전체에서 가장 얕은 단계이자, 안전한 후속 조회를 위한 첫 번째 필터라고 볼 수 있다.

3.2. 실제 씬 오브젝트 연결 정보 조회
internal int GetIndexForCellObject(Vector3Int currentTilePosition)
{
    if (gridCellsDictionary.ContainsKey(currentTilePosition) == false)
        return -1;
    return gridCellsDictionary[currentTilePosition].gameObjectIndex;
}

internal int GetIndexForEdgeObject(Vector3Int currentTilePosition, int rotation)
{
    List<Edge> edgePositions = GetEdgePositions(currentTilePosition, Vector2Int.one, rotation);
    if (gridEdgesDictionary.ContainsKey(edgePositions[0]) == false)
        return -1;
    return gridEdgesDictionary[edgePositions[0]].gameObjectIndex;
}

이 두 함수는 Grid 데이터와 실제 씬 오브젝트를 연결하는 인덱스를 반환한다.

이 프로젝트에서는 Grid 데이터와 씬의 GameObject가 분리되어 있고, Grid 쪽에는 gameObjectIndex 형태로 씬 오브젝트를 참조할 수 있는 값이 저장되어 있다.

따라서 구조물을 실제로 제거하거나, 회전값을 읽거나, 이동 대상으로 선택한 뒤 StructurePlacer와 연결하려면 이 인덱스가 필요하다.

셀 기반 함수인 GetIndexForCellObject는 구조가 단순하다.

먼저 ContainsKey로 해당 좌표가 실제로 Dictionary에 존재하는지 확인한다.

존재하지 않으면 -1을 반환한다.

여기서 -1은 C#에서 자주 사용하는 sentinel value 역할을 한다.

유효한 인덱스가 없는 것을 명시적으로 나타내는 값이다.

null을 쓸 수 없는 int 반환형에서 실패 상태를 표현하기 위한 방식이다.

존재하는 경우에는 gridCellsDictionary[currentTilePosition].gameObjectIndex를 그대로 반환한다.

이 코드는 현재 셀이 참조하는 구조물 데이터에서 실제 씬 오브젝트 인덱스만 꺼내는 구조다.

Edge 기반 함수인 GetIndexForEdgeObject는 한 단계 더 복잡하다.

입력으로 currentTilePosition과 rotation을 받는데, 이는 Edge 구조물은 단순 셀 좌표 하나로 바로 조회하지 않기 때문이다.

먼저 GetEdgePositions(currentTilePosition, Vector2Int.one, rotation)을 호출해 현재 기준 위치와 회전에서 실제로 만들어지는 Edge를 계산한다.

여기서 Vector2Int.one을 넘기는 이유는 한 개의 기본 Edge를 만들기 위한 최소 단위 조회이기 때문이다.

그 다음 계산된 edgePositions[0]가 Dictionary에 존재하는지 검사하고, 존재하지 않으면 -1, 존재하면 해당 Edge가 참조하는 구조물 데이터의 gameObjectIndex를 반환한다.

이 함수가 중요한 이유는 Edge 구조물 조회가 단순 좌표 접근이 아니라, 기준 좌표와 회전을 통해 실제 조회 키를 재구성하는 과정을 필요로 한다는 점을 보여주기 때문이다.

셀과 Edge의 차이는 저장 구조뿐 아니라 조회 방식에도 그대로 반영된다.

결과적으로 이 두 함수는 Grid 데이터 안에 저장된 구조물 정보를 실제 씬 오브젝트와 연결하는 다리 역할을 한다.

3.3. 구조물 전체 점유 범위 조회
public List<Edge> GetEdgesOccupiedForEdgeObject(Edge selectedEdge)
{
    if (gridEdgesDictionary.ContainsKey(selectedEdge) == false)
        return null;
    return new(gridEdgesDictionary[selectedEdge].PositionsOccupied);
}

public List<Vector3Int> GetPositionsOccupiedForCellObject(Vector3Int currentTilePosition)
{
    if (gridCellsDictionary.ContainsKey(currentTilePosition) == false)
        return null;
    return new(gridCellsDictionary[currentTilePosition].PositionsOccupied);
}

이 두 함수는 특정 좌표가 속한 구조물이 실제로 점유하고 있는 전체 범위를 반환한다.

이 함수들이 중요한 이유는 구조물을 좌표 하나가 아니라 점유 영역 전체로 다뤄야 하는 상황이 매우 많기 때문이다.

제거 시스템은 특정 좌표 하나만 클릭해도 구조물 전체를 지워야 하고, 저장 시스템은 구조물 단위로 좌표를 저장해야 하며, 이동 시스템은 현재 선택한 구조물의 전체 범위를 알고 있어야 한다.

이런 상황에서 바로 이 두 함수가 사용된다.

셀 기반 함수인 GetPositionsOccupiedForCellObject는 먼저 해당 좌표가 실제로 Dictionary에 존재하는지 확인한다.

존재하지 않으면 null을 반환한다.

여기서 반환값이 List<Vector3Int>인데 null을 사용할 수 있는 이유는 참조형이기 때문이다.

이 방식은 해당 위치에 구조물이 없다는 상태를 명시적으로 표현한다.

존재하는 경우에는 gridCellsDictionary[currentTilePosition].PositionsOccupied를 가져와 new(...)를 통해 새 List를 만들어 반환한다.

이 부분이 중요하다.

원본 컬렉션을 그대로 반환하지 않고 복사본을 만들어 주는 이유는 외부 시스템이 이 리스트를 수정하더라도 원래 Grid 데이터가 오염되지 않도록 하기 위해서다.

이 함수는 단순 조회가 아니라, 안전한 복사본 반환까지 포함한 함수다.

Edge 기반 함수인 GetEdgesOccupiedForEdgeObject도 같은 구조를 따른다.

selectedEdge가 실제 Dictionary에 존재하지 않으면 null을 반환하고, 존재하는 경우에는 해당 구조물이 점유 중인 PositionsOccupied 전체를 새 List로 복사해 반환한다.

이 함수가 중요한 이유는 벽 구조물이나 InWall 구조물이 한 개의 Edge만 차지하는 것이 아니라 여러 Edge를 동시에 차지할 수 있기 때문이다.

특정 Edge 하나만 알고 있어도 구조물 전체의 점유 Edge 목록을 얻을 수 있어야, 삭제나 저장, 교체 같은 기능이 안정적으로 동작한다.

결과적으로 이 두 함수는 좌표 하나를 통해 구조물 전체 범위를 역으로 추적하는 함수이며, Grid 데이터가 구조물 단위 데이터를 여러 좌표에 공유 저장하고 있다는 설계의 핵심 활용 지점이라고 볼 수 있다.

3.4. 구조물 기준점 조회
internal Vector3Int? GetOriginForCellObject(Vector3Int currentTilePosition)
{
    if (gridCellsDictionary.ContainsKey(currentTilePosition) == false)
        return null;
    return gridCellsDictionary[currentTilePosition].origin;
}

internal Vector3Int? GetOriginForEdgeObject(Edge selectedEdge)
{
    if (gridEdgesDictionary.ContainsKey(selectedEdge) == false)
        return null;
    return gridEdgesDictionary[selectedEdge].origin;
}

이 두 함수는 특정 좌표가 속한 구조물의 origin, 즉 기준 좌표를 반환하는 함수다.

origin은 매우 중요하다.

구조물이 2x2처럼 여러 셀을 점유하더라도, 저장과 복원, 이동, 재배치 같은 로직은 보통 구조물 전체를 대표하는 기준 좌표 하나를 중심으로 동작한다.

따라서 현재 선택한 셀이나 Edge가 구조물의 일부에 불과하더라도, 결국에는 그 구조물의 origin을 알아야 구조물 단위 작업을 수행할 수 있다.

반환형이 Vector3Int?라는 점이 중요하다.

여기서 ?는 C#의 Nullable value type 문법이다.

원래 Vector3Int는 값 형식이기 때문에 null이 될 수 없지만, 조회 실패를 표현하기 위해 Nullable로 감싸서 null을 반환할 수 있도록 만든 것이다.

이건 실용적인 설계다.

origin이 (0,0,0)인 실제 구조물과, '값이 없음' 을 명확히 구분할 수 있기 때문이다.

셀 기반 함수는 해당 좌표가 Dictionary에 존재하는지 확인한 뒤, 존재하면 origin을 그대로 반환한다.

Edge 기반 함수도 같은 구조다.

여기서 중요한 건 계산을 다시 하지 않는다는 점이다.

origin은 이미 AddCellObject/AddEdgeObject 시점에 구조물 데이터 안에 저장되어 있으므로, 조회 시에는 단순 접근만으로 가져올 수 있다.

이 구조 덕분에 매번 origin을 다시 계산할 필요가 없다.

즉 이 함수들은 특정 셀이나 Edge가 구조물의 어느 부분인지가 아니라, 그 부분이 속한 구조물 전체의 기준점이 어디인지를 알려주는 함수다.

상위 시스템이 구조물 단위로 동작하려면 결국 origin이 필요하기 때문에, 이 두 함수는 탐색 시스템에서 매우 자주 사용되는 핵심 조회 함수다.

3.5. 구조물 식별자 조회
public int GetStructureIDForCellObject(Vector3Int currentTilePosition)
{
    if (gridCellsDictionary.ContainsKey(currentTilePosition) == false)
        return -1;
    return gridCellsDictionary[currentTilePosition].structureID;
}

public int GetStructureIDForEdgeObject(Vector3Int currentTilePosition, int rotation)
{
    List<Edge> edgePositions = GetEdgePositions(currentTilePosition, Vector2Int.one, rotation);
    if (gridEdgesDictionary.ContainsKey(edgePositions[0]) == false)
        return -1;
    return gridEdgesDictionary[edgePositions[0]].structureID;
}

이 두 함수는 현재 위치가 어떤 구조물인지 식별할 수 있는 structureID를 반환한다.

이 ID는 저장/복원 시스템에서 매우 중요하다.

Grid 데이터 안에는 prefab 자체를 저장하지 않기 때문에, 나중에 구조물을 다시 생성하려면 ItemDataBaseSO 같은 데이터베이스에서 structureID를 기준으로 원래 구조물 정보를 찾아야 한다.

따라서 좌표 하나를 기준으로 structureID를 알아내는 기능은 필수다.

셀 기반 함수는 구조가 단순하다.

좌표가 실제로 존재하는지 확인한 뒤, 존재하면 해당 셀이 참조하는 구조물 데이터의 structureID를 반환한다.

존재하지 않으면 -1을 반환한다.

여기서도 -1은 유효하지 않은 ID를 나타내는 sentinel value다.

해당 위치에 구조물이 없음을 명시적으로 표현한다.

이 함수는 선택 시스템이나 저장 시스템이 현재 셀의 구조물 종류를 판별할 때 사용된다.

Edge 기반 함수는 인덱스 조회와 마찬가지로 먼저 GetEdgePositions(currentTilePosition, Vector2Int.one, rotation)을 호출한다.

이 과정은 기준 좌표와 회전으로 실제 조회 대상 Edge를 재구성하는 단계다.

그 다음 첫 번째 Edge가 Dictionary에 존재하면 그 구조물의 structureID를 반환한다.

Edge 구조물의 종류를 조회하려면 셀처럼 단순 좌표 접근이 아니라, '좌표 + 회전' 으로 실제 Edge 키를 다시 만들어 접근해야 한다는 점이 코드에 그대로 드러난다.

결국 이 두 함수는 좌표 하나를 통해 이게 어떤 구조물인지를 식별하는 함수다.

존재 여부 함수가 '있냐 없냐' 를, origin 함수가 기준점이 어디냐를 말한다면, structureID 함수는 그 구조물의 정체가 무엇인지를 알려준다.

이 정보가 있어야 이동, 교체, 저장/복원 단계에서 올바른 ItemData를 다시 찾을 수 있다.

4. 개발 의도

조회 및 탐색 시스템의 핵심 설계 의도는 Grid에 저장된 구조물 상태를 좌표 하나만으로도 구조물 단위로 해석할 수 있게 만드는 것이다.

이 프로젝트는 구조물 전체 데이터를 한 좌표에만 두는 것이 아니라, 구조물이 점유하는 모든 셀 또는 Edge가 동일한 데이터를 공유하는 구조를 사용한다.

그래서 특정 좌표 하나만 알고 있어도, 그 좌표가 속한 구조물의 origin, structureID, 점유 범위, 실제 GameObject 인덱스를 모두 역으로 얻어낼 수 있다.

이 설계 덕분에 상위 시스템은 별도의 매핑 테이블 없이도 현재 선택한 위치를 구조물 단위로 다룰 수 있다.

또한 셀과 Edge는 저장 방식이 다르기 때문에 조회 방식도 분리했다.

셀은 곧바로 좌표 키로 조회할 수 있지만, Edge는 필요할 때 기준 좌표와 회전에서 실제 Edge를 다시 계산해 접근해야 한다.

이 차이를 함수 수준에서 분리했기 때문에, 상위 시스템은 셀 구조물 조회와 Edge 구조물 조회를 각각 명확한 규칙으로 사용할 수 있게 되었다.

결과적으로 이 계층은 단순 getter 모음이 아니다.

이 계층은 Grid 저장 구조를 상위 시스템이 이해할 수 있는 구조물 단위 정보로 해석하는 역할을 하며, 선택, 제거, 이동, 교체, 저장/복원 같은 모든 상위 기능의 출발점이 된다.

이 시스템은 Grid 데이터와 상위 게임 로직 사이를 연결하는 탐색 해석 계층이라고 볼 수 있다.