건축 객체 데이터 시스템

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. 단일 구조물에 대한 데이터 정의

       3.2. 데이터베이스 구조

       3.3. 구조물 데이터 조회

       3.4. 데이터 변경 시 자동 검증

4. 개발 의도

1. 시스템 요구 사항

건축 시스템에서 가장 먼저 정의되어야 하는 것은 무엇을 배치할 것인지에 대한 데이터이다.

플레이어는 다양한 구조물(벽, 바닥, 가구 등)을 선택하여 Grid 위에 배치하게 되는데, 이때 각 구조물은 단순한 GameObject가 아니라 고유한 식별자, 크기, 배치 방식, 프리뷰 정보 등을 함께 가지는 데이터 단위로 관리되어야 한다.

특히 이 시스템에서는 구조물을 저장하고 다시 불러오는 기능이 존재하기 때문에, 단순히 prefab을 참조하는 방식으로는 충분하지 않다.

구조물을 다시 복원하기 위해서는 어떤 구조물이었는지를 식별할 수 있는 고유 ID가 반드시 필요하다.

따라서 모든 구조물은 ID 기반으로 관리되어야 하며, 이 ID를 통해 데이터베이스에서 다시 구조물 정보를 조회할 수 있어야 한다.

또한 건축 시스템은 확장 가능해야 한다.

새로운 구조물을 추가하거나 기존 구조물의 속성을 수정할 때 코드 수정 없이 데이터만으로 관리할 수 있어야 한다.

이를 위해 Unity의 ScriptableObject를 활용하여 구조물 데이터를 에셋 단위로 관리하는 구조가 필요하다.

따라서 이 시스템은 구조물의 속성(크기, 배치 타입, prefab 등)을 정의구조물 데이터를 ID 기반으로 조회할 수 있어야 한다.

또한, 데이터 수정 및 추가가 코드 수정 없이 가능해야 하며, 프리뷰 및 실제 배치에 필요한 데이터를 함께 관리해야 한다.

2. 흐름도

ItemData (개별 구조물 정의)

   ↓

ItemDataBaseSO (데이터 목록 관리)

   ↓

Placement / Selection / Command 시스템에서 ID로 조회

   ↓

구조물 배치 / 제거 / 복원

이 구조에서 핵심은 ItemData가 단순 데이터 정의에 그치지 않고, ItemDataBaseSO를 통해 “중앙 데이터 저장소”로 관리된다는 점이다.

게임 실행 중에는 특정 구조물을 직접 참조하는 것이 아니라, ID를 통해 데이터베이스에서 조회하는 방식으로 동작한다.

이 구조 덕분에 저장/복원 시스템에서도 동일한 방식으로 데이터를 사용할 수 있으며, 데이터 중심 구조가 유지된다.

3. 구현

3.1. 단일 구조물에 대한 데이터 정의
[Serializable]
public class ItemData
{
    public string name;
    public int ID;
    public Vector2Int size;
    public PlacementType objectPlacementType;
    public GameObject prefab;
    public GameObject previewObject;
}

이 클래스는 건축 시스템에서 배치 가능한 단일 구조물의 모든 속성을 정의하는 데이터 구조이다.

여기서 중요한 점은 이 클래스가 단순히 데이터를 담는 컨테이너가 아니라, 이후 배치 시스템, 선택 시스템, 저장/복원 시스템 전반에서 공통적으로 사용되는 기준 데이터라는 점이다.

[Serializable] 속성은 Unity의 직렬화 시스템에 이 클래스를 포함시키기 위한 설정이다.

이 속성이 적용되면 해당 클래스는 ScriptableObject 내부에서 리스트 형태로 저장될 수 있고, Inspector에서도 값을 직접 수정할 수 있게 된다.

Unity는 기본적으로 public 필드만 직렬화하지만, 클래스 자체가 직렬화 대상이 되려면 이 속성이 필요하다.

이 구조 덕분에 ItemData는 코드가 아닌 데이터 에셋 형태로 관리될 수 있다.

name 필드는 단순 문자열처럼 보이지만, 실제 개발 과정에서는 디버깅과 데이터 식별에서 중요한 역할을 한다.

특히 ID 기반 시스템에서는 숫자만으로 구조물을 구분하기 어렵기 때문에, 로그 출력이나 Inspector 확인 시 name 값이 함께 사용되어 가독성을 높인다.

ID는 이 클래스에서 가장 중요한 필드이다.

이 값은 저장/복원 시스템에서 구조물을 식별하는 유일한 키로 사용된다.

