피드백 (흰색/빨간색)

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. 초기화 처리

       3.2. 프리뷰 표시 시작

       3.3. 프리뷰 오브젝트 생성과 재질 교체

       3.4. 프리뷰 재질 준비

       3.5. 프리뷰 위치 및 회전 갱신

       3.6. 프리뷰 피드백 전환

       3.7. 배치 가능 상태 피드백

       3.8. 배치 가능 여부 시각화

       3.9. 프리뷰 종료 및 정리

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서 프리뷰는 단순히 오브젝트를 미리 보여주는 장식 기능이 아니다.

플레이어는 구조물을 배치하기 전에 현재 선택 위치에 실제로 무엇이 놓일지, 어떤 방향으로 놓일지, 그리고 그 위치가 유효한지까지 즉시 확인할 수 있어야 한다.

건축 시스템은 입력과 동시에 계속 상태가 바뀌기 때문에, 프리뷰는 그 상태 변화를 실시간으로 시각화하는 역할을 한다.

특히 이 시스템은 단일 셀만 다루는 것이 아니라 여러 Grid 위치를 동시에 선택하는 경우도 지원해야 한다.

따라서 프리뷰도 하나의 오브젝트만 관리하는 것이 아니라, 선택된 위치 개수에 맞춰 여러 개를 생성하거나 제거할 수 있어야 한다.

또한 배치가 가능한 경우와 불가능한 경우를 색상으로 구분하여 플레이어가 클릭하기 전에 이미 결과를 예측할 수 있어야 한다.

또 다른 중요한 요구사항은 프리뷰가 실제 배치 로직과 분리되어 있어야 한다는 점이다.

PlacementManager나 Validator가 직접 색을 바꾸거나 오브젝트를 생성하는 구조가 되면 건축 시스템의 책임이 섞이게 된다.

따라서 PlacementPreview는 선택 결과와 검증 결과를 받아 보여주는 것만 전담하는 별도의 시각화 계층으로 동작해야 한다.

결과적으로 이 시스템은 프리뷰 오브젝트의 생성, 위치 및 회전 갱신, 재질 변경, 피드백 색상 전환, 상태 종료 시 정리까지 포함하는 독립적인 View Layer로 설계되어야 한다.

2. 흐름도

배치 상태 시작

   ↓

StartShowingPreview()

   ↓

프리뷰 템플릿 생성

   ↓

MovePreview()

   ↓

선택 위치 수에 맞게 프리뷰 오브젝트 생성 / 제거

   ↓

프리뷰 위치, 회전, 스케일 갱신

   ↓

ShowPlacementFeedback()

   ↓

PlacementFeedbackPositive() / PlacementFeedbackNegative()

   ↓

색상 변경으로 배치 가능 여부 표현

   ↓

배치 상태 종료

   ↓

StopShowingPreview()

이 시스템의 흐름은 입력 시스템과 직접 연결되지 않고, PlacementManager를 통해 전달된 선택 결과와 검증 결과를 화면으로 바꾸는 구조이다.

건축 모드가 시작되면 StartShowingPreview가 호출되어 프리뷰에 사용할 템플릿 오브젝트가 준비된다.

이후 마우스 이동이나 회전으로 선택 상태가 바뀔 때마다 MovePreview가 호출되고, 이 함수가 현재 선택 위치 수에 맞춰 프리뷰 오브젝트들을 재배치한다.

그 다음 Validator가 계산한 유효성 결과가 전달되면 ShowPlacementFeedback가 호출되어 프리뷰 색상을 기본색이나 빨간색으로 바꾼다.

최종적으로 배치 상태가 끝나면 StopShowingPreview가 호출되어 생성된 프리뷰 오브젝트를 모두 제거하고 내부 상태를 초기화한다.

즉, 이 시스템은 '생성 → 갱신 → 피드백 → 종료' 라는 분명한 생명주기를 가진다.

3. 구현

