간단한 폰(Pawn) 및 캐릭터(Character) 클래스 구현
언리얼 엔진에서 폰(Pawn)과 캐릭터(Character)는 플레이어나 AI가 제어할 수 있는 게임 내 객체를 나타냅니다.
이 절에서는 이들의 개념과 C++에서의 구현 방법을 살펴보겠습니다.
APawn과 ACharacter 클래스의 차이점
1. APawn
- 기본적인 이동 가능 객체
- 물리 기반 이동이나 비표준 이동에 적합
- 예 : 차량, 비행체, 로봇 등
2. ACharacter
- APawn을 상속받아 확장한 클래스
- 기본적인 이족 보행 기능 포함
- CharacterMovementComponent 내장
- 예 : 플레이어 캐릭터, 인간형 NPC 등
기본 폰 클래스 구현
폰 클래스 선언
// MyPawn.h
##pragma once
##include "CoreMinimal.h"
##include "GameFramework/Pawn.h"
##include "MyPawn.generated.h"
UCLASS()
class MYPROJECT_API AMyPawn : public APawn
{
GENERATED_BODY()
public:
AMyPawn();
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
UStaticMeshComponent* MeshComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
UFloatingPawnMovement* MovementComponent;
private:
void MoveForward(float Value);
void MoveRight(float Value);
};
폰 클래스 구현
// MyPawn.cpp
##include "MyPawn.h"
##include "Components/StaticMeshComponent.h"
##include "GameFramework/FloatingPawnMovement.h"
AMyPawn::AMyPawn()
{
PrimaryActorTick.bCanEverTick = true;
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
SetRootComponent(MeshComponent);
MovementComponent = CreateDefaultSubobject<UFloatingPawnMovement>(TEXT("MovementComponent"));
}
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &AMyPawn::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AMyPawn::MoveRight);
}
void AMyPawn::MoveForward(float Value)
{
if (Value != 0.0f)
{
AddMovementInput(GetActorForwardVector(), Value);
}
}
void AMyPawn::MoveRight(float Value)
{
if (Value != 0.0f)
{
AddMovementInput(GetActorRightVector(), Value);
}
}
캐릭터 클래스 구현
캐릭터 클래스 선언
// MyCharacter.h
##pragma once
##include "CoreMinimal.h"
##include "GameFramework/Character.h"
##include "MyCharacter.generated.h"
UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
class UCameraComponent* CameraComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera")
class USpringArmComponent* SpringArmComponent;
private:
void MoveForward(float Value);
void MoveRight(float Value);
void Turn(float Value);
void LookUp(float Value);
void StartJump();
void StopJump();
};
캐릭터 클래스 구현
// MyCharacter.cpp
##include "MyCharacter.h"
##include "Camera/CameraComponent.h"
##include "GameFramework/SpringArmComponent.h"
##include "GameFramework/CharacterMovementComponent.h"
AMyCharacter::AMyCharacter()
{
PrimaryActorTick.bCanEverTick = true;
SpringArmComponent = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComponent"));
SpringArmComponent->SetupAttachment(RootComponent);
SpringArmComponent->TargetArmLength = 300.0f;
SpringArmComponent->bUsePawnControlRotation = true;
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
CameraComponent->SetupAttachment(SpringArmComponent);
GetCharacterMovement()->JumpZVelocity = 400.0f;
GetCharacterMovement()->AirControl = 0.2f;
}
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &AMyCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AMyCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &AMyCharacter::Turn);
PlayerInputComponent->BindAxis("LookUp", this, &AMyCharacter::LookUp);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMyCharacter::StartJump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &AMyCharacter::StopJump);
}
void AMyCharacter::MoveForward(float Value)
{
if (Value != 0.0f)
{
AddMovementInput(GetActorForwardVector(), Value);
}
}
void AMyCharacter::MoveRight(float Value)
{
if (Value != 0.0f)
{
AddMovementInput(GetActorRightVector(), Value);
}
}
void AMyCharacter::Turn(float Value)
{
AddControllerYawInput(Value);
}
void AMyCharacter::LookUp(float Value)
{
AddControllerPitchInput(Value);
}
void AMyCharacter::StartJump()
{
Jump();
}
void AMyCharacter::StopJump()
{
StopJumping();
}
AIController 연동
AIController를 폰이나 캐릭터와 연동하려면
- AIController 클래스 생성
- 폰/캐릭터의 AIControllerClass 속성 설정
- Possess 함수에서 AI 로직 초기화
예시
// MyAIController.h
UCLASS()
class MYPROJECT_API AMyAIController : public AAIController
{
GENERATED_BODY()
public:
virtual void OnPossess(APawn* InPawn) override;
};
// MyAIController.cpp
void AMyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// AI 로직 초기화
}
// 폰/캐릭터 클래스에서
AMyCharacter::AMyCharacter()
{
// ...
AIControllerClass = AMyAIController::StaticClass();
}
애니메이션 기초 설정
- AnimInstance 클래스 생성
- 캐릭터의 Mesh 컴포넌트에 AnimInstance 설정
// MyAnimInstance.h
UCLASS()
class MYPROJECT_API UMyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float Speed;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsInAir;
};
// MyAnimInstance.cpp
void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
APawn* Pawn = TryGetPawnOwner();
if (Pawn)
{
Speed = Pawn->GetVelocity().Size();
bIsInAir = Pawn->GetMovementComponent()->IsFalling();
}
}
// 캐릭터 클래스에서
AMyCharacter::AMyCharacter()
{
// ...
GetMesh()->SetAnimInstanceClass(UMyAnimInstance::StaticClass());
}
네트워크 멀티플레이어 준비
- 리플리케이션 설정
- RPC (Remote Procedure Calls) 구현
// MyCharacter.h
UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
// ...
UFUNCTION(Server, Reliable, WithValidation)
void ServerPerformAction();
};
// MyCharacter.cpp
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyCharacter, SomeReplicatedVariable);
}
bool AMyCharacter::ServerPerformAction_Validate()
{
return true;
}
void AMyCharacter::ServerPerformAction_Implementation()
{
// 서버에서 실행될 로직
}
성능 고려사항 및 Best Practices
1. Tick 함수 최적화
- 불필요한 경우 비활성화
PrimaryActorTick.bCanEverTick = false;
2. 네트워크 트래픽 최적화
- 필요한 정보만 리플리케이션
- 업데이트 빈도 조절
3. LOD (Level of Detail) 시스템 활용
- 거리에 따른 메시 및 애니메이션 복잡도 조절
4. 물리 시뮬레이션 최적화
- 필요한 경우에만 물리 시뮬레이션 활성화
5. 메모리 관리
- 동적 할당 최소화
- 객체 풀링 기법 고려
6. 코드 구조화
- 기능별 컴포넌트 분리
- 인터페이스 활용으로 확장성 확보
7. 입력 처리 최적화
- 불필요한 입력 체크 제거
- 입력에 따른 상태 관리 효율화
8. 애니메이션 시스템 효율적 사용
- 블렌드 스페이스 및 애님 몽타주 활용
- 애니메이션 블루프린트 최적화
폰과 캐릭터 클래스는 게임플레이의 핵심 요소입니다. 적절한 설계와 최적화를 통해 반응성 좋고 효율적인 게임 캐릭터를 구현할 수 있습니다.
기본적인 이동과 상호작용부터 시작하여 애니메이션, AI, 네트워크 기능을 점진적으로 추가해 나가는 것이 좋습니다.