건축 객체 배치 실행 시스템 ( PlacementManager (2) )
1. 시스템 요구 사항
Grid 기반 건축 시스템에서 플레이어가 구조물을 설치하기 위해서는 단순히 GameObject를 생성하는 것 이상의 과정이 필요하다.
플레이어가 마우스를 통해 선택한 위치는 먼저 Grid 좌표로 해석되어야 하며, 해당 위치가 실제로 구조물을 배치할 수 있는 공간인지 검증되어야 한다.
배치 가능 여부가 확인되면 실제 구조물을 씬에 생성하고, 동시에 Grid 데이터 시스템에도 해당 구조물이 점유한 셀 정보를 기록해야 한다.
이 과정에서 중요한 점은 씬에 존재하는 실제 GameObject와 Grid 데이터가 항상 동일한 상태를 유지해야 한다는 것이다.
예를 들어 씬에서는 구조물이 생성되었지만 Grid 데이터에 해당 정보가 기록되지 않았다면 이후 공간 검사나 충돌 판정에서 오류가 발생할 수 있다.
반대로 Grid 데이터에만 구조물이 기록되고 실제 GameObject가 존재하지 않는 경우에도 시스템 상태가 일관성을 잃게 된다.
또한 건축 시스템에서는 구조물의 배치 방식이 여러 종류로 나뉜다.
바닥이나 가구와 같은 오브젝트는 Grid 셀 중심을 기준으로 배치되지만, 벽과 같은 구조물은 셀의 경계선을 기준으로 배치된다.
이러한 차이는 단순한 좌표 계산의 차이가 아니라 Grid 데이터에 기록되는 방식 자체를 다르게 만든다.
셀 기반 구조물은 셀 좌표를 기준으로 저장해야 하고, 벽과 같은 구조물은 셀 경계 정보를 기준으로 저장해야 한다.
또한 건축 시스템에서는 구조물 배치가 단순히 한 번 실행되는 작업이 아니라 Undo 시스템과도 연결되는 동작이기 때문에 모든 배치 실행은 기록 가능한 명령(Command) 형태로 관리될 필요가 있다.
따라서 건축 시스템에서는 배치 요청이 발생했을 때 다음과 같은 기능을 수행하는 실행 계층이 필요하다.
선택된 Grid 위치를 기준으로 실제 구조물을 씬에 생성한다.
생성된 구조물의 인덱스를 Grid 데이터 시스템에 기록한다.
셀 기반 배치와 경계 기반 배치를 구분하여 저장한다.
배치 완료 이후 선택 상태를 갱신한다.
Undo 시스템과 연결하기 위해 배치 기록을 명령 객체 형태로 관리한다.
PlacementManager 내부의 배치 실행 로직은 이러한 역할을 수행하도록 설계되어 있다.
2. 흐름도
SelectionResult 생성
↓
PlacementManager.TryPlacingObjects
↓
CommandManager.Invoke
↓
StructurePlacementCommand.Execute
↓
PlacementManager.PlaceStructureAt
↓
StructurePlacer.PlaceStructure
↓
PlacementGridData 업데이트
↓
BuildingState.RefreshSelection
건축 시스템에서 실제 구조물 배치는 여러 단계의 과정을 거쳐 이루어진다.
먼저 BuildingState 시스템이 플레이어의 선택 정보를 분석하여 SelectionResult 객체를 생성한다.
SelectionResult는 플레이어가 선택한 결과를 표현하는 데이터 구조로, 선택된 Grid 좌표 목록, 월드 좌표, 회전 값, 배치 가능 여부 등의 정보를 포함하고 있다.
PlacementManager는 이 SelectionResult 정보를 기반으로 TryPlacingObjects 함수를 호출하여 실제 배치를 수행할지 여부를 판단한다.
이때 배치 동작은 CommandManager를 통해 실행되며, 이는 Undo 시스템과 연결하기 위한 구조이다.
CommandManager는 실행된 명령 객체를 기록하여 이후 Undo 요청이 발생했을 때 해당 명령을 되돌릴 수 있도록 한다.
Command 객체가 실행되면 PlacementManager의 PlaceStructureAt 함수가 호출되고, 이 함수가 실제 구조물 생성과 Grid 데이터 기록을 수행한다.
3. 구현
3.1. 배치 실행 진입점
private void TryPlacingObjects(SelectionResult selectionResult)
{
if (itemData.objectPlacementType == PlacementType.InWalls)
{
int previousItemIndex = gridData.WallPlacementData.GetStructureIDForEdgeObject(
buildingState.SelectionData.GetSelectedGridPositions()[0],
Mathf.RoundToInt(buildingState.SelectionData.GetSelectedPositionsGridRotation()[0].eulerAngles.y));
commandManager.Invoke(
new StructureSwapCommand(
this,
buildingState.CurrentPlacementData,
gridData.WallPlacementData,
gridManager,
itemData,
selectionResult,
structuresData.GetItemWithID(previousItemIndex)
)
);
if (selectionResult.placementValidity)
{
OnPlaceConstructionObject?.Invoke();
}
else
{
OnInvalidPlacement?.Invoke();
}
}
else
{
commandManager.Invoke( new StructurePlacementCommand(
this,
buildingState.CurrentPlacementData,
itemData,
selectionResult
)
);
if (selectionResult.placementValidity)
{
if (itemData.objectPlacementType == PlacementType.Floor || itemData.objectPlacementType == PlacementType.Wall)
OnPlaceConstructionObject?.Invoke();
else
OnPlaceFurnitureObject?.Invoke();
}
else
{
OnInvalidPlacement?.Invoke();
}
}
}이 함수는 건축 시스템에서 구조물 배치가 실제로 실행되는 진입점 역할을 한다.
BuildingState 시스템이 선택 결과를 계산하면 해당 결과가 PlacementManager로 전달되고, PlacementManager는 이 정보를 바탕으로 실제 배치를 수행할지 판단한다.
이 함수의 중요한 특징은 구조물을 직접 생성하지 않고 Command 시스템을 통해 배치를 수행한다는 점이다.
CommandManager는 Command Pattern 기반 구조로 설계되어 있으며, 모든 배치 동작을 명령 객체 형태로 기록한다.
Command Pattern은 특정 동작을 별도의 객체로 캡슐화하는 디자인 패턴으로, 실행된 동작을 기록하고 되돌릴 수 있다는 장점이 있다.
건축 시스템에서는 플레이어가 구조물을 설치한 뒤 이를 되돌리는 기능이 필요하기 때문에 이러한 패턴이 매우 적합하다.
또한 이 함수에서는 구조물의 배치 타입에 따라 서로 다른 Command를 사용한다.
StructurePlacementCommand는 일반적인 구조물 배치를 수행하는 명령 객체이다.
반면 StructureSwapCommand는 벽 내부 오브젝트와 같이 기존 구조물을 교체해야 하는 상황에서 사용된다.
예를 들어 벽 위에 설치되는 창문이나 문과 같은 구조물은 기존 벽을 제거하고 새로운 구조물을 설치해야 한다.
StructureSwapCommand는 이러한 교체 작업을 하나의 명령으로 캡슐화하여 Undo 시스템에서도 동일하게 되돌릴 수 있도록 설계된 Command 객체이다.
3.2. 실제 구조물 배치 함수
[SerializeField]private StructurePlacer structurePlacer;
public void PlaceStructureAt(SelectionResult selectionResult,
PlacementGridData placementData,
ItemData itemData, bool animatePlacement = true)
{
for (int i = 0; i < selectionResult.selectedGridPositions.Count; i++)
{
if (itemData.objectPlacementType.IsEdgePlacement())
{
int objectIndex = structurePlacer.PlaceStructure(
itemData.prefab,
selectionResult.selectedPositions[i],
selectionResult.selectedPositionsObjectRotation[i],
0, animatePlacement);
// 경계 기반 배치 처리
placementData.AddEdgeObject(objectIndex,
itemData.ID,
selectionResult.selectedGridPositions[i],
itemData.size,
Mathf.RoundToInt(selectionResult.selectedPositionGridCheckRotation[i].eulerAngles.y),
Mathf.RoundToInt(selectionResult.selectedPositionsObjectRotation[i].eulerAngles.y));
}
else
{
int objectIndex = structurePlacer.PlaceStructure(itemData.prefab,
selectionResult.selectedPositions[i],
selectionResult.selectedPositionsObjectRotation[i],
0,
animatePlacement);
// 셀 기반 배치 처리
placementData.AddCellObject(objectIndex,
itemData.ID,
selectionResult.selectedGridPositions[i],
itemData.size,
Mathf.RoundToInt(selectionResult.selectedPositionGridCheckRotation[i].eulerAngles.y),
Mathf.RoundToInt(selectionResult.selectedPositionsObjectRotation[i].eulerAngles.y));
}
}
// 배치 이후 상태 갱신
buildingState.RefreshSelection();
OnToggleUndo?.Invoke(true);
}StructurePlacer는 실제 GameObject를 생성하거나 제거하는 시스템이다.
PlacementManager는 직접 Instantiate를 호출하지 않고 StructurePlacer를 통해 구조물을 생성하도록 설계하였다.
이 구조는 실제 오브젝트 생성 로직을 별도의 클래스에 분리하여 책임을 명확히 하기 위한 설계이다.
PlaceStructureAt 함수는 건축 시스템에서 실제 구조물을 씬에 생성하는 핵심 함수이다.
PlacementManager는 SelectionResult에 포함된 정보를 기반으로 구조물을 생성하고 Grid 데이터 시스템을 업데이트한다.
먼저 SelectionResult에 포함된 Grid 위치 목록을 반복하면서 각 위치에 구조물을 생성한다.
SelectionResult는 단일 위치뿐 아니라 여러 셀을 동시에 선택하는 경우도 지원하기 때문에 리스트 형태의 데이터를 사용한다.
구조물 생성 자체는 StructurePlacer 시스템을 통해 수행된다.
StructurePlacer는 실제 GameObject를 Instantiate하는 역할을 담당하며, PlacementManager는 이를 직접 수행하지 않는다.
이 구조는 오브젝트 생성 로직을 별도의 시스템으로 분리하여 책임을 명확히 하기 위한 설계이다.
StructurePlacer.PlaceStructure 함수는 생성된 GameObject의 인덱스를 반환한다.
이 인덱스는 이후 구조물을 제거하거나 이동할 때 사용되는 참조 값이 된다.
Unity에서 GameObject를 생성할 때 Instantiate 함수를 직접 호출할 수도 있지만, StructurePlacer를 별도의 시스템으로 분리한 이유는 오브젝트 생성과 관리 로직을 한 곳에서 처리하기 위해서이다.
이를 통해 애니메이션 처리, 생성 효과, 오브젝트 관리 로직 등을 중앙에서 제어할 수 있다.
placementData.AddEdgeObject(
objectIndex,
itemData.ID,
selectionResult.selectedGridPositions[i],
itemData.size,
gridRotation,
objectRotation
);이 부분은 경계 기반 구조물 배치를 처리하는 코드이다.
벽과 같은 구조물은 셀 중심이 아니라 Grid 셀의 경계선을 기준으로 배치된다.
이러한 구조물은 두 개의 셀 사이에 위치하게 되며, 일반적인 셀 기반 배치와는 다른 데이터 구조를 사용해야 한다.
AddEdgeObject 함수는 이러한 경계 기반 구조물을 Grid 데이터 시스템에 기록하는 역할을 수행한다.
Edge 기반 데이터는 셀 좌표와 회전 정보를 조합하여 특정 Grid 경계를 식별한다.
이 구조를 사용하면 동일한 Grid 좌표라도 서로 다른 방향의 벽을 구분할 수 있다.
예를 들어 동일한 셀에서도 동쪽 벽과 북쪽 벽은 서로 다른 구조물로 저장될 수 있다.
Grid 기반 건축 시스템에서 셀 기반 배치와 경계 기반 배치를 분리하는 설계는 매우 중요하다.
두 방식을 동일한 구조로 처리하려고 하면 벽 배치나 코너 연결과 같은 기능에서 복잡한 예외 처리가 필요하게 되기 때문이다.
placementData.AddCellObject(
objectIndex,
itemData.ID,
selectionResult.selectedGridPositions[i],
itemData.size,
rotationValue,
objectRotation
);이 부분은 셀 기반 배치 처리 부분이다.
셀 기반 배치는 바닥, 가구, 자유 배치 오브젝트와 같은 구조물에 사용된다.
이러한 오브젝트는 Grid 셀의 중심을 기준으로 배치되며, Grid 데이터 시스템에서는 해당 셀 좌표를 기준으로 저장된다.
AddCellObject 함수는 특정 Grid 셀에 구조물이 배치되었다는 정보를 PlacementGridData에 기록한다.
이 데이터에는 구조물 ID, Grid 좌표, 구조물 크기, 회전 값 등이 포함된다.
Grid 데이터 시스템에 이러한 정보를 저장하는 이유는 이후 공간 검사나 충돌 판정을 수행하기 위해서이다.
예를 들어 새로운 구조물을 배치할 때 해당 셀이 이미 점유되어 있는지를 검사해야 하는데, 이때 Grid 데이터가 사용된다.
이 구조는 게임 세계의 실제 오브젝트 상태와 데이터 상태를 동시에 유지하는 방식으로 설계되어 있다.
씬에 존재하는 GameObject는 시각적 표현을 담당하고, Grid 데이터는 논리적인 배치 정보를 관리한다.
buildingState.RefreshSelection();
OnToggleUndo?.Invoke(true);마지막으로 배치 완료 후 상태를 갱신하는 부분이다.
구조물 배치가 완료된 이후에는 현재 선택 상태를 갱신해야 한다.
buildingState.RefreshSelection 함수는 현재 선택 상태를 다시 계산하여 프리뷰 위치나 배치 가능 여부를 업데이트한다.
건축 시스템에서는 구조물이 배치되면 Grid 점유 상태가 변경되기 때문에 이전 선택 결과가 더 이상 유효하지 않을 수 있다.
따라서 RefreshSelection을 통해 현재 마우스 위치 기준으로 다시 배치 가능 여부와 미리보기 위치를 계산하도록 설계하였다.
또한 OnToggleUndo 이벤트를 호출하여 Undo 기능이 활성화될 수 있도록 한다.
이 이벤트는 UI 시스템과 연결되어 있으며 Undo 버튼의 활성화 상태를 제어하는 데 사용된다.
4. 개발 의도
건축 객체 배치 실행 시스템의 핵심 설계 의도는 씬의 실제 구조물 상태와 Grid 데이터 상태를 항상 동기화하는 것이다.
건축 시스템에서는 구조물이 어디에 배치되어 있는지를 정확하게 관리해야 하기 때문에 데이터와 실제 오브젝트 상태가 서로 어긋나지 않도록 하는 것이 매우 중요하다.
이를 위해 PlacementManager는 구조물 생성 로직과 Grid 데이터 기록 로직을 동시에 수행하도록 설계하였다.
StructurePlacer는 실제 GameObject를 생성하고, PlacementGridData는 해당 구조물이 점유한 Grid 정보를 기록한다.
또한 셀 기반 배치와 경계 기반 배치를 분리하여 Grid 시스템의 구조를 명확하게 유지하였다.
이 구조 덕분에 벽과 같은 특수한 구조물도 안정적으로 처리할 수 있다.
마지막으로 Command Pattern을 사용하여 배치 동작을 명령 객체 형태로 실행하도록 설계하였다.
이 구조는 Undo 기능을 구현하는 데 매우 적합하며 건축 시스템에서 발생하는 다양한 동작을 일관된 방식으로 관리할 수 있도록 해준다.
결과적으로 이 시스템은 단순히 구조물을 생성하는 기능이 아니라 선택 결과를 실제 구조물 배치로 변환하고 Grid 데이터와 씬 상태를 동기화하는 실행 계층(Execution Layer)으로 설계되었다.