3.1. 초기화 처리
private void Start()
{ 
    defautlColor = transparentMaterial.color;
    transparentMaterialInstance = new Material(transparentMaterial);
}

이 함수는 PlacementPreview가 사용할 재질 상태를 초기화하는 역할을 한다.

Unity에서 Start는 MonoBehaviour가 활성화된 이후 한 번 호출되는 생명주기 함수이다.

이 클래스는 씬에 존재하는 프리뷰 관리 컴포넌트이기 때문에, 런타임에 한 번만 준비하면 되는 초기 설정을 Start에 넣는 것이 자연스럽다.

첫 줄에서는 transparentMaterial의 현재 색상을 defautlColor에 저장한다.

이 값은 이후 배치 불가능 상태에서 빨간색으로 바뀐 재질을 다시 원래 색으로 되돌릴 때 기준이 된다.

즉, 단순 보관이 아니라 복원 기준값 역할을 한다.

둘째 줄은 더 중요하다.

transparentMaterialInstance = new Material(transparentMaterial);

Unity에서 Material은 기본적으로 공유 자원처럼 동작한다.

만약 transparentMaterial 원본을 그대로 수정하면, 그 재질을 참조하는 다른 오브젝트까지 모두 색상이 바뀔 수 있다.

그래서 여기서는 new Material(...)을 사용해 별도의 인스턴스를 만든다.

이렇게 하면 프리뷰 시스템 안에서만 안전하게 색상을 바꿀 수 있다.

이 방식의 장점은 프리뷰용 재질이 독립적으로 관리된다는 점이다.

배치 가능 여부에 따라 색을 바꾸더라도 원본 재질은 손상되지 않는다.

단점은 런타임에 추가 Material 인스턴스를 만들기 때문에 메모리 사용량이 조금 늘어난다는 점이다.

하지만 프리뷰 시스템은 정확한 시각 피드백이 핵심이기 때문에, 공유 재질을 건드려 전체 씬 상태를 오염시키는 것보다 훨씬 안전한 선택이다.

3.2. 프리뷰 표시 시작
public void StartShowingPreview(GameObject placedObject, bool keepMaterial = false)
{
    if (keepMaterial)
    {
        previewTemplate = Instantiate(placedObject, transform);
    }
    else
    {
        previewTemplate = CreatePreviewObject(placedObject);
    }
    
    previewObjects.Clear();
    previewObjects.Add(previewTemplate);
}

이 함수는 배치 상태나 제거 상태가 시작될 때 프리뷰 시스템을 활성화하는 진입점이다.

외부에서는 이 오브젝트를 프리뷰로 보여줘라는 의미로 이 함수를 호출한다.

여기서 핵심 분기는 keepMaterial이다.

이 값이 true이면 기존 재질을 유지한 채로 프리뷰를 생성하고, false이면 CreatePreviewObject를 통해 투명 재질로 바뀐 프리뷰를 만든다.

이 구조는 시스템이 단순한 배치 프리뷰만 처리하는 것이 아니라, 제거 모드처럼 다른 표현이 필요한 상태도 지원한다는 뜻이다.

예를 들어 삭제 모드에서는 빨간색 사각형 같은 별도 프리뷰를 그대로 보여줄 수 있고, 일반 배치 모드에서는 실제 구조물 프리팹을 투명 재질로 바꿔서 보여줄 수 있다.

Instantiate(placedObject, transform)은 Unity의 오브젝트 복제 함수이다.

두 번째 인자로 transform을 넘기면 생성된 오브젝트의 부모가 현재 PlacementPreview 오브젝트가 된다.

이 구조는 프리뷰 오브젝트들을 한 부모 아래에 묶어 관리하기 쉽게 만들어준다.

이후 previewObjects.Clear()로 리스트를 비우고, 새로 만든 템플릿을 리스트에 넣는다.

이 시점에서 리스트를 비우는 이유는 이전 프리뷰 상태가 남아 있지 않도록 하기 위해서다.

