[Day21] Random Walk Simulation(2)

베리투스·2025년 9월 2일

TIL: Today I Learned

목록 보기
29/93

언리얼 엔진에서 액터의 랜덤 워크시각화를 구현하는 방법을 학습했습니다. ✨AActor를 상속받아 메시 컴포넌트를 추가하고, 월드 공간에서 액터를 이동시키며, 디버그 드로우동적 머티리얼을 사용하여 이동 경로와 이벤트를 시각적으로 표현합니다. 🚀


📌 목표

  • AActorUStaticMeshComponent를 추가하여 시각적 표현 구현
  • 액터의 랜덤 워크를 월드 공간에서 시각적으로 이동 및 확인
  • 특정 조건(ShouldTriggerEvent())에 따라 액터의 머티리얼 색상 변경으로 이벤트 시각화
  • DrawDebugSphere를 사용하여 액터의 이동 경로를 에디터에서 추적

💻 코드

MyActor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h" // AActor를 상속받아 기본적인 액터 기능 사용
#include "Components/StaticMeshComponent.h" // 액터에 메시를 추가하여 시각적으로 표현하기 위한 컴포넌트
#include "MyActor.generated.h"

class UMaterialInstanceDynamic; // 동적 머티리얼 인스턴스를 전방 선언 (헤더에서 직접 포함하지 않고 사용)

UCLASS()
class HOMEWORK5_API AMyActor : public AActor // APawn 대신 AActor를 상속받음
{
	GENERATED_BODY()

public:
	// 생성자: 액터가 생성될 때 초기 설정을 담당
	AMyActor();

protected:
	// 게임 시작 시 호출되는 함수 (초기화 로직에 적합)
	virtual void BeginPlay() override;

public:
	// 매 프레임마다 호출되는 함수 (이번 예제에서는 직접 이동 처리 안 함)
	virtual void Tick(float DeltaTime) override;

private:
	// 메시 컴포넌트: 액터의 시각적 표현을 담당하는 스태틱 메시 (VisibleAnywhere로 에디터에서 확인 가능)
	UPROPERTY(VisibleAnywhere, Category = "Components")
	UStaticMeshComponent* MeshComponent;

	// 동적 머티리얼 인스턴스: 런타임에 머티리얼의 파라미터를 변경하기 위해 사용
	UPROPERTY()
	UMaterialInstanceDynamic* DynamicMaterialInstance;

	// 기본 머티리얼: 액터의 기본 외형 (에디터에서 설정 가능)
	UPROPERTY(EditAnywhere, Category = "Materials")
	UMaterialInterface* DefaultMaterial;
	// 이벤트 발생 시 적용될 머티리얼 (선택 사항, 여기서는 동적 머티리얼 색상 변경으로 대체)
	UPROPERTY(EditAnywhere, Category = "Materials")
	UMaterialInterface* EventTriggeredMaterial;

	// 이동량을 결정하는 함수 (0 또는 1 반환)
	int32 Step();
	// 이벤트 발생 여부를 결정하는 함수 (50% 확률로 true 반환)
	bool ShouldTriggerEvent();
	// 액터의 월드 위치를 이동시키고 시각화하는 함수
	void MoveActor();

	// 액터의 현재 월드 위치를 저장하는 변수
	FVector CurrentWorldPosition;

	// 총 이동 횟수를 정의하는 상수
	const int32 MaxSteps = 10;

	// 랜덤 워크의 각 스텝을 처리하기 위한 타이머 핸들
	FTimerHandle RandomWalkTimerHandle;
	// 타이머에 의해 주기적으로 호출될 함수
	void PerformRandomWalkStep();
	// 현재 진행 중인 스텝의 인덱스
	int32 CurrentStepIndex = 0;
};

MyActor.cpp

#include "MyActor.h"
#include "Math/UnrealMathUtility.h" // 수학 유틸리티 함수 (랜덤 값 생성 등)
#include "Engine/Engine.h" // UE_LOG 등 엔진 관련 기능
#include "DrawDebugHelpers.h" // 디버그 드로우 기능 (DrawDebugSphere 등)
#include "Materials/MaterialInstanceDynamic.h" // 동적 머티리얼 사용
#include "Kismet/GameplayStatics.h" // GetWorldTimerManager() 접근을 위해 포함