실제 게임에서는 GameObject나 prefab을 직접 저장하지 않고, 이 ID 값만 저장한 뒤 로드 시점에 ItemDataBaseSO에서 해당 ID를 조회하여 구조물을 복원한다.

이 방식은 Unity에서 GameObject 참조를 직접 직렬화할 때 발생할 수 있는 문제를 피하고, 데이터 기반으로 안정적인 저장/복원을 가능하게 한다.

size는 구조물이 Grid에서 차지하는 영역을 정의하는 값이다.

이 값은 단순히 시각적인 크기를 나타내는 것이 아니라, PlacementGridData에서 점유 영역을 계산할 때 직접 사용된다.

예를 들어 구조물이 배치될 때 GetCellPositions 또는 GetEdgePositions 함수에서 이 size를 기반으로 점유 셀을 계산하고, 해당 영역이 비어 있는지를 검사한다.

따라서 이 값은 렌더링이 아니라 충돌 검사 및 배치 가능 여부 판단에 직접적으로 연결된 데이터이다.

objectPlacementType은 구조물의 배치 방식 자체를 정의하는 필드이다.

이 값에 따라 SelectionStrategy와 PlacementValidator에서 완전히 다른 로직이 적용된다.

예를 들어 셀 중심 배치인지, 벽처럼 Edge 기반 배치인지에 따라 좌표 계산 방식, 점유 검사 방식, 회전 처리 방식이 모두 달라진다.

prefab은 실제 씬에 생성되는 GameObject를 참조한다.

Unity에서는 런타임 객체를 생성할 때 Instantiate를 사용하는데, 이 prefab은 StructurePlacer에서 해당 구조물을 실제로 생성할 때 사용된다.

previewObject는 배치 전에 보여주는 프리뷰용 오브젝트이다.

플레이어가 구조물을 배치하기 전에 위치를 확인할 수 있도록 시각적 피드백을 제공하는 역할을 한다.

만약 이 값이 설정되지 않은 경우에는 prefab을 그대로 사용하는 구조로 되어 있으며, 별도의 프리뷰 모델이 필요한 경우를 대비한 확장 설계이다.

3.2. 데이터베이스 구조
[CreateAssetMenu]
public class ItemDataBaseSO : ScriptableObject
{
    public List<ItemData> structures;

    ...
}

이 클래스는 여러 ItemData를 하나의 데이터베이스 형태로 관리하는 ScriptableObject이다.

ItemData가 개별 구조물의 정의라면, ItemDataBaseSO는 전체 구조물 목록을 관리하는 중앙 저장소 역할을 한다.

ScriptableObject를 사용한 이유는 데이터와 로직을 분리하기 위함이다.

일반적인 MonoBehaviour 기반 구조에서는 데이터가 코드 내부에 포함되기 쉽지만, ScriptableObject를 사용하면 데이터를 에셋 형태로 분리할 수 있어 코드 수정 없이 데이터 변경이 가능하다.

특히 건축 시스템처럼 구조물 종류가 자주 추가되거나 수정되는 경우, 이 방식은 유지보수성과 확장성 측면에서 큰 장점을 가진다.

[CreateAssetMenu] 속성은 Unity Editor에서 해당 ScriptableObject를 쉽게 생성할 수 있도록 한다.

이 속성을 통해 메뉴에서 클릭만으로 데이터베이스를 생성할 수 있으며, 기획자나 디자이너도 코드 수정 없이 구조물 데이터를 추가할 수 있다.

structures 리스트는 모든 구조물 데이터를 담는 컨테이너이다.

이 리스트에 등록된 ItemData는 게임에서 배치 가능한 모든 구조물의 집합이 된다.

이 클래스는 건축 시스템 전체에서 사용하는 데이터 소스라고 볼 수 있다.

실제 시스템에서는 ItemData를 직접 참조하지 않고, 항상 이 데이터베이스를 통해 ID 기반으로 조회하는 방식으로 동작한다.

이 구조 덕분에 저장/복원, Command 시스템, Placement 시스템 모두 동일한 데이터 접근 방식을 유지할 수 있다.

3.3. 구조물 데이터 조회
public ItemData GetItemWithID(int id)
{
    return structures.FirstOrDefault(structureData => structureData.ID == id);
}

이 함수는 ID를 기반으로 구조물 데이터를 조회하는 역할을 한다.

건축 시스템 전반에서는 구조물을 직접 참조하지 않고, 항상 ID를 통해 ItemData를 가져오는 방식으로 동작하기 때문에 이 함수는 매우 자주 사용된다.

