액터 (Actor) 클래스 이해와 구현
언리얼 엔진에서 액터(Actor)는 게임 월드에 배치하거나 스폰할 수 있는 모든 객체의 기본 클래스입니다.
이 절에서는 AActor 클래스의 특징, 생명주기, 구현 방법 등을 살펴보겠습니다.
AActor 클래스의 역할과 특징
AActor는 다음과 같은 주요 특징을 가집니다.
- 월드 내 위치, 회전, 스케일 정보 보유
- 틱(Tick) 기능을 통한 프레임마다의 업데이트
- 리플리케이션을 통한 네트워크 동기화
- 컴포넌트를 포함할 수 있는 컨테이너 역할
- 블루프린트로 확장 가능
액터의 생명주기
- 스폰(Spawning) : 월드에 생성됨
- 초기화(Initialization) : BeginPlay 호출
- 틱(Ticking) : 매 프레임마다 Tick 함수 호출
- 파괴(Destruction) : EndPlay 호출 후 메모리에서 제거
C++에서 커스텀 액터 클래스 생성 및 구현
기본 클래스 선언
// MyActor.h
##pragma once
##include "CoreMinimal.h"
##include "GameFramework/Actor.h"
##include "MyActor.generated.h"
UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="MyActor")
float MyFloat;
UFUNCTION(BlueprintCallable, Category="MyActor")
void MyFunction();
};
클래스 구현
// MyActor.cpp
##include "MyActor.h"
AMyActor::AMyActor()
{
PrimaryActorTick.bCanEverTick = true;
MyFloat = 0.0f;
}
void AMyActor::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Log, TEXT("MyActor BeginPlay called"));
}
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
MyFloat += DeltaTime;
}
void AMyActor::MyFunction()
{
UE_LOG(LogTemp, Log, TEXT("MyFloat value: %f"), MyFloat);
}
주요 가상 함수 오버라이딩
BeginPlay()
: 액터가 게임에 배치되거나 스폰될 때 호출Tick(float DeltaTime)
: 매 프레임마다 호출EndPlay(const EEndPlayReason::Type EndPlayReason)
: 액터가 파괴될 때 호출
액터의 속성과 동작 정의
- UPROPERTY 매크로를 사용하여 에디터에서 편집 가능한 속성 정의
- UFUNCTION 매크로를 사용하여 블루프린트에서 호출 가능한 함수 정의
- 컴포넌트 추가로 기능 확장
UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
UStaticMeshComponent* MeshComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="MyActor")
float Damage;
UFUNCTION(BlueprintCallable, Category="MyActor")
void ApplyDamage(AActor* OtherActor);
};
AMyActor::AMyActor()
{
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
RootComponent = MeshComponent;
Damage = 10.0f;
}
void AMyActor::ApplyDamage(AActor* OtherActor)
{
// 데미지 적용 로직
}
액터 간 상호작용 구현
액터 간 상호작용은 주로 다음과 같은 방법으로 구현합니다.
- 직접 참조
- 인터페이스 사용
- 이벤트 시스템 활용
예시 (인터페이스 사용)
// IDamageable.h
##pragma once
##include "CoreMinimal.h"
##include "UObject/Interface.h"
##include "IDamageable.generated.h"
UINTERFACE(MinimalAPI)
class UDamageable : public UInterface
{
GENERATED_BODY()
};
class MYPROJECT_API IDamageable
{
GENERATED_BODY()
public:
virtual void TakeDamage(float DamageAmount) = 0;
};
// MyDamageableActor.h
##include "IDamageable.h"
UCLASS()
class MYPROJECT_API AMyDamageableActor : public AActor, public IDamageable
{
GENERATED_BODY()
public:
virtual void TakeDamage(float DamageAmount) override;
};
// MyDamageableActor.cpp
void AMyDamageableActor::TakeDamage(float DamageAmount)
{
// 데미지 처리 로직
}
레벨에 액터 배치 및 동적 스폰
1. 레벨에 직접 배치
- 언리얼 에디터에서 액터를 월드에 드래그 앤 드롭
2. 런타임에 동적 스폰
AMyActor* SpawnedActor = GetWorld()->SpawnActor<AMyActor>(AMyActor::StaticClass(), Location, Rotation);
3. 액터 제거
Actor->Destroy();
성능 고려사항 및 Best Practices
1. Tick 함수 최적화
- 필요한 경우에만 Tick 활성화
PrimaryActorTick.bCanEverTick = false;
- Tick 간격 조절
PrimaryActorTick.TickInterval = 0.1f;
2. 컴포넌트 사용
- 기능을 모듈화하고 재사용성을 높이기 위해 컴포넌트 활용
3. 메모리 관리
- 불필요한 액터는 제때 제거
- 객체 풀링 기법 고려
4. 네트워크 최적화
- 필요한 속성만 리플리케이션 설정
UPROPERTY(Replicated)
float ReplicatedValue;
5. 레벨 스트리밍 고려
- 대규모 월드의 경우 레벨 스트리밍을 통해 메모리 사용 최적화
6. 인터페이스 활용
- 느슨한 결합을 위해 인터페이스 사용
7. 에디터 작업 최적화
- 에디터에서의 작업을 용이하게 하는 속성과 함수 제공
UPROPERTY(EditAnywhere, Category="Debug")
bool bShowDebugInfo;
UFUNCTION(CallInEditor, Category="Debug")
void ToggleDebugInfo();
8. 액터 초기화 최적화
- 생성자에서 필수적인 초기화만 수행
- BeginPlay에서 게임 시작 시 필요한 초기화 수행
9. 상속 구조 설계
- 공통 기능을 기본 클래스로 추출하여 코드 중복 최소화
10. 문서화
- UPROPERTY와 UFUNCTION에 메타데이터를 활용한 문서화
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stats", meta=(ClampMin="0.0", ClampMax="100.0", ToolTip="Health value between 0 and 100"))
float Health;
액터 클래스는 언리얼 엔진 게임플레이의 기본 구성 요소입니다.
적절한 설계와 구현을 통해 효율적이고 확장 가능한 게임 시스템을 구축할 수 있습니다.
액터의 생명주기를 이해하고, 필요에 따라 적절한 가상 함수를 오버라이딩하며, 컴포넌트와 인터페이스를 활용하여 모듈화된 설계를 추구하는 것이 중요합니다.