이 함수는 프리뷰 시작점이기 때문에, 새로운 배치 상태가 시작될 때 기존 상태를 덮어써야 한다.

그러므로 이 코드는 단순 추가가 아니라 프리뷰 세션 초기화의 의미를 가진다.

3.3. 프리뷰 오브젝트 생성과 재질 교체
private GameObject CreatePreviewObject(GameObject placedObject)
{
    GameObject preview = Instantiate(placedObject, transform);
    preview.transform.position = Vector3.zero;
    previewObjectRenderer = preview.GetComponent<Renderer>();
    if(previewObjectRenderer == null)
        foreach (var renderer in preview.GetComponentsInChildren<Renderer>())
        {
            PreparePreviewPrefab(renderer);
        }
    else
        PreparePreviewPrefab(previewObjectRenderer);
    return preview;
}

이 함수는 일반 배치 모드에서 사용할 프리뷰 오브젝트를 생성하고, 그 재질을 프리뷰 전용 투명 재질로 바꾸는 역할을 한다.

StartShowingPreview가 프리뷰를 시작한다는 상위 함수라면, 이 함수는 프리뷰 오브젝트를 어떻게 만들지를 담당하는 하위 구현이다.

먼저 원본 프리팹을 복제해 preview를 만든다.

여기서도 부모를 현재 transform으로 설정해 프리뷰 계층 아래에 배치한다.

그 다음 위치를 Vector3.zero로 맞추는데, 이건 초기 생성 시 월드 기준으로 엉뚱한 위치에 떠 있는 상태를 방지하기 위한 기본화 작업이다.

실제 위치는 이후 MovePreview에서 다시 설정되므로, 생성 시점에는 0 위치에 두는 것이 안전하다.

다음 줄에서는 GetComponent<Renderer>()를 사용해 루트 오브젝트에 Renderer가 있는지를 확인한다.

Unity의 GetComponent<T>()는 특정 컴포넌트를 찾는 가장 기본적인 API다.

장점은 직관적이고 사용이 간단하다는 점이고, 단점은 반복 호출 시 비용이 생긴다는 점이다.

여기서는 생성 시 1회만 호출되므로 큰 문제가 없다.

그런데 모든 프리팹이 루트에 Renderer를 갖는 것은 아니다.

어떤 프리팹은 시각적 메시는 자식 오브젝트들에만 붙어 있을 수 있다.

그래서 루트에서 Renderer를 찾지 못하면 GetComponentsInChildren<Renderer>()를 사용한다.

이 함수는 자신과 자식 계층 전체에서 모든 Renderer를 찾아 배열로 반환한다.

장점은 복잡한 프리팹 계층도 대응 가능하다는 점이고, 단점은 루트 하나만 찾는 것보다 비용이 크다는 점이다.

하지만 이 함수는 생성 시점에 한 번만 호출되므로, 안정성을 우선한 선택이라고 볼 수 있다.

루트에 Renderer가 있으면 그 하나만 PreparePreviewPrefab에 넘기고, 없으면 자식의 모든 Renderer를 순회하면서 각각 처리한다.

이 구조 덕분에 PlacementPreview는 프리팹 구조가 단순하든 복잡하든 모두 대응할 수 있다.

3.4. 프리뷰 재질 준비
private void PreparePreviewPrefab(Renderer renderer)
{
    previewObjectRenderer = renderer;
    Material[] newMaterialArray = new Material[previewObjectRenderer.materials.Length];
    for (int i = 0; i < newMaterialArray.Length; i++)
    {
        newMaterialArray[i] = transparentMaterialInstance;
    }
    previewObjectRenderer.materials = newMaterialArray;
}

이 함수는 특정 Renderer가 사용하는 모든 Material을 프리뷰용 투명 재질로 교체하는 역할을 한다.

프리뷰 시스템에서 중요한 것은 실제 구조물과 형태는 같되, 시각적으로는 아직 확정되지 않은 상태라는 점을 보여주는 것이다. 그래서 재질을 통째로 교체하는 방식을 택하고 있다.