FirstOrDefault는 LINQ에서 제공하는 함수로, 컬렉션에서 조건을 만족하는 첫 번째 요소를 반환한다.

조건을 만족하는 값이 없을 경우 null을 반환하는 특징이 있다.

이 함수는 내부적으로 리스트를 앞에서부터 순회하면서 조건을 검사하는 구조이기 때문에 시간 복잡도는 O(n)이다.

이 구조 대신 Dictionary<int, ItemData>를 사용할 수도 있지만, 이 프로젝트에서는 List 기반 구조를 유지하였다.

그 이유는 구조물 데이터의 개수가 제한적이며, 성능보다 Inspector에서의 관리 편의성과 직관성을 더 중요하게 판단했기 때문이다.

List는 Unity Inspector에서 순서와 내용을 쉽게 확인하고 수정할 수 있으며, ScriptableObject와의 호환성도 뛰어나다.

또한 이 함수는 저장/복원 시스템에서 핵심적으로 사용된다.

예를 들어 저장된 데이터에는 구조물 ID만 기록되고, 로드 시점에 이 함수를 통해 해당 ID의 ItemData를 찾아 prefab을 생성하게 된다.

3.4. 데이터 변경 시 자동 검증
private void OnValidate()
{
    HashSet<int> IDs = new();
    foreach (var item in structures)
    {
        if (IDs.Contains(item.ID))
            Debug.LogError($"StructuresData에서 ID가 {item.ID}, 
                           이름이 {item.name}인 객체의 중복된 ID가 발견되었습니다.");
    IDs.Add(item.ID);

        if (item.previewObject == null) item.previewObject = item.prefab;
    }
}

OnValidate는 Unity Editor 환경에서 값이 변경될 때 자동으로 호출되는 함수이다.

이 함수는 런타임이 아니라 에디터 단계에서 실행되며, Inspector에서 값이 수정되거나 ScriptableObject가 저장될 때마다 호출된다.

이 함수를 사용한 이유는 데이터 검증을 런타임이 아닌 에디터 단계에서 수행하기 위함이다.

만약 ID 중복 검사를 런타임에서 수행한다면, 이미 잘못된 데이터가 게임에 반영된 이후에야 오류를 확인하게 된다.

반면 OnValidate를 사용하면 데이터 입력 시점에서 즉시 문제를 감지할 수 있기 때문에, 데이터 무결성을 훨씬 안정적으로 유지할 수 있다.

여기서는 HashSet을 사용하여 ID 중복을 검사하고 있다.

HashSet은 중복을 허용하지 않는 자료구조로, 특정 값이 이미 존재하는지를 O(1)에 가까운 시간으로 확인할 수 있다.

이 구조를 사용하면 리스트를 순회하면서도 빠르게 중복 여부를 판단할 수 있으며, 동일한 ID가 존재할 경우 즉시 오류를 출력할 수 있다.

또한 previewObject가 null인 경우 prefab을 자동으로 할당하는 로직이 포함되어 있다.

이 부분은 데이터 입력 과정에서 발생할 수 있는 실수를 보완하기 위한 안전장치이다.

previewObject를 설정하지 않더라도 시스템이 정상적으로 동작하도록 기본값을 보정하는 역할을 한다.

이처럼 OnValidate를 활용하면 데이터 검증뿐만 아니라 기본값 보정까지 함께 처리할 수 있다.

4. 개발 의도

이 데이터 시스템의 핵심 설계 의도는 구조물을 GameObject가 아닌 데이터 중심으로 관리하는 것이다.

일반적인 Unity 개발에서는 prefab을 직접 참조하는 방식이 흔하지만, 이 프로젝트에서는 ID 기반 데이터 시스템을 도입하여 저장/복원과 시스템 확장성을 동시에 확보하였다.

특히 ScriptableObject를 사용한 이유는 데이터와 로직을 완전히 분리하기 위함이다.

이를 통해 구조물 추가나 수정이 코드 변경 없이 가능해졌고, 시스템 유지보수성이 크게 향상되었다.

또한 ItemData와 ItemDataBaseSO를 분리한 구조는 단일 책임 원칙을 따른다.

ItemData는 구조물 하나의 정의만 담당하고, ItemDataBaseSO는 그 데이터를 관리하는 역할만 수행한다.

이 분리는 데이터 구조를 명확하게 만들고, 시스템 확장을 쉽게 한다.

이 시스템은 단순한 데이터 저장 구조가 아니라, 건축 시스템 전체의 기반이 되는 데이터 중심 아키텍처로 설계되었다.