2024.08.02
이번 글에서는 프로젝트 StackBlock의 개발 과정, 문제 해결 방법, 최적화 과정 및 최종 릴리즈에 대한 모든 것을 정리합니다. 각 단계에서 겪었던 어려움과 그에 대한 해결 방법을 상세히 설명합니다.
UE5 Project_StackBlock #1 - 기획 초안
UE5 언리얼 엔진 협업을 위한 Perforce 설정 및 문제 해결 가이드 - Github 대신 Perforce를 써야 하는 이유
UE5 Project_StackBlock #2 - DynamicMeshComponent 활용 및 메쉬 불리언 연산 문제 해결
UE5 Project_StackBlock #3 - DynamicMeshComponent의 겹친 영역을 완벽히 제거하는 방법!
다음의 릴리즈 버전에서 게임을 해볼 수 있습니다.
Project_StackBlock | Project_StackBlock v1.0 |
---|---|
깃허브 레포지토리 | 릴리즈 |
게임 초기 화면 |
게임 플레이 화면 |
StackBlock은 플레이어가 다양한 블록을 쌓아 올리며 높은 점수를 목표로 하는 퍼즐 게임입니다. UE5의 기능을 활용하여 설계했습니다. 이번 프로젝트를 통해 얻은 많은 경험을 공유하고자 합니다.
게임의 초기 기획 단계에서, 우리는 다음과 같은 핵심 요소들을 결정했습니다:
블록의 동적 생성과 관리는 게임의 핵심 요소 중 하나였습니다. 다음과 같은 과정을 거쳐서 만들었습니다.
AABB의 자세한 내용은 여기를 참고하면 좋습니다.
[UE5] Project_StackBlock #3 - DynamicMeshComponent의 겹친 영역을 완벽히제거하는 방법!
다음처럼 OnConstruction에서 HitBox의 크기에 맞게끔 다이나믹 메시가 생성이되고 GetOverlappedArea에서 겹친 영역의 크기와 좌표를 가지는 다이나믹 메시를 생성해서 반환하게끔 만들었습니다.
#include "DynamicBlockActor.h"
#include "AI/NavigationModifier.h"
#include "GeometryScript/MeshPrimitiveFunctions.h"
ADynamicBlockActor::ADynamicBlockActor()
{
BoxCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxCollision"));
BoxCollision->SetupAttachment(DynamicMeshComponent);
}
// BoxCollision의 Scale에 따라서 매시의 크기가 바뀜
void ADynamicBlockActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
UDynamicMesh* DynamicMesh = DynamicMeshComponent->GetDynamicMesh();
if(DynamicMesh)
{
DynamicMesh->Reset();
FTransform T;
FGeometryScriptPrimitiveOptions Options;
FVector Dimension = BoxCollision->GetScaledBoxExtent();
float DimensionX = Dimension.X * 2;
float DimensionY = Dimension.Y * 2;
float DimensionZ = Dimension.Z * 2;
float TransformLocationZ = Dimension.Z * -1;
T.SetLocation(FVector(0,0,TransformLocationZ));
UGeometryScriptLibrary_MeshPrimitiveFunctions::AppendBox(DynamicMesh, Options, T, DimensionX, DimensionY, DimensionZ);
DynamicMeshComponent->EnableComplexAsSimpleCollision();
}
}
// 겹쳐진 영역의 좌표, 크기를 구하는 함수
void ADynamicBlockActor::SetOverlapExtentAndLocation(UBoxComponent* Box1, UBoxComponent* Box2)
{
FVector Box1Location = Box1->GetComponentLocation();
FVector Box1Extent = Box1->GetScaledBoxExtent();
FVector Box2Location = Box2->GetComponentLocation();
FVector Box2Extent = Box2->GetScaledBoxExtent();
FVector Max1 = Box1Location + Box1Extent;
FVector Min1 = Box1Location - Box1Extent;
FVector Max2 = Box2Location + Box2Extent;
FVector Min2 = Box2Location - Box2Extent;
FVector OverlapMax = FVector(FMath::Min(Max1.X, Max2.X), FMath::Min(Max1.Y, Max2.Y), FMath::Min(Max1.Z, Max2.Z));
FVector OverlapMin = FVector(FMath::Max(Min1.X, Min2.X), FMath::Max(Min1.Y, Min2.Y), FMath::Max(Min1.Z, Min2.Z));
OverlapLocationVector = (OverlapMin + OverlapMax) / 2.0f;
OverlapExtentVector = OverlapMax - OverlapMin;
UE_LOG(LogTemp, Warning, TEXT("OverlapLocationVector : %s"), *OverlapLocationVector.ToString());
UE_LOG(LogTemp, Warning, TEXT("OverlapExtentVector : %s"), *OverlapExtentVector.ToString());
}
// 겹친 영역 만큼 DynamicMesh를 만들고 Return하는 함수
UDynamicMesh* ADynamicBlockActor::GetOverlappedArea(UBoxComponent* TargetBoxComponent)
{
UDynamicMesh* TargetMesh = nullptr;
SetOverlapExtentAndLocation(BoxCollision, TargetBoxComponent);
UDynamicMesh* DynamicMesh = DynamicMeshComponent->GetDynamicMesh();
FTransform Transform = DynamicMeshComponent->GetComponentTransform();
FVector BoxTransform = Transform.InverseTransformPosition(OverlapLocationVector);
if(DynamicMesh)
{
UDynamicMesh* ComputeMesh = AllocateComputeMesh();
if(ComputeMesh)
{
FTransform T;
FGeometryScriptPrimitiveOptions Options;
FVector LocalOverlapLocation = FVector(BoxTransform.X, BoxTransform.Y, BoxTransform.Z - OverlapExtentVector.Z /2 );
T.SetLocation(LocalOverlapLocation);
TargetMesh = UGeometryScriptLibrary_MeshPrimitiveFunctions::AppendBox(
ComputeMesh,
Options,
T,
OverlapExtentVector.X,
OverlapExtentVector.Y,
OverlapExtentVector.Z
);
}
}
return TargetMesh;
}
스폰 로직 |
블록을 자르는 기능은 다이나믹 매쉬를 사용했습니다. 처음에는 Apply Mesh Boolean의 Substract로 보이지 않는 벽과 부딪치면 해당 면을 자르게끔 했습니다. 그러다보니 바닥면을 둘러쌓고 있는 보이지 않는 벽을 모두 배치해야 했습니다.
그런데 생각을 해보니 차라리 바닥면을 포함하는 부분만 살려두고 나머지를 자르면 되는걸 깨닫고는 Apply Mesh Boolean을 Intersection으로 바꾸고 바닥면에 수직인 보이지 않는 벽 1개를 만들었습니다.
보이지 않는 벽 배치 |
Substract | Intersection |
충돌 감지는 블록이 적절히 쌓이고 무너지지 않도록 하는 데 중요한 역할을 합니다. 다음과 같은 절차로 구현했습니다:
스포너에서 쌓을 수 있는 블록인지 관리 | 블록에서 액터 태그로 충돌 검출 |
초기에는 블루프린트로 많은 로직을 구현했으나, 성능 이슈로 인해 C++로 전환했습니다. 이 과정에서 블루프린트와 C++ 간의 호환성 문제를 겪었고, 이를 해결하기 위해 다음과 같은 방법을 사용했습니다:
UFUNCTION(BlueprintCallable)
void UpdateScore(int32 NewScore);
충돌 감지 로직이 복잡해지면서 성능 저하가 발생했습니다. 이를 해결하기 위해 다음과 같은 최적화 기법을 적용했습니다:
이번 프로젝트를 통해 블록 쌓기 게임의 개발 과정, 다이나믹 메시의 사용법, AABB, 이벤트 디스패처등을 학습할 수 있었습니다. 블루프린트와 C++의 효율적인 사용과 충돌 감지 로직의 최적화가 큰 도움이 되었습니다.
이 글이 여러분의 프로젝트에 도움이 되길 바랍니다. 추가적인 질문이나 수정이 필요하다면 언제든지 알려주세요!