https://github.com/K-Tia20/Basic-Actor-Control-Project.git
과제 조건
- Pawn 클래스 생성:
Pawn을 상속받는 새로운 C++ 클래스를 생성합니다. - 컴포넌트 추가: 아래 컴포넌트들을 Pawn 클래스에 추가합니다.
- `CapsuleComponent
-
SkeletalMeshComponent -
SpringArmComponent -
CameraComponent - 계층 구조 설정: 충돌 컴포넌트를 RootComponent로 설정하고, 나머지 컴포넌트들을 부착합니다.
- DefaultPawn 설정:
GameMode클래스에서DefaultPawnClass를 본인이 만든 Pawn으로 지정합니다. - Physics 설정: 루트 충돌체 및 Mesh의
Simulate Physics를 false로 설정합니다. (물리 대신 코드로 직접 제어) - Enhanced Input 매핑 & 바인딩
- 입력 액션(IA) 생성: 아래 두 가지 액션을 생성합니다. (타입: Vector2D)
-
IA_Move(WASD 이동용) -
IA_Look(마우스 회전용) - IMC 매핑: 생성한 액션들을
Input Mapping Context에 등록하고 키를 할당합니다. - 액션 바인딩:
SetupPlayerInputComponent()에서 입력 처리 함수와 액션들을 바인딩합니다. - 프레임 독립성:
DeltaTime을 사용하여 프레임 속도와 관계없이 일정한 속도로 움직이도록 구현합니다. - 이동 구현:
AddActorLocalOffset()등을 활용해 WASD 입력에 따라 Pawn이 움직이도록 작성합니다. - 이동 방향은 Pawn의 Forward/Right 벡터를 기준으로 결정됩니다.
- 회전 구현:
AddActorLocalRotation()등을 활용해 마우스 입력에 따라 회전하도록 작성합니다. - 마우스 입력값으로 Yaw와 Pitch를 직접 계산하여 구현합니다.
- ⚠️ 주의:
AddControllerYawInput(),AddControllerPitchInput()등 엔진 기본 제공 함수는 사용하지 않습니다. - 제한 사항: 평면 상에서의 이동과 회전만 처리하며, 중력이나 낙하 효과는 고려하지 않습니다.
- 6축 액션 구현: 아래 방향에 대한 이동 및 회전을 모두 구현합니다.
- 이동: 전/후(W, S), 좌/우(A, D), 상/하(Space, Shift)
- 회전: Yaw(마우스 X), Pitch(마우스 Y), Roll(마우스 휠 또는 별도 키) 회전: Yaw(마우스 X), Pitch(마우스 Y), Roll(마우스 휠 또는 별도 키)
- Local 기반 이동: 단순 월드 좌표 이동이 아닌, Pawn의 현재 회전 상태(로컬 좌표계)를 기준으로 이동 방향이 결정되도록 구현합니다.
- 인공 중력:
Tick()함수에서 매 프레임 중력 가속도를 직접 계산하여 적용합니다. (예: -980 cm/s²) - 지면 충돌 감지:
LineTrace또는SweepTrace를 사용하여 지면과의 충돌을 체크합니다. - 상태 초기화: 지면에 착지하는 순간 Z축 낙하 속도를 0으로 초기화합니다.
- 이동 속도 제한: 공중에 떠 있는 상태에서는 지상 이동 속도의 30~50% 수준으로 제한합니다.
- 상태별 로직 분기: 지상 상태와 공중 상태를 구분하여 각각 자연스러운 움직임이 나타나도록 구현합니다.
OkPlayerController.h
// OkPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "OkPlayerController.generated.h"
class UInputMappingContext;
class UInputAction;
UCLASS()
class P3P4_API AOkPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AOkPlayerController();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputMappingContext* InputMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* LookAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* MoveUpAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* MoveDownAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* RollAction;
protected:
virtual void BeginPlay() override;
};
OkPlayerController.cpp
// OkPlayerController.cpp
#include "Controller/OkPlayerController.h"
#include "EnhancedInputSubsystems.h"
AOkPlayerController::AOkPlayerController()
: InputMappingContext(nullptr),
MoveAction(nullptr),
LookAction(nullptr),
MoveUpAction(nullptr),
MoveDownAction(nullptr),
RollAction(nullptr)
{
}
void AOkPlayerController::BeginPlay()
{
Super::BeginPlay();
if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
if (InputMappingContext)
{
Subsystem->AddMappingContext(InputMappingContext, 0);
}
}
}
}
OkPawn.h
// OkPawn.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "OkPawn.generated.h"
class UCapsuleComponent;
class USkeletalMeshComponent;
class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;
UCLASS()
class P3P4_API AOkPawn : public APawn
{
GENERATED_BODY()
public:
AOkPawn();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UCapsuleComponent* CapsuleComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USkeletalMeshComponent* MeshComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USpringArmComponent* SpringArmComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UCameraComponent* CameraComp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MoveSpeed = 1000.0f;
FVector MoveInputVector;
FVector LookInputVector;
float VerticalVelocity = 0.0f;
const float Gravity = -300.0f;
bool bIsGrounded = false;
UFUNCTION()
void Move(const FInputActionValue& Value);
UFUNCTION()
void MoveUp(const FInputActionValue& Value);
UFUNCTION()
void MoveDown(const FInputActionValue& Value);
UFUNCTION()
void Look(const FInputActionValue& Value);
UFUNCTION()
void RollAxis(const FInputActionValue& Value);
};
OkPawn.cpp
// OkPawn.cpp
#include "Pawn/OkPawn.h"
#include "Controller/OkPlayerController.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputComponent.h"
AOkPawn::AOkPawn()
{
PrimaryActorTick.bCanEverTick = true;
CapsuleComp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComp"));
SetRootComponent(CapsuleComp);
CapsuleComp->InitCapsuleSize(34.0f, 88.0f);
CapsuleComp->SetSimulatePhysics(false);
MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("MeshComp"));
MeshComp->SetupAttachment(RootComponent);
MeshComp->SetSimulatePhysics(false);
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComp"));
SpringArmComp->SetupAttachment(RootComponent);
SpringArmComp->TargetArmLength = 300.0f;
SpringArmComp->bUsePawnControlRotation = false;
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComp"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
CameraComp->bUsePawnControlRotation = false;
MoveInputVector = FVector::ZeroVector;
LookInputVector = FVector::ZeroVector;
}
void AOkPawn::BeginPlay()
{
Super::BeginPlay();
}
void AOkPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FHitResult HitResult;
FVector StartLocation = GetActorLocation();
float TraceLength = CapsuleComp->GetScaledCapsuleHalfHeight() + 5.0f;
FVector EndLocation = StartLocation - FVector(0.0f, 0.0f, TraceLength);
FCollisionQueryParams TraceParams;
TraceParams.AddIgnoredActor(this);
bool bHit = GetWorld()->LineTraceSingleByChannel(HitResult, StartLocation, EndLocation, ECC_Visibility, TraceParams);
if (bHit)
{
bIsGrounded = true;
if (VerticalVelocity < 0.0f)
{
VerticalVelocity = 0.0f;
}
}
else
{
bIsGrounded = false;
VerticalVelocity += Gravity * DeltaTime;
}
float CurrentSpeed = bIsGrounded ? MoveSpeed : MoveSpeed * 0.4f;
FVector InstantMove = MoveInputVector * CurrentSpeed * DeltaTime;
AddActorLocalOffset(InstantMove, true);
AddActorWorldOffset(FVector(0.0f, 0.0f, VerticalVelocity * DeltaTime), true);
FRotator PawnRotation = FRotator(0.0f, LookInputVector.X, LookInputVector.Z);
AddActorLocalRotation(PawnRotation);
FRotator NewArmRotation = SpringArmComp->GetRelativeRotation();
NewArmRotation.Pitch = FMath::Clamp(NewArmRotation.Pitch + LookInputVector.Y, -60.0f, 60.0f);
SpringArmComp->SetRelativeRotation(NewArmRotation);
MoveInputVector = FVector::ZeroVector;
LookInputVector = FVector::ZeroVector;
}
void AOkPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (AOkPlayerController* PC = Cast<AOkPlayerController>(GetController()))
{
if (PC->MoveAction)
EnhancedInput->BindAction(PC->MoveAction, ETriggerEvent::Triggered, this, &AOkPawn::Move);
if (PC->LookAction)
EnhancedInput->BindAction(PC->LookAction, ETriggerEvent::Triggered, this, &AOkPawn::Look);
if (PC->MoveUpAction)
EnhancedInput->BindAction(PC->MoveUpAction, ETriggerEvent::Triggered, this, &AOkPawn::MoveUp);
if (PC->MoveDownAction)
EnhancedInput->BindAction(PC->MoveDownAction, ETriggerEvent::Triggered, this, &AOkPawn::MoveDown);
if (PC->RollAction)
EnhancedInput->BindAction(PC->RollAction, ETriggerEvent::Triggered, this, &AOkPawn::RollAxis);
}
}
}
void AOkPawn::Move(const FInputActionValue& Value)
{
FVector2D Input = Value.Get<FVector2D>();
MoveInputVector.X = Input.Y;
MoveInputVector.Y = Input.X;
}
void AOkPawn::MoveUp(const FInputActionValue& Value)
{
MoveInputVector.Z = 1.0f;
}
void AOkPawn::MoveDown(const FInputActionValue& Value)
{
MoveInputVector.Z = -1.0f;
}
void AOkPawn::Look(const FInputActionValue& Value)
{
FVector2D Input = Value.Get<FVector2D>();
LookInputVector.X = Input.X;
LookInputVector.Y = -Input.Y;
}
void AOkPawn::RollAxis(const FInputActionValue& Value)
{
LookInputVector.Z = Value.Get<float>() * 5.0f;
}
OkGameMode.h
// OkGameMode.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "OkGameMode.generated.h"
UCLASS()
class P3P4_API AOkGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AOkGameMode();
};
OkGameMode.cpp
// OkGameMode.cpp
#include "GameMode/OkGameMode.h"
#include "Pawn/OkPawn.h"
#include "Controller/OkPlayerController.h"
AOkGameMode::AOkGameMode()
{
DefaultPawnClass = AOkPawn::StaticClass();
PlayerControllerClass = AOkPlayerController::StaticClass();
}
자세한 설명
작성 방법
1. 에디터에서 입력 에셋(IA, IMC) 생성하기
콘텐츠 브라우저의 원하는 폴더에서 우클릭한 뒤 Input(입력) 메뉴를 통해 생성합니다.
- Input Action(입력 액션) 생성 (총 5개)
IA_Move생성 더블클릭 후 Value Type을Axis2D (Vector2D)로 변경IA_Look생성 더블클릭 후 Value Type을Axis2D (Vector2D)로 변경IA_MoveUp생성 Value Type은 기본값인Digital (bool)유지IA_MoveDown생성 Value Type은 기본값인Digital (bool)유지IA_Roll생성 더블클릭 후 Value Type을Axis1D (float)로 변경
- Input Mapping Context(입력 매핑 컨텍스트) 생성
- 이름은
IMC_Ok등으로 생성합니다.
- 이름은
2. IMC에 키 매핑하기 (중요)
IMC_Ok를 더블클릭해 열고, 위에서 만든 액션들을 등록한 뒤 키를 매핑합니다. 과제 조건(6축)을 만족하려면 아래 설정을 똑같이 맞춰야 합니다.
IA_Move (WASD 이동)
Mappings에 IA_Move를 추가하고 키를 4개 등록합니다.
- W 키: Modifiers에
Swizzle Input Axis Values추가 (Y축 전진으로 변경하기 위함) - S 키: Modifiers에
Swizzle Input Axis Values와Negate둘 다 추가 (Y축 후진) - D 키: 아무 Modifier도 넣지 않음 (기본 X축 우측 이동)
- A 키: Modifiers에
Negate추가 (X축 좌측 이동)
상하 이동 (IA_MoveUp / IA_MoveDown)
IA_MoveUp추가 Space Bar 지정IA_MoveDown추가 Left Shift 지정
IA_Look (마우스 회전)
Mappings에 IA_Look을 추가합니다.
- Mouse XY 2D-Axis 지정
- Modifiers에
Negate를 추가하고, 내부 화살표를 열어Y축만 체크합니다. (마우스를 올렸을 때 화면이 위를 보게 만드는 세팅입니다.)
IA_Roll (휠 회전)
Mappings에 IA_Roll을 추가합니다.
- Mouse Wheel Axis 지정
3. 블루프린트 클래스 만들고 에셋 연결하기
C++ 코드는 뼈대일 뿐이므로, 에디터에서 블루프린트 클래스로 상속받아 에셋들을 변수에 심어주어야 합니다.
① OkPlayerController 블루프린트 만들기
OkPlayerControllerC++ 클래스를 우클릭하고 [Create Blueprint class based on OkPlayerController]를 선택합니다.- 이름은
BP_OkPlayerController로 만듭니다. - 블루프린트를 열어 우측 Details(디테일) 패널의 Input 카테고리를 찾습니다.
- 우리가 만들었던 C++ 변수들(
Input Mapping Context,Move Action,Look Action등)이 보일 겁니다. 여기에 방금 에디터에서 만든IMC_Ok와IA_...에셋들을 전부 하나씩 매칭하여 넣어줍니다. - 컴파일 후 저장합니다.
② OkPawn 블루프린트 만들기
OkPawnC++ 클래스를 우클릭하고 [Create Blueprint class based on OkPawn]을 선택합니다.- 이름은
BP_OkPawn으로 만듭니다. - 뷰포트나 컴포넌트 창에서
MeshComp를 선택한 뒤, 디테일 창에서 원하는 Skeletal Mesh(캐릭터 외형)를 지정해 줍니다. - 컴파일 후 저장합니다.
③ OkGameMode 블루프린트 만들기 (또는 월드 세팅 수정)
OkGameModeC++ 클래스를 우클릭하고BP_OkGameMode를 생성합니다.BP_OkGameMode를 열어 디테일 패널의 Classes 항목을 확인합니다.- Default Pawn Class를 우리가 방금 만든
BP_OkPawn으로 변경합니다. - Player Controller Class를 우리가 방금 만든
BP_OkPlayerController로 변경합니다.
4. 최종 게임에 적용하기
마지막으로 내가 만든 레벨(맵)이 이 게임모드를 사용하도록 지정해야 합니다.
- 에디터 상단 메뉴에서 Window(창) World Settings(월드 세팅)을 켭니다.
- 우측에 월드 세팅 창이 뜨면 GameMode Override 항목을 찾습니다.
- 여기에 방금 만든
BP_OkGameMode를 지정해 줍니다.