previewObjectRenderer.materials.Length를 통해 현재 Renderer가 사용하는 Material 개수를 확인한 뒤, 같은 길이의 새 배열을 만든다.

Unity에서 하나의 Renderer는 여러 서브메시를 가지면서 여러 Material을 사용할 수 있기 때문에, 단일 Material만 바꾸는 방식으로는 전체를 일관되게 처리할 수 없다.

그래서 길이를 맞춘 새 배열을 만든 뒤, 모든 슬롯에 같은 transparentMaterialInstance를 넣는다.

마지막으로 previewObjectRenderer.materials = newMaterialArray;를 통해 실제 재질 배열을 교체한다.

이 코드는 프리뷰 오브젝트의 외형을 통째로 프리뷰용 표현으로 바꾸는 결정적 단계다.

이 방식의 장점은 프리뷰 표현이 매우 일관적이라는 점이다.

프리팹이 여러 머티리얼을 쓰더라도 결과적으로 모두 같은 프리뷰 재질로 보이게 된다.

단점은 원래 재질 정보를 잃는다는 점인데, 이 시스템은 실제 오브젝트가 아니라 미리보기 전용 오브젝트에 적용되기 때문에 문제가 되지 않는다.

3.5. 프리뷰 위치 및 회전 갱신
public void MovePreview(List<Vector3> positions, List<Quaternion> rotation)
{
    if(previewObjects.Count > positions.Count)
    {
        for (int i = previewObjects.Count - 1; i >= positions.Count; i--)
        {
            Destroy(previewObjects[i]);
            previewObjects.RemoveAt(i);
        }
    }
    for (int i = 0; i < positions.Count; i++)
    {
        if(previewObjects.Count == i)
        {
            previewObjects.Add(Instantiate(previewTemplate));
        }
        Vector3 pos = positions[i];
        pos.y += yOffset;
        previewObjects[i].transform.position = pos;
        previewObjects[i].transform.localScale = Vector3.one*1.02f;
        if (previewObjects[i].transform.childCount != 0)
            previewObjects[i].transform.GetChild(0).rotation = rotation[i];
        else
            previewObjects[i].transform.rotation = rotation[i];
    }
}

이 함수는 PlacementPreview의 핵심이다.

선택 결과가 바뀔 때마다 호출되며, 현재 선택 위치 목록과 회전값 목록에 맞춰 프리뷰 오브젝트들을 갱신한다.

처음 부분에서는 현재 프리뷰 개수가 새로 전달된 위치 개수보다 많은 경우를 처리한다.

if(previewObjects.Count > positions.Count)
{
    for (int i = previewObjects.Count - 1; i >= positions.Count; i--)
    {
        Destroy(previewObjects[i]);
        previewObjects.RemoveAt(i);
    }
}

이 코드는 선택 범위가 줄어든 상황을 처리한다.

예를 들어 여러 셀을 선택하다가 범위를 줄이면 더 이상 필요 없는 프리뷰 오브젝트가 생긴다.

이를 제거하지 않으면 화면에 남아 있게 된다.

반복문을 뒤에서 앞으로 도는 이유도 중요하다.

List에서 중간 요소를 제거하면 인덱스가 당겨지기 때문에, 앞에서부터 제거하면 잘못된 요소를 건너뛰거나 인덱스 오류가 발생할 수 있다.

뒤에서 앞으로 제거하면 이런 문제가 없다.

Destroy는 Unity에서 오브젝트를 제거하는 API다.

즉시 메모리에서 사라지는 것은 아니고 프레임 끝에서 제거되지만, 일반적인 씬 오브젝트 관리에서는 표준적인 방식이다.

장점은 사용이 간단하고 안전하다는 점이고, 단점은 매우 자주 반복하면 비용이 생길 수 있다는 점이다.

이 시스템은 프리뷰 수가 극단적으로 많은 구조가 아니기 때문에 현재 방식이 충분히 합리적이다.