// 생성자: 액터가 월드에 스폰될 때 한 번 호출되어 컴포넌트 설정 및 초기화
AMyActor::AMyActor()
{
	// 액터의 Tick 함수가 매 프레임 호출되도록 설정 (이번 예제에서는 타이머로 이동 처리)
	PrimaryActorTick.bCanEverTick = true;

	// 메시 컴포넌트 생성 및 액터의 루트 컴포넌트로 설정
	MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
	RootComponent = MeshComponent;

	// 기본 메시 할당: 언리얼 엔진 StarterContent의 큐브 메시 사용
	static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeMeshAsset(TEXT("/Game/StarterContent/Shapes/Shape_Cube.Shape_Cube"));
	if (CubeMeshAsset.Succeeded())
	{
		MeshComponent->SetStaticMesh(CubeMeshAsset.Object);
	}

	// 기본 머티리얼 할당: 언리얼 엔진 StarterContent의 기본 벽 머티리얼 사용
	static ConstructorHelpers::FObjectFinder<UMaterial> BaseMaterialAsset(TEXT("/Game/StarterContent/Materials/M_Basic_Wall.M_Basic_Wall"));
	if (BaseMaterialAsset.Succeeded())
	{
		DefaultMaterial = BaseMaterialAsset.Object;
		MeshComponent->SetMaterial(0, DefaultMaterial); // 메시의 첫 번째 슬롯에 머티리얼 설정
	}
}

// BeginPlay: 게임이 시작되거나 액터가 스폰된 후 초기화 로직
void AMyActor::BeginPlay()
{
	Super::BeginPlay();

	// 액터의 현재 월드 위치를 CurrentWorldPosition에 저장 (초기 위치)
	CurrentWorldPosition = GetActorLocation();

	// 동적 머티리얼 인스턴스 생성 및 할당: 런타임에 머티리얼 파라미터(색상)를 변경하기 위함
	if (MeshComponent && DefaultMaterial)
	{
		DynamicMaterialInstance = UMaterialInstanceDynamic::Create(DefaultMaterial, this);
		MeshComponent->SetMaterial(0, DynamicMaterialInstance);
	}

	// BeginPlay에서 랜덤 워크 시작: 0.5초마다 PerformRandomWalkStep 함수를 반복 호출하도록 타이머 설정
	GetWorldTimerManager().SetTimer(RandomWalkTimerHandle, this, &AMyActor::PerformRandomWalkStep, 0.5f, true);
}

// Tick: 매 프레임마다 호출되지만, 이 예제에서는 이동 로직을 타이머로 분리하여 사용하지 않음
void AMyActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

// Step: 이동할 방향의 크기를 결정 (현재는 항상 0 또는 1)
int32 AMyActor::Step()
{
	return FMath::RandRange(0, 1);
}

// ShouldTriggerEvent: 50% 확률로 true를 반환하여 이벤트 발생 여부 결정
bool AMyActor::ShouldTriggerEvent()
{
	return FMath::RandBool();
}

// MoveActor: 액터를 실제 월드 공간에서 이동시키고, 이동 경로 및 이벤트를 시각화
void AMyActor::MoveActor()
{
	FVector PreviousPosition = CurrentWorldPosition; // 이동 전 위치 저장

	// X, Y축으로 무작위 이동량 결정 (-1, 0, 1 중 하나)
	int32 dx = FMath::RandRange(-1, 1);
	int32 dy = FMath::RandRange(-1, 1);
	
	// dx와 dy가 모두 0인 경우 (액터가 움직이지 않는 경우)를 방지하기 위해 최소한 한 축은 움직이도록 보장
	if (dx == 0 && dy == 0)
	{
		if (FMath::RandBool()) dx = FMath::RandBool() ? 1 : -1; // X축을 강제로 움직이거나
		else dy = FMath::RandBool() ? 1 : -1; // Y축을 강제로 움직임
	}

	// CurrentWorldPosition을 업데이트 (각 스텝마다 100 Unreal Unit 이동)
	CurrentWorldPosition.X += dx * 100.0f;
	CurrentWorldPosition.Y += dy * 100.0f;

	SetActorLocation(CurrentWorldPosition); // 액터를 새로운 월드 위치로 이동

	// 이동 경로를 디버그 스피어로 시각화 (빨간색, 5초 동안 유지)
	DrawDebugSphere(GetWorld(), CurrentWorldPosition, 25.0f, 12, FColor::Red, false, 5.0f);

	// 이벤트 발생 여부 확인 및 머티리얼 색상 변경으로 시각화
	bool bEventTriggered = ShouldTriggerEvent();
	if (bEventTriggered)
	{
		// 이벤트 발생 시 액터의 색상을 빨간색으로 변경
		if (DynamicMaterialInstance) DynamicMaterialInstance->SetVectorParameterValue(TEXT("BaseColor"), FLinearColor::Red);
		UE_LOG(LogTemp, Warning, TEXT("Event Triggered at (%s)! Actor color changed to Red."), *CurrentWorldPosition.ToString());
	}
	else
	{
		// 이벤트 미발생 시 액터의 색상을 파란색으로 변경
		if (DynamicMaterialInstance) DynamicMaterialInstance->SetVectorParameterValue(TEXT("BaseColor"), FLinearColor::Blue);
		UE_LOG(LogTemp, Log, TEXT("No Event. Actor color changed to Blue."));
	}

	// 이동 로그 출력: 현재 스텝, 이전 위치, 현재 위치, 이벤트 발생 여부
	UE_LOG(LogTemp, Log, TEXT("Step %d: From (%s) to (%s). Event: %s"),
		CurrentStepIndex, *PreviousPosition.ToString(), *CurrentWorldPosition.ToString(), bEventTriggered ? TEXT("Yes") : TEXT("No"));
}

