화면 중앙을 기준으로 라인트레이스를 쏘고, 해당 위치를 그리드 좌표로 변환한 뒤 건물을 셀 단위로 배치하는 구조를 만들었다.
1. 건물 데이터 에셋 생성
건물마다 차지하는 그리드 크기와 실제 생성할 클래스를 다르게 설정하기 위해 GridBuildingDataAsset을 만들었다.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Building")
TSubclassOf<AActor> BuildingClass;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Grid")
FIntPoint GridSize = FIntPoint(1, 1);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Grid")
float PlacementZOffset = 0.0f;
2. 화면 중앙 기준 라인트레이스 구현
마우스 위치가 아니라 플레이어가 바라보는 화면 중앙을 기준으로 건설 위치를 찾도록 했다.
PC->GetPlayerViewPoint(ViewLocation, ViewRotation);
const FVector TraceStart = ViewLocation;
const FVector TraceEnd = TraceStart + ViewRotation.Vector() * TraceDistance;
이후 라인트레이스가 맞은 위치를 그리드 좌표로 변환했다.
const FIntPoint HitGrid = GridSub->WorldToGridPosition(HitResult.ImpactPoint);
3. 월드 좌표와 그리드 좌표 변환
GridSubsystem에서 월드 좌표를 그리드 좌표로 바꾸는 함수를 작성했다.
FIntPoint UGridSubsystem::WorldToGridPosition(const FVector& WorldLocation) const
{
const int32 X = FMath::FloorToInt((WorldLocation.X - GridOrigin.X) / CellSize);
const int32 Y = FMath::FloorToInt((WorldLocation.Y - GridOrigin.Y) / CellSize);
return FIntPoint(X, Y);
}
월드상의 위치를 그리드 셀 단위로 계산
4. 고스트 프리뷰 생성
건물을 바로 설치하지 않고, 먼저 고스트 프리뷰를 보여주도록 GridBuildingPreviewActor를 만들었다.
AGridBuildingPreviewActor* PreviewActor = World->SpawnActor<AGridBuildingPreviewActor>(
PreviewActorClass,
FVector::ZeroVector,
FRotator::ZeroRotator,
SpawnParams
);
실제 건물 클래스의 메시를 읽어와 프리뷰 메시로 사용했다.
PreviewActor->SetupFromBuildingClass(CurrentBuildingData->BuildingClass);
별도의 PreviewMesh를 DataAsset에 따로 넣지 않고, 실제 건물 블루프린트의 메시를 그대로 활용하였다.
5. 설치 가능 여부에 따라 머티리얼 변경
현재 위치에 설치할 수 있는지에 따라 고스트 머티리얼을 다르게 적용했다.
UMaterialInterface* TargetMaterial = bCanBuild
? BuildableGhostMaterial
: NotBuildableGhostMaterial;
설치 가능한 위치면 설치 가능 머티리얼을, 불가능한 위치면 설치 불가능 머티리얼을 적용했다.
6. 바닥 탐색 채널과 배치 충돌 채널 분리
바닥을 찾는 Collision Channel과 건물 배치 충돌을 검사하는 Collision Channel을 분리하였다.
바닥과 장애물 충돌이 같은 기준으로 처리되면, 바닥 자체가 충돌 대상으로 잡혀서 건물이 설치 불가능한 위치로 판단될 수 있었다. 그래서 GridSubsystem 안에서 두 채널을 따로 두었다.
// 지형 찾을 때 사용할 채널
UPROPERTY(EditDefaultsOnly, Category = "Grid|Trace")
TEnumAsByte<ECollisionChannel> GroundTraceChannel = ECC_Visibility;
// 배치 충돌 검사 채널
UPROPERTY(EditDefaultsOnly, Category = "Grid|Collision")
TEnumAsByte<ECollisionChannel> PlacementCollisionChannel = ECC_GameTraceChannel1;
GroundTraceChannel은 바닥을 찾기 위한 라인트레이스에 사용하고, PlacementCollisionChannel은 건물이 다른 장애물이나 건물과 겹치는지 확인할 때 사용한다.
7. 건물이 차지할 그리드 영역 계산
2x2 같은 건물은 한 칸만 차지하지 않기 때문에 Anchor 좌표를 기준으로 영역을 계산했다.
CurrentAnchor = FIntPoint(AnchorX, AnchorY);
그리고 해당 영역이 모두 설치 가능한지 검사했다.
const bool bCanBuild = GridSub->CanBuildArea(
CurrentAnchor,
CurrentBuildingSize,
bCheckPlacementCollision
);
8. 실제 건물 설치 및 점유 처리
설치 가능한 위치라면 실제 건물을 생성하고, 해당 그리드 영역을 점유 처리했다.
AActor* NewBuilding = World->SpawnActor<AActor>(
CurrentBuildingData->BuildingClass,
SpawnLocation,
FRotator::ZeroRotator,
SpawnParams
);
GridSub->OccupyArea(
CurrentAnchor,
CurrentBuildingSize,
NewBuilding
);
건물이 설치된 셀에는 다른 건물이 중복으로 설치되지 않도록 처리하였다.
정리
1x1 건물은 단순하지만, 2x2나 2x3처럼 여러 칸을 차지하는 건물은 기준점이 중요했다.
그래서 Anchor 좌표를 기준으로 건물이 점유할 셀을 계산하는 구조를 만들었다.
또한 고스트 프리뷰를 실제 건물 메시에서 읽어오도록 구현하면서, DataAsset에 중복 데이터를 넣지 않아도 되는 구조로 정리하였다.
다음에는 건물이 파괴되었을 때 점유된 그리드 셀을 해제하는 기능 작업을 진행할 예정이다.
'Unreal 최종 팀프로젝트' 카테고리의 다른 글
| [TIL] 건설 회전 기능 | 최종 팀프로젝트 (0) | 2026.06.02 |
|---|---|
| [TIL] UI 입력 흐름 | 최종 팀프로젝트 (0) | 2026.06.01 |
| [TIL] 설비 및 아이템 데이터화 | 최종 팀프로젝트 (0) | 2026.05.22 |
| [TIL] 건설 시스템 구조 확장 | 최종 팀프로젝트 (0) | 2026.05.21 |
| [TIL] 그리드 기반 건물 파괴 | 최종 팀프로젝트 (0) | 2026.05.20 |