그 다음 본격적으로 위치 목록을 순회한다.

for (int i = 0; i < positions.Count; i++)

이 반복문은 현재 필요한 프리뷰 개수만큼 오브젝트를 준비하고 위치와 회전을 적용하는 역할을 한다.

먼저 아직 프리뷰 오브젝트 수가 부족하면 새로 만든다.

if(previewObjects.Count == i)
{
    previewObjects.Add(Instantiate(previewTemplate));
}

이 코드는 없으면 생성하고, 있으면 재사용한다는 구조다.

즉, 매번 전부 Destroy 후 재생성하는 것이 아니라, 가능한 한 기존 프리뷰를 재사용하고 필요한 만큼만 추가 생성한다.

이 방식은 성능상 훨씬 유리하다.

그 다음 위치를 설정한다.

Vector3 pos = positions[i];
pos.y += yOffset;
previewObjects[i].transform.position = pos;

yOffset을 더하는 이유는 프리뷰 오브젝트가 바닥면과 정확히 같은 높이에 놓일 경우 Z-fighting이 발생할 수 있기 때문이다.

Z-fighting은 두 면이 거의 같은 깊이에 있을 때 렌더러가 어느 면을 먼저 그릴지 흔들리면서 깜빡이는 현상이다.

그래서 프리뷰를 아주 조금 위로 띄워 시각적 겹침을 피한다. 이는 건축 시스템에서 자주 쓰는 실용적인 처리다.

다음은 스케일 보정이다.

previewObjects[i].transform.localScale = Vector3.one*1.02f;

이 코드는 프리뷰를 실제 구조물보다 아주 약간 크게 보여준다.

이것도 Z-fighting 완화와 시각적 구분을 위한 처리다.

구조물과 정확히 같은 크기라면 배경과 섞여 보일 수 있는데, 1.02배 정도로 살짝 키우면 플레이어가 프리뷰라는 것을 더 쉽게 인식할 수 있다.

마지막으로 회전을 적용한다.

if (previewObjects[i].transform.childCount != 0)
    previewObjects[i].transform.GetChild(0).rotation = rotation[i];
else
    previewObjects[i].transform.rotation = rotation[i];

이 부분도 프리팹 구조 대응을 위한 방어 코드다.

어떤 프리팹은 실제 메쉬가 자식 오브젝트에 있고, 루트는 빈 컨테이너 역할만 할 수 있다.

그런 경우 루트 회전만 바꾸면 원하는 시각적 결과가 나오지 않을 수 있다.

그래서 자식이 있으면 첫 번째 자식에 회전을 적용하고, 그렇지 않으면 루트에 직접 적용한다.

이 코드의 장점은 프리팹 구조가 완전히 동일하지 않아도 어느 정도 유연하게 대응할 수 있다는 점이다.

단점은 첫 번째 자식이 실제 메쉬라는 암묵적 가정이 들어간다는 점인데, 현재 프로젝트의 프리팹 구조가 그 규칙을 따르기 때문에 가능한 선택이다.

3.6. 프리뷰 피드백 전환
public void ShowPlacementFeedback(bool val)
{
    if (val)
        PlacementFeedbackPositive();
    else
        PlacementFeedbackNegative();
}

이 함수는 배치 가능 여부를 시각 피드백으로 연결하는 인터페이스다.

Validator가 계산한 placementValidity 같은 값을 받아, 실제로 어떤 색을 쓸지 결정하는 것은 내부 함수에 위임한다.

이 구조가 좋은 이유는 역할이 분리되어 있기 때문이다.

외부 시스템은 단지 '가능 / 불가능' 논리값만 넘기면 되고, 색을 어떻게 바꾸고 어떤 표현을 쓰는지는 PlacementPreview 내부가 책임진다.

즉 표현 방식이 바뀌더라도 외부 코드를 수정할 필요가 없다.

