[UE5_C++]Spline 이용한 메시 반복배치 만들기

이진혁·2024년 5월 13일
0

언리얼 공부정리

목록 보기
4/6
post-thumbnail

SplineComponent(스플라인이라고 하겠다)와 InstancedStaticMesh를 이용하여 스플라인을 따라 지정된 메시를 정해진 수만큼 배치해주는 기능을 만들어보겠다.


시연영상


ActorArrangementSpline.h

#pragma once

//'EngineMinimal.h`와 같은 언리얼 기본 세팅을 담아둔 헤더파일
#include "../GameInfo.h"
#include "GameFramework/Actor.h"
#include "Components/SplineComponent.h"
#include "Components/SplineMeshComponent.h"
#include "ActorArrangementSpline.generated.h"

//배치될 메시의 정보를 저장하는 구조체
USTRUCT(BlueprintType)
struct FSplineMesh
{
	GENERATED_USTRUCT_BODY()

public :
//배치될 메시
	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UStaticMesh> mMesh;

//배치될 메시의 수
	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	int mMeshCount;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	FRotator mMeshRotation;

//메시간의 거리
	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	float mDistance;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UInstancedStaticMeshComponent> mInstanceMesh;
};


UCLASS()
class MODULETEST_API AActorArrangementSpline : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AActorArrangementSpline();

protected :
	TObjectPtr<USceneComponent> mScene;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	TArray<FSplineMesh> mSplineMeshInfo;

	//전체 InstancedMesh를 반복 배치할 횟수
	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	int32 mArrangementCount = 1;

	//스플라인 컴포넌트
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	class USplineComponent* mSplinePath;

	//배치된 메시들이 스플라인의 곡선에 따라 회전할지 여부
	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	bool mIsCurved;

	//mIsCurved가 true일 시 회전량
	FRotator mCurvedRot;

	//현재 메시가 스플라인 상에서 얼마만큼의 거리에 배치되어야 하는지를 저장
	float mNowArrangementDistance;

	//현재 배치할 메시의 X,Y,Z길이의 최대값을 저장
	FVector mMeshBound;

	//현재 메시와 다음 메시간의 거리
	float mNextDistance;

protected:

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	virtual void OnConstruction(const FTransform& Transform);
};

InstancedStaticMeshComponent를 이용해 mMesh에 설정된 메시를 mMeshCount값만큼 반복 배치한다.

InstancedStaticMeshComponent

메시를 반복 배치하는데 사용되는 컴포넌트이다. 각 컴포넌트는 Transform값을 인덱스로 가지는 인스턴스를 가지고 있으며, 인덱스를 추가하면 세팅된 메시를 각 인덱스의 Transform마다 배치해준다.

InstancedStaticMeshComponent는 한 종류의 메시만을 반복 배치할 수 있기 때문에 여러 종류의 메시를 배치하기 위해 배치에 필요한 값과 InstancedStaticMeshComponent를 구조체로 구성하였다. 주의할 점은 InstancedStaticMeshComponent는 Ctrl+Z를 통해 되돌리기를 할 경우 이전의 메시가 남는 문제가 발생할 수 있으므로 꼭 UPROPERTY 매크로를 붙여주어야한다.
에디터 상에서는 아래와 같이 표기된다.

ActorArrangementSpline.cpp

#include "ActorArrangementSpline.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMathLibrary.h"

// Sets default values
AActorArrangementSpline::AActorArrangementSpline()
{
	mScene = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
	mScene->bVisualizeComponent = true;

	mSplinePath = CreateDefaultSubobject<USplineComponent>(TEXT("SplinePath"));
	mSplinePath->SetupAttachment(mScene);

	SetRootComponent(mScene);
}

// Called when the game starts or when spawned
void AActorArrangementSpline::BeginPlay()
{
	Super::BeginPlay();
}

void AActorArrangementSpline::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

	mNowArrangementDistance = 0.f;

	//인스턴스 메시 초기화 및 스태틱메시 수만큼 추가생성
	for (auto& Mesh : mSplineMeshInfo)
	{
		if (IsValid(Mesh.mInstanceMesh))
		{
			//액터 이동 시 메시의 복사를 방지하기 위해 인스턴스를 비워주어야함
			Mesh.mInstanceMesh->ClearInstances();
		}

		else
		{
			//OnConstruction에서 컴포넌트를 추가하기 위해 NewObject 사용
			Mesh.mInstanceMesh = NewObject<UInstancedStaticMeshComponent>(
				this,
				UInstancedStaticMeshComponent::StaticClass()
			);

			Mesh.mInstanceMesh->SetupAttachment(RootComponent);
		}
	}

	
	for (int i = 0; i < mArrangementCount; i++)
	{
		for (auto& Mesh : mSplineMeshInfo)
		{
			if (IsValid(Mesh.mMesh))
			{
				Mesh.mInstanceMesh->SetStaticMesh(Mesh.mMesh);

				//mMeshBound : 배치된 메시의 x,y,z 최대 길이를 벡터값으로 저장
				//mNextDistance : 다음에 배치될 메시와의 거리
				mMeshBound = Mesh.mMesh->GetBoundingBox().Max - Mesh.mMesh->GetBoundingBox().Min;
				mNextDistance = fmax(mMeshBound.X, mMeshBound.Y) + Mesh.mDistance;

				for (int j = 0; j < Mesh.mMeshCount; j++)
				{
					if (mIsCurved)
					{
						//mIsCurved가 true일 때 회전값을 계산
						mCurvedRot = UKismetMathLibrary::MakeRotFromX(
							mSplinePath->GetLocationAtDistanceAlongSpline(
								mNowArrangementDistance + mNextDistance,
								ESplineCoordinateSpace::Local
							) - 
							mSplinePath->GetLocationAtDistanceAlongSpline(
								mNowArrangementDistance, 
								ESplineCoordinateSpace::Local
							) 
						);
					}

					else if (mCurvedRot != FRotator::ZeroRotator)
					{
						mCurvedRot = FRotator::ZeroRotator;
					}

					//instancedStaticMeshComponent에 mMeshCount의 개수만큼 인스턴스 추가
					//GetLocationAtDistanceAlongSpline : 스플라인 상에서 원점을 기준으로 Distance값만큼 떨어진 위치의 좌표를 가져옴
					Mesh.mInstanceMesh->AddInstance(
						FTransform(
							Mesh.mMeshRotation + mCurvedRot,
							mSplinePath->GetLocationAtDistanceAlongSpline(mNowArrangementDistance, ESplineCoordinateSpace::Local),
							FVector(1.f, 1.f, 1.f)
						)
					);

					mNowArrangementDistance += mNextDistance;
				}
			}
		}
	}
}

액터를 이동시킬 때마다 배치될 메시의 transform값을 수정하기 위해 OnConstruction에서 배치를 구현해준다.
mSplineMeshInfo 구조체에 인덱스가 추가된 경우 InstancedStaticMeshComponent의 인스턴스를 추가로 생성해 주어야 하는데 CreateDefaultSubObject의 경우 생성자에서만 호출이 가능하므로 NewObject를 사용해 인스턴스를 생성해준다. 또한 이미 생성된 InstancedStaticMeshComponent의 경우 이동 시 복제되는 것을 방지하기 위해 ClearInstances()를 호출해준다.

0개의 댓글

관련 채용 정보