// PerformRandomWalkStep: 타이머에 의해 주기적으로 호출되어 랜덤 워크의 한 스텝을 수행
void AMyActor::PerformRandomWalkStep()
{
	// MaxSteps 횟수만큼 이동을 수행
	if (CurrentStepIndex < MaxSteps)
	{
		CurrentStepIndex++; // 스텝 인덱스 증가
		MoveActor(); // 액터 이동 및 시각화 함수 호출
	}
	else
	{
		// 모든 스텝이 완료되면 타이머를 중지
		GetWorldTimerManager().ClearTimer(RandomWalkTimerHandle);
		UE_LOG(LogTemp, Warning, TEXT("=== Movement Summary ==="));
		UE_LOG(LogTemp, Warning, TEXT("Random walk completed after %d steps."), MaxSteps);
	}
}

⚠️ 실수

  • UMaterialInstanceDynamic의 늪과 초기화 순서 🌊
    생성자에서 기본 Material을 할당하고 BeginPlay에서 UMaterialInstanceDynamic을 생성해 다시 할당했는데, BeginPlay 시점에 DefaultMaterial이 로드되지 않은 경우를 간과했다. 에디터에서는 괜찮았지만 빌드 후 크래시가 발생할 수 있다는 것을 깨달았다. ConstructorHelpers::FObjectFinder는 생성자에서만 안전하게 사용해야 하고, 런타임 리소스는 FSoftObjectPathFStreamableManager 같은 비동기 로딩 방식을 고려해야 한다는 점을 다시 한번 상기했다. 초기화 순서는 정말 중요했다. 💡

  • 랜덤 워크의 편향성 문제 🎲
    dx, dyRandRange(-1, 1)로 설정하고 둘 다 0일 때만 보정했지만, 여전히 특정 방향으로 쏠리는 경향이 보일 수 있다는 것을 느꼈다. 대각선 이동 확률이 더 높다거나 하는 식이었다. 정말 완벽한 랜덤 워크를 구현하려면 균등 분포를 보장하는 더 정교한 알고리즘이나, 각 방향으로의 이동 확률을 명시적으로 제어하는 방법을 고민해야 한다는 것을 알았다. 단순히 RandRange만으로는 부족할 수 있다. 🧠


✅ 핵심 요약

개념설명비고
AActor언리얼 엔진의 가장 기본적인 게임 오브젝트. 월드에 배치되고 동작 가능.APawn보다 가볍고 일반적인 오브젝트에 적합.
UStaticMeshComponent액터에 3D 메시를 추가하여 시각적으로 표현하는 컴포넌트.CreateDefaultSubobject로 생성, SetStaticMesh로 메시 할당.
UMaterialInstanceDynamic런타임에 머티리얼의 파라미터(예: 색상)를 동적으로 변경할 수 있는 인스턴스.Create 함수로 생성 후 SetVectorParameterValue 등으로 파라미터 변경.
SetActorLocation액터의 월드 공간 위치를 설정하는 함수.액터를 물리적으로 이동시키는 핵심 함수.
DrawDebugSphere언리얼 에디터에서 디버깅 목적으로 구체를 그리는 함수.GetWorld()를 통해 월드에 접근하여 그릴 수 있음.
FTimerHandle특정 함수를 주기적으로 또는 한 번만 호출하도록 스케줄링하는 타이머 시스템.GetWorldTimerManager().SetTimer로 설정, ClearTimer로 중지.
profile
Shin Ji Yong // Unreal Engine 5 공부중입니다~

0개의 댓글