코드 자체는 단순한 if 분기지만, 구조적으로는 논리 결과를 시각 표현으로 번역하는 어댑터 함수라고 볼 수 있다.

3.7. 배치 가능 상태 피드백
private void PlacementFeedbackPositive()
{
    transparentMaterialInstance.color = defautlColor;
}

이 함수는 현재 위치가 유효한 배치 위치일 때 프리뷰 재질을 기본 색상으로 복원하는 역할을 한다.

배치 불가능 상태에서 빨간색으로 바뀌었던 프리뷰를 다시 정상 색으로 되돌리는 기능이라고 보면 된다.

여기서 transparentMaterialInstance.color를 직접 수정하는 이유는, 이미 모든 프리뷰 오브젝트가 이 재질 인스턴스를 공유하고 있기 때문이다.

따라서 한 번 색을 바꾸면 프리뷰 전체가 동시에 색을 바꾸게 된다.

이는 다중 프리뷰 환경에서 매우 효율적인 구조다.

각 오브젝트를 하나씩 돌면서 색을 바꿀 필요가 없고, 공유 재질 인스턴스 하나만 수정하면 되기 때문이다.

장점은 코드가 매우 간단하고 갱신 비용이 낮다는 점이다.

단점은 모든 프리뷰가 같은 재질 인스턴스를 공유하므로, 개별 오브젝트별 색상 차등 표현은 어렵다는 점이다.

하지만 이 시스템은 전체 선택의 유효성만 보여주면 되므로 현재 요구사항에는 잘 맞는다.

3.8. 배치 가능 여부 시각화
private void PlacementFeedbackNegative()
{
    Color c = Color.red;
    c.a = defautlColor.a;
    transparentMaterialInstance.color = c;
}

PlacementFeedbackNegative 함수는 현재 위치가 배치 불가능할 때 프리뷰를 빨간색으로 바꾸는 역할을 한다.

핵심은 단순히 Color.red를 그대로 넣지 않고, 알파값을 기본 색의 알파로 다시 맞춘다는 점이다.

Color c = Color.red;
c.a = defautlColor.a;

이 처리는 매우 중요하다.

프리뷰 재질은 투명 재질이기 때문에, 단순히 빨간색으로 바꾸면 알파값까지 기본값으로 바뀔 수 있다.

그러면 프리뷰가 너무 진하게 보이거나, 오히려 투명도가 깨질 수 있다.

그래서 RGB는 빨간색으로 바꾸되, 투명도는 기존 알파값을 유지하기 위해 c.a = defautlColor.a를 수행한다.

이는 Unity의 Color 구조 때문이다.

Unity에서 Color는 RGBA 값을 가지며, 알파값은 투명도를 의미한다.

만약 알파값을 유지하지 않고 Color.red를 그대로 적용하면 프리뷰가 완전히 불투명해질 수 있다.

이 경우 프리뷰가 원래 의도한 투명한 안내 UI가 아니라, 실제 오브젝트처럼 보이게 되어 사용자 경험을 해칠 수 있다.

따라서 이 코드에서는 색상만 변경하고 투명도는 유지하는 방식으로 설계되어 있다.

이러한 설계는 단순한 색 변경이 아니라, 프리뷰의 시각적 일관성을 유지하면서 상태만 전달하는 UX 설계라고 볼 수 있다.

또한 이 방식은 Material 하나만 수정하면 모든 프리뷰 오브젝트에 동시에 반영된다는 특징을 가진다.

이는 Unity에서 Material이 Renderer에 공유되어 사용되는 구조이기 때문이다.

이 덕분에 프리뷰 오브젝트가 여러 개일 경우에도 각각을 순회하면서 색상을 변경할 필요 없이, Material 한 번만 수정하면 전체 프리뷰 상태를 갱신할 수 있다.

마지막 줄에서 그 색을 공유 재질 인스턴스에 적용한다.

이 처리로 인해 현재 활성화된 모든 프리뷰가 동시에 빨간색으로 보이게 된다.

