오늘은 언리얼 엔진의 Character 클래스가 아닌, 가장 기본 단위인 "Pawn 클래스"를 사용하여 3D 캐릭터를 직접 구현하는 학습을 진행했다. CharacterMovementComponent의 도움 없이, CapsuleComponent, SkeletalMeshComponent 등을 직접 조합하고 3인칭 시점 카메라를 구성했다. 또한, 새로운 입력 시스템인 "Enhanced Input System"을 사용하여 입력을 처리하고, Tick 함수 내에서 AddActorLocalOffset을 호출하여 프레임 독립적인 이동 로직을 직접 작성했다. 이를 통해 언리얼 엔진의 액터와 컴포넌트, 그리고 입력 처리의 기본 원리를 더 깊게 이해할 수 있었다. 🚀
Pawn 클래스의 구조와 역할을 이해한다.Capsule, SkeletalMesh, SpringArm, Camera 컴포넌트를 수동으로 조합하여 3인칭 캐릭터를 구성한다.Enhanced Input System을 사용하여 입력 액션(IA)과 매핑 컨텍스트(IMC)를 설정하고 코드에 바인딩한다.CharacterMovementComponent 없이 Tick 함수와 DeltaTime을 이용해 직접 이동 로직을 구현한다.Pawn vs Character
Pawn: 플레이어나 AI에 의해 "빙의(Possess)"될 수 있는 액터의 최소 단위이다. 그 자체로는 이동 기능이 없어 직접 구현해야 한다.Character: Pawn을 상속받은 클래스로, 사람 형태의 캐릭터를 위해 CharacterMovementComponent가 기본적으로 포함되어 있다. 점프, 걷기, 낙하 등 복잡한 이동 로직이 이미 구현되어 있어 편리하다. 오늘은 이 편리함을 버리고 기본 원리를 배우기 위해 Pawn을 사용했다.컴포넌트 기반 구조 (Component-Based Architecture)
CapsuleComponent, 외형을 위한 SkeletalMeshComponent, 3인칭 카메라를 위한 SpringArmComponent와 CameraComponent를 직접 조립했다.프레임 독립적인(Frame-Independent) 이동
Tick 함수는 매 프레임 호출되는데, 컴퓨터 성능에 따라 호출 간격이 다르다.500.0f * 0.016초 (60fps) 와 500.0f * 0.008초 (120fps)는 결과가 다르다.DeltaTime(이전 프레임과의 시간 간격)을 곱해준다. 이렇게 하면 어떤 프레임 속도에서도 캐릭터가 동일한 속도로 움직이게 된다.#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "MyPawn.generated.h"
class UCapsuleComponent;
class USkeletalMeshComponent;
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;
// Pawn 클래스를 기반으로 3D 캐릭터를 구현하는 클래스
UCLASS()
class HOMEWORK7_API AMyPawn : public APawn
{
GENERATED_BODY()
public:
// 생성자: 컴포넌트 생성 및 기본값 설정
AMyPawn();
protected:
// 게임 시작 시 호출되는 함수
virtual void BeginPlay() override;
public:
// 매 프레임 호출되는 함수
virtual void Tick(float DeltaTime) override;
// 플레이어 입력 컴포넌트 설정 및 액션 바인딩 함수
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
private:
// 컴포넌트 선언부
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UCapsuleComponent> CapsuleComponent; // 충돌을 담당하는 캡슐 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<USkeletalMeshComponent> SkeletalMesh; // 캐릭터의 외형을 담당하는 스켈레탈 메시 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<USpringArmComponent> SpringArm; // 카메라를 캐릭터와 일정 거리를 유지시켜주는 스프링 암 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UCameraComponent> Camera; // 실제 플레이어가 보게 될 시점을 담당하는 카메라 컴포넌트
// 입력 애셋 선언부
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInputMappingContext> DefaultMappingContext; // 기본 입력 매핑 컨텍스트
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInputAction> MoveAction; // 이동 입력을 처리할 Input Action
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInputAction> LookAction; // 둘러보기(카메라 회전) 입력을 처리할 Input Action
// 입력 처리 함수 선언부
// 이동 입력 처리 함수
void Move(const FInputActionValue& Value); // 입력 시스템으로부터 전달된 값 (예: Vector2D)
// 둘러보기 입력 처리 함수
void Look(const FInputActionValue& Value); // 입력 시스템으로부터 전달된 값 (예: Vector2D)
// 이동 관련 변수
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Movement", meta = (AllowPrivateAccess = "true"))
float MoveSpeed = 500.0f; // 캐릭터의 이동 속도
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Movement", meta = (AllowPrivateAccess = "true"))
float LookSpeed = 100.0f; // 카메라 회전 속도 (현재 코드에서는 사용되지 않음)
};
#include "MyPawn.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Kismet/GameplayStatics.h"
AMyPawn::AMyPawn()
{
// 이 Pawn이 매 프레임 Tick 함수를 호출하도록 설정합니다.
PrimaryActorTick.bCanEverTick = true;
// 캡슐 컴포넌트를 생성합니다.
CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComponent"));
// 캡슐 컴포넌트를 이 액터의 루트 컴포넌트로 설정합니다. 모든 다른 컴포넌트는 여기에 부착됩니다.
SetRootComponent(CapsuleComponent);
CapsuleComponent->SetCapsuleHalfHeight(88.0f);
CapsuleComponent->SetCapsuleRadius(34.0f);
// 물리 시뮬레이션을 비활성화합니다. 코드 기반으로 직접 이동을 제어하기 위함입니다.
CapsuleComponent->SetSimulatePhysics(false);
// 스켈레탈 메시 컴포넌트를 생성합니다.
SkeletalMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalMesh"));
// 스켈레탈 메시를 루트 컴포넌트(캡슐)에 부착합니다.
SkeletalMesh->SetupAttachment(RootComponent);
// 캡슐 컴포넌트의 중앙 하단에 메시가 위치하도록 상대 위치와 회전을 조정합니다.
SkeletalMesh->SetRelativeLocation(FVector(0.f, 0.f, -88.0f));
SkeletalMesh->SetRelativeRotation(FRotator(0.f, -90.f, 0.f));
// 물리 시뮬레이션을 비활성화합니다.
SkeletalMesh->SetSimulatePhysics(false);
// 스프링 암 컴포넌트를 생성합니다.
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->TargetArmLength = 300.0f;
// 컨트롤러의 회전(마우스 입력에 의한)을 스프링 암에 적용하여 카메라가 따라 회전하도록 합니다.
SpringArm->bUsePawnControlRotation = true;
// 카메라 컴포넌트를 생성합니다.
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
// 카메라를 스프링 암의 끝에 부착합니다.
Camera->SetupAttachment(SpringArm);
}
// 게임이 시작될 때 Enhanced Input 시스템에 매핑 컨텍스트를 추가합니다.
void AMyPawn::BeginPlay()
{
Super::BeginPlay();
// 유효한 플레이어 컨트롤러를 가져옵니다.
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
// 플레이어 컨트롤러에서 Enhanced Input 서브시스템을 가져옵니다.
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
// 서브시스템에 DefaultMappingContext를 추가하여 정의된 입력들을 활성화합니다.
Subsystem->AddMappingContext(DefaultMappingContext, 0); // 0은 우선순위(Priority)
}
}
}
// 입력 컴포넌트에 Input Action들을 바인딩합니다.
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 입력 컴포넌트를 Enhanced Input Component로 캐스팅합니다.
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
{
// MoveAction이 Triggered 상태일 때, 이 객체의 Move 함수를 호출하도록 바인딩합니다.
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyPawn::Move);
// LookAction이 Triggered 상태일 때, 이 객체의 Look 함수를 호출하도록 바인딩합니다.
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMyPawn::Look);
}
}
// MoveAction 입력에 따라 이동 방향과 값을 저장합니다.
void AMyPawn::Move(const FInputActionValue& Value)
{
// 입력 값을 Vector2D로 가져옵니다 (W/S는 X, A/D는 Y).
const FVector2D MoveVector = Value.Get<FVector2D>();
// 컨트롤러가 유효한지 확인합니다.
if (Controller != nullptr)
{
// Pawn의 현재 앞쪽과 오른쪽 방향 벡터를 가져옵니다.
const FVector ForwardDirection = GetActorForwardVector();
const FVector RightDirection = GetActorRightVector();
// 앞/뒤 방향으로 이동 입력을 추가합니다.
AddMovementInput(ForwardDirection, MoveVector.Y); // 이동할 월드 방향, 스케일 값 (입력 강도)
// 좌/우 방향으로 이동 입력을 추가합니다.
AddMovementInput(RightDirection, MoveVector.X); // 이동할 월드 방향, 스케일 값 (입력 강도)
}
}
// LookAction 입력(마우스 움직임)에 따라 컨트롤러의 회전을 처리합니다.
void AMyPawn::Look(const FInputActionValue& Value)
{
// 입력 값을 Vector2D로 가져옵니다 (마우스 X, Y 움직임).
const FVector2D LookVector = Value.Get<FVector2D>();
// 컨트롤러가 유효한지 확인합니다.
if (Controller != nullptr)
{
// 마우스 X축 움직임으로 컨트롤러의 Yaw(좌우) 회전을 추가합니다.
AddControllerYawInput(LookVector.X);
// 마우스 Y축 움직임으로 컨트롤러의 Pitch(상하) 회전을 추가합니다.
AddControllerPitchInput(LookVector.Y);
}
}
// 매 프레임마다 누적된 이동 입력을 기반으로 Pawn을 실제로 이동시킵니다.
void AMyPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// AddMovementInput으로 누적된 입력 벡터를 가져오고, 대각선 이동 보정을 위해 크기를 1로 제한합니다.
// 마지막으로 속도와 DeltaTime을 곱해 이번 프레임에 이동할 변위(Delta)를 계산합니다.
FVector DeltaLocation = ConsumeMovementInputVector().GetClampedToMaxSize(1.0f) * MoveSpeed * DeltaTime;
// 이동할 거리가 있을 경우에만 이동 로직을 실행합니다.
if (!DeltaLocation.IsNearlyZero())
{
// 계산된 변위만큼 Pawn을 로컬 좌표계 기준으로 이동시킵니다.
AddActorLocalOffset(DeltaLocation, true); // true는 스윕(Sweep) 옵션 활성화. 이동 중 다른 액터와 충돌하는지 검사합니다.
}
}
BeginPlay에서 Subsystem->AddMappingContext() 코드를 누락했기 때문이었다. IMC를 등록하지 않으면 엔진이 어떤 키에 어떤 액션을 실행할지 전혀 알 수 없다는 것을 깨달았다.IA_Move에 A, D키를 매핑할 때 "Swizzle Input Axis Values" Modifier를 추가하지 않아서 발생했다. 이 옵션은 Vector2D의 X와 Y를 바꿔주는 역할을 하는데, 이를 통해 A/D 입력을 Y축(좌우) 입력으로 올바르게 변환할 수 있었다.Tick 함수에서 DeltaTime을 곱하는 것을 잊었다. 내 컴퓨터에서는 정상 속도처럼 보였지만, 프레임이 다른 환경에서는 문제가 될 수 있다는 것을 배우고 바로 수정했다. 프레임 독립성은 정말 중요한 개념이다.| 개념 | 설명 | 비고 |
|---|---|---|
| Pawn 클래스 | 플레이어가 빙의할 수 있는 액터의 기본 단위. 자체 이동 기능이 없다. | Character는 이동 기능이 추가된 Pawn의 자식 클래스이다. |
| 루트 컴포넌트 | 액터의 위치와 회전의 기준이 되는 최상위 컴포넌트. | 보통 충돌을 담당하는 CapsuleComponent 등을 루트로 설정한다. |
| Enhanced Input | 액션 기반의 최신 입력 시스템. IA(무엇을), IMC(어떻게), Bind(실행)의 3단계로 구성된다. | BeginPlay에서 AddMappingContext를 잊지 말자! |
DeltaTime | 이전 프레임부터 현재 프레임까지 걸린 시간. | 이동/회전 값에 곱하여 "프레임 독립성"을 확보하는 데 필수적이다. |
AddActorLocalOffset | 액터의 로컬 좌표계를 기준으로 액터를 이동시키는 함수. | 캐릭터 이동처럼 자기 자신의 방향을 기준으로 움직일 때 유용하다. |
