언리얼 엔진에서 액터의 랜덤 워크와 시각화를 구현하는 방법을 학습했습니다. ✨AActor를 상속받아 메시 컴포넌트를 추가하고, 월드 공간에서 액터를 이동시키며, 디버그 드로우와 동적 머티리얼을 사용하여 이동 경로와 이벤트를 시각적으로 표현합니다. 🚀
AActor에 UStaticMeshComponent를 추가하여 시각적 표현 구현ShouldTriggerEvent())에 따라 액터의 머티리얼 색상 변경으로 이벤트 시각화DrawDebugSphere를 사용하여 액터의 이동 경로를 에디터에서 추적#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;
};
#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는 생성자에서만 안전하게 사용해야 하고, 런타임 리소스는 FSoftObjectPath나 FStreamableManager 같은 비동기 로딩 방식을 고려해야 한다는 점을 다시 한번 상기했다. 초기화 순서는 정말 중요했다. 💡
랜덤 워크의 편향성 문제 🎲
dx, dy를 RandRange(-1, 1)로 설정하고 둘 다 0일 때만 보정했지만, 여전히 특정 방향으로 쏠리는 경향이 보일 수 있다는 것을 느꼈다. 대각선 이동 확률이 더 높다거나 하는 식이었다. 정말 완벽한 랜덤 워크를 구현하려면 균등 분포를 보장하는 더 정교한 알고리즘이나, 각 방향으로의 이동 확률을 명시적으로 제어하는 방법을 고민해야 한다는 것을 알았다. 단순히 RandRange만으로는 부족할 수 있다. 🧠
| 개념 | 설명 | 비고 |
|---|---|---|
AActor | 언리얼 엔진의 가장 기본적인 게임 오브젝트. 월드에 배치되고 동작 가능. | APawn보다 가볍고 일반적인 오브젝트에 적합. |
UStaticMeshComponent | 액터에 3D 메시를 추가하여 시각적으로 표현하는 컴포넌트. | CreateDefaultSubobject로 생성, SetStaticMesh로 메시 할당. |
UMaterialInstanceDynamic | 런타임에 머티리얼의 파라미터(예: 색상)를 동적으로 변경할 수 있는 인스턴스. | Create 함수로 생성 후 SetVectorParameterValue 등으로 파라미터 변경. |
SetActorLocation | 액터의 월드 공간 위치를 설정하는 함수. | 액터를 물리적으로 이동시키는 핵심 함수. |
DrawDebugSphere | 언리얼 에디터에서 디버깅 목적으로 구체를 그리는 함수. | GetWorld()를 통해 월드에 접근하여 그릴 수 있음. |
FTimerHandle | 특정 함수를 주기적으로 또는 한 번만 호출하도록 스케줄링하는 타이머 시스템. | GetWorldTimerManager().SetTimer로 설정, ClearTimer로 중지. |