즉 이 함수는 개별 오브젝트를 하나하나 조작하는 것이 아니라, 공유 재질 인스턴스를 수정해 전체 피드백을 일괄 적용하는 방식으로 동작한다.

3.9. 프리뷰 종료 및 정리
public void StopShowingPreview()
{
    previewObjectRenderer = null;
    transparentMaterialInstance.color = defautlColor;
    foreach (var item in previewObjects)
    {
        Destroy(item);
    }
    if(cursorObject != null)
        Destroy(cursorObject);
    previewTemplate = null;
    previewObjects.Clear();
}

이 함수는 프리뷰 시스템의 종료 처리다.

건축 모드가 끝나거나 상태가 변경될 때, 화면에 남아 있는 프리뷰를 모두 제거하고 내부 상태를 초기화한다.

먼저 previewObjectRenderer = null로 현재 참조를 비운다.

이는 이후 잘못된 참조 재사용을 방지하기 위한 정리 작업이다.

다음 줄에서 transparentMaterialInstance.color = defautlColor를 호출하는 것도 중요하다.

프리뷰가 이전 상태에서 빨간색으로 바뀌어 있었을 수 있기 때문에, 다음 번 시작할 때 색상이 남지 않도록 기본색으로 복원한다.

즉, 이 함수는 오브젝트만 지우는 것이 아니라 프리뷰 시스템 상태 전체를 초기화하는 역할을 한다.

그 다음 previewObjects 리스트를 순회하며 모든 프리뷰 오브젝트를 Destroy한다.

이 리스트는 현재 활성 프리뷰들의 실제 참조를 들고 있으므로, 여기만 정리하면 프리뷰 본체는 모두 제거된다.

cursorObject도 null이 아니면 제거하는데, 현재 업로드된 코드만 놓고 보면 이 필드는 실제 사용되지 않지만, 향후 커서형 프리뷰를 따로 지원하거나 이전 구현 흔적일 가능성이 있다.

중요한 것은 사용 중일 수 있는 보조 프리뷰 오브젝트까지 함께 정리하는 구조를 미리 갖고 있다는 점이다.

마지막으로 previewTemplate = null과 previewObjects.Clear()를 호출해 내부 상태를 완전히 비운다.

이렇게 해야 다음 프리뷰 세션이 이전 상태를 전혀 물려받지 않고 깨끗한 상태에서 시작된다.

즉, 이 함수는 단순 종료가 아니라, 프리뷰 시스템의 생명주기를 닫는 정리 함수라고 볼 수 있다.

4. 개발 의도

PlacementPreview의 핵심 설계 의도는 건축 시스템의 상태를 플레이어에게 즉시, 그리고 명확하게 전달하는 것이다.

건축 시스템은 입력, 선택, 검증, 실행이 빠르게 이어지는 구조이기 때문에, 플레이어는 현재 위치가 어떤 의미를 가지는지 한눈에 이해할 수 있어야 한다.

이를 위해 PlacementPreview는 실제 배치 로직과 분리된 독립적인 시각화 계층으로 설계되었다.

PlacementManager가 결과를 전달하면, PlacementPreview는 오직 그 결과를 화면에 표현하는 데만 집중한다.

또한 이 시스템은 단순한 단일 프리뷰가 아니라, 다중 선택과 회전, 제거 모드, 유효성 피드백까지 모두 처리할 수 있도록 구성되어 있다.

이 과정에서 Material 인스턴스 분리, 자식 Renderer 탐색, yOffset 보정, 기본색 복원 같은 세부 구현들은 단순 편의 코드가 아니라, 실제 프로젝트에서 발생하는 시각적 문제와 프리팹 구조 다양성에 대응하기 위한 설계 선택들이다.

결과적으로 PlacementPreview는 단순한 미리 보기 기능이 아니라, 선택 결과와 검증 결과를 플레이어가 이해할 수 있는 화면 표현으로 번역하는 핵심 View Layer로 설계되었다.