스파르타 클럽 챕터3 과제4 진행
Pawn으로 이동하는 객체 만들기와 도전과제로 비행체 만들기입니다.
https://www.youtube.com/watch?v=qnovx8h6Qr8
Pawn에 걷기, 점프, 달리기를 추가했고, 추가적으로 Pawn클래스를 하나 더 만들어 비행체를 구현했습니다.
이동과 화면회전은 AddActorLocalLocation과 AddActorLocalRotation으로 구현했습니다.
Character클래스의 경우는 이동할 때 바닥이나 벽면에서 Block되었어도 미끄러지도록 구현이 되어있지만
Pawn의 경우 그런 물리로직을 직접 구현해야 했습니다.
SpartaPawn.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "SpartaPawn.generated.h"
class UCameraComponent;
class UCapsuleComponent;
class USkeletalMeshComponent;
class USpringArmComponent;
class USpartaMoveComponent;
struct FInputActionValue;
UCLASS()
class SPARTA3_4_API ASpartaPawn : public APawn
{
GENERATED_BODY()
public:
ASpartaPawn();
UPROPERTY(EditAnywhere)
UCapsuleComponent* Capsule;
UPROPERTY(EditAnywhere)
USkeletalMeshComponent* SkeletalMesh;
UPROPERTY(EditAnywhere)
USpringArmComponent* SpringArm;
UPROPERTY(EditAnywhere)
UCameraComponent* Camera;
UPROPERTY(EditAnywhere)
USpartaMoveComponent* MoveComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Look")
float LookSensitivity = 90.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Look")
float MinPitch = -50.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Look")
float MaxPitch = 40.f;
UFUNCTION(BlueprintCallable)
USpartaMoveComponent* GetMoveComponent() { return MoveComponent; }
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
public:
virtual void Tick(float DeltaTime) override;
private:
FVector2D LookInput = FVector2D::ZeroVector;
float CurrentPitch = 0.f;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartJump(const FInputActionValue& Value);
void StopJump(const FInputActionValue& Value);
void StartSprint(const FInputActionValue& Value);
void StopSprint(const FInputActionValue& Value);
void UpdateLook(float DeltaTime);
};
SpartaPawn.cpp
#include "SpartaPawn.h"
#include "EnhancedInputComponent.h"
#include "SpartaPlayerContrller.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "SpartaMoveComponent.h"
/*
* Pawn의 역할
*
* 이 클래스는 플레이어의 컴포넌트 구성과 입력 연결을 담당합니다.
* 실제 이동, 점프, 중력, 벽 슬라이딩은 USpartaMoveComponent에서 처리합니다.
*
* 카메라 회전은 여기서 직접 처리합니다.
* - 마우스 X: Pawn 전체를 좌우(Yaw) 회전
* - 마우스 Y: SpringArm만 위아래(Pitch) 회전
*
* 이렇게 나누면 Capsule은 항상 똑바로 서 있고,
* 카메라만 위아래로 움직이는 Character 같은 시점을 만들 수 있습니다.
*/
ASpartaPawn::ASpartaPawn()
{
PrimaryActorTick.bCanEverTick = true;
// Capsule은 RootComponent이며, 플레이어 이동 충돌을 담당합니다.
Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
SetRootComponent(Capsule);
Capsule->InitCapsuleSize(42.f, 96.f);
Capsule->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Capsule->SetCollisionResponseToAllChannels(ECR_Block);
Capsule->SetSimulatePhysics(false);
// SkeletalMesh는 화면에 보이는 용도만 담당하고, 충돌은 Capsule만 사용합니다.
SkeletalMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Skeleton"));
SkeletalMesh->SetupAttachment(Capsule);
SkeletalMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
SkeletalMesh->SetSimulatePhysics(false);
// SpringArm은 카메라의 위아래 회전을 담당합니다. Pawn 본체는 위아래로 기울이지 않습니다.
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("Spring Arm"));
SpringArm->AttachToComponent(Capsule, FAttachmentTransformRules::KeepRelativeTransform);
SpringArm->SetRelativeLocation(FVector(0.f, 0.f, 70.f));
SpringArm->SetRelativeRotation(FRotator(CurrentPitch, 0.f, 0.f));
SpringArm->TargetArmLength = 500.f;
SpringArm->bUsePawnControlRotation = false;
SpringArm->bDoCollisionTest = false;
// Camera는 SpringArm 소켓에 붙어 SpringArm 끝 위치를 따라갑니다.
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
Camera->SetRelativeLocation(FVector::ZeroVector);
Camera->SetRelativeRotation(FRotator::ZeroRotator);
Camera->bUsePawnControlRotation = false;
// 실제 이동, 점프, 중력, 벽 슬라이딩은 MoveComponent에서 처리합니다.
MoveComponent = CreateDefaultSubobject<USpartaMoveComponent>(TEXT("MoveComponent"));
MoveComponent->UpdatedComponent = Capsule;
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
}
void ASpartaPawn::BeginPlay()
{
Super::BeginPlay();
}
void ASpartaPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
UpdateLook(DeltaTime);
}
void ASpartaPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Pawn은 Enhanced Input을 받고, 이동 관련 입력은 MoveComponent로 전달합니다.
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (ASpartaPlayerContrller* PlayerController = Cast<ASpartaPlayerContrller>(GetController()))
{
EnhancedInput->BindAction(PlayerController->MoveAction, ETriggerEvent::Triggered, this, &ASpartaPawn::Move);
EnhancedInput->BindAction(PlayerController->MoveAction, ETriggerEvent::Completed, this, &ASpartaPawn::Move);
EnhancedInput->BindAction(PlayerController->LookAction, ETriggerEvent::Triggered, this, &ASpartaPawn::Look);
EnhancedInput->BindAction(PlayerController->LookAction, ETriggerEvent::Completed, this, &ASpartaPawn::Look);
EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Triggered, this, &ASpartaPawn::StartJump);
EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Completed, this, &ASpartaPawn::StopJump);
EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Triggered, this, &ASpartaPawn::StartSprint);
EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Completed, this, &ASpartaPawn::StopSprint);
}
}
}
void ASpartaPawn::Move(const FInputActionValue& Value)
{
// 이동 입력값만 MoveComponent에 넘기고, 실제 이동은 MoveComponent Tick에서 처리합니다.
if (MoveComponent)
{
MoveComponent->SetMoveInput(Value.Get<FVector2D>());
}
}
void ASpartaPawn::Look(const FInputActionValue& Value)
{
// 마우스 입력값은 저장해두고 Tick에서 DeltaTime을 곱해 회전 처리합니다.
LookInput = Value.Get<FVector2D>();
}
void ASpartaPawn::StartJump(const FInputActionValue& Value)
{
if (MoveComponent)
{
MoveComponent->StartJump();
}
}
void ASpartaPawn::StopJump(const FInputActionValue& Value)
{
if (MoveComponent)
{
MoveComponent->StopJump();
}
}
void ASpartaPawn::StartSprint(const FInputActionValue& Value)
{
if (MoveComponent && Value.Get<bool>())
{
MoveComponent->StartSprint();
}
}
void ASpartaPawn::StopSprint(const FInputActionValue& Value)
{
if (MoveComponent)
{
MoveComponent->StopSprint();
}
}
void ASpartaPawn::UpdateLook(float DeltaTime)
{
if (LookInput.IsNearlyZero()) return;
// 마우스 X축은 Pawn 전체를 좌우(Yaw)로 회전시킵니다.
// 이 회전은 이동 방향에도 영향을 줍니다. Forward/Right 벡터가 같이 바뀌기 때문입니다.
const float Yaw = LookInput.X * LookSensitivity * DeltaTime;
AddActorLocalRotation(FRotator(0.f, Yaw, 0.f), false);
// 마우스 Y축은 SpringArm만 위아래(Pitch)로 회전시켜 Capsule은 항상 똑바로 서 있게 합니다.
// CurrentPitch를 제한해서 카메라가 너무 위나 아래로 꺾이지 않게 합니다.
CurrentPitch = FMath::Clamp(
CurrentPitch - LookInput.Y * LookSensitivity * DeltaTime,
MinPitch,
MaxPitch
);
if (SpringArm) SpringArm->SetRelativeRotation(FRotator(CurrentPitch, 0.f, 0.f));
}
SpartaMoveComponent.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PawnMovementComponent.h"
#include "SpartaMoveComponent.generated.h"
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class SPARTA3_4_API USpartaMoveComponent : public UPawnMovementComponent
{
GENERATED_BODY()
public:
USpartaMoveComponent();
// 기본 이동 속도입니다. Unreal 단위 기준으로 초당 이동 거리입니다.
UPROPERTY(EditAnywhere, Category = "Move")
float MoveSpeed = 450;
// 달리기 입력이 켜져 있을 때 MoveSpeed에 곱해지는 값입니다.
UPROPERTY(EditAnywhere, Category = "Move")
float SprintMultiplier = 2.5f;
// 점프를 시작할 때 위쪽으로 주는 초기 속도입니다.
UPROPERTY(EditAnywhere, Category = "Jump")
float JumpPower = 800.f;
// 공중에 있을 때 매 프레임 아래쪽으로 누적되는 중력 가속도입니다.
UPROPERTY(EditAnywhere, Category = "Physics")
float Gravity = 980.f;
// Pawn 중심에서 아래로 레이를 쏴서 바닥을 찾는 거리입니다.
UPROPERTY(EditAnywhere, Category = "Physics")
float GroundCheckDistance = 120.f;
// 이 값보다 Normal.Z가 크면 걸을 수 있는 바닥이나 경사로 판단합니다.
UPROPERTY(EditAnywhere, Category = "Physics")
float WalkableFloorZ = 0.6f;
// 이동 및 달리기 애니메이션을 위한 값을 반환합니다.
UFUNCTION(BlueprintCallable)
bool IsMove() { return MoveInput.IsNearlyZero(); }
// 점프하는 애니메이션을 위한 값을 반환합니다.
UFUNCTION(BlueprintCallable)
float GetVerticalVelocity() { return VerticalVelocity; }
// 떨어지는 애니메이션을 위한 값을 반환합니다.
UFUNCTION(BlueprintCallable)
bool GetIsGrounded() { return bIsGrounded; }
// 달리는 애니메이션을 위한 값을 반환합니다.
UFUNCTION(BlueprintCallable)
bool GetIsSprinting() { return bIsSprinting; }
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
public:
void SetMoveInput(const FVector2D& Value);
void StartJump();
void StopJump();
void StartSprint();
void StopSprint();
private:
// 매 Tick마다 GetOwner, FindComponent를 반복하지 않기 위해 캐싱합니다.
UPROPERTY()
APawn* CachedOwnerPawn = nullptr;
UPROPERTY()
UCapsuleComponent* CachedCapsule = nullptr;
// 현재 밟고 있는 Actor입니다. 움직이는 발판을 따라가기 위해 사용합니다.
UPROPERTY()
AActor* GroundActor = nullptr;
// Pawn의 Enhanced Input 함수에서 받은 이동 입력값을 저장합니다.
FVector2D MoveInput = FVector2D::ZeroVector;
// CharacterMovement를 쓰지 않기 때문에 Z축 속도를 직접 관리합니다.
float VerticalVelocity = 0.f;
// Capsule이 현재 바닥 위에 정확히 서기 위한 ActorLocation.Z 값입니다.
float GroundZ =0.f;
// 기본이동값과 달리기의 값을 저장하기 위한 변수입니다.
float CurrentSpeed = 0.f;
// 현재 밟고 있는 바닥의 Normal입니다. 울퉁불퉁한 바닥을 따라 이동할 때 사용합니다.
FVector GroundNormal = FVector::UpVector;
// 이전 프레임의 GroundActor 위치입니다. 움직인 발판의 이동량을 계산합니다.
FVector LastGroundActorLocation = FVector::ZeroVector;
bool bIsGrounded = true;
bool bIsSprinting = false;
// 아래 방향 LineTrace로 바닥 여부, 바닥 Normal, 밟고 있는 Actor를 갱신합니다.
void CheckGround();
// 입력 이동, 중력, 점프 속도를 합친 뒤 Sweep 이동과 벽면 슬라이드를 처리합니다.
void ApplyMovement(float DeltaTime);
// 현재 밟고 있는 움직이는 발판의 이동량만큼 Pawn을 먼저 이동시킵니다.
void ApplyGroundActorMovement();
};
SpartaMoveComponent.cpp
#include "SpartaMoveComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/Pawn.h"
/*
* 이 컴포넌트의 전체 이동 원리
*
* 1. Pawn에서 받은 입력값을 MoveInput에 저장한다.
* 2. Tick마다 아래 순서로 이동을 처리한다.
* - 이전 프레임에 밟고 있던 발판이 움직였으면 Pawn도 먼저 따라간다.
* - 아래로 LineTrace를 쏴서 현재 바닥 상태를 확인한다.
* - 입력 이동, 점프 속도, 중력을 합쳐 이번 프레임 이동량을 만든다.
* - Capsule을 Sweep 이동시켜 벽이나 바닥에 부딪혔는지 확인한다.
* - 충돌했다면 SlideAlongSurface로 벽면이나 경사면을 따라 미끄러지게 한다.
*
* Simulate Physics는 사용하지 않는다.
* 대신 직접 계산한 이동량 + Sweep 충돌 검사 + 바닥 레이 + 슬라이딩으로
* CharacterMovement와 비슷한 동작을 흉내낸다.
*/
USpartaMoveComponent::USpartaMoveComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void USpartaMoveComponent::BeginPlay()
{
Super::BeginPlay();
CachedOwnerPawn = Cast<APawn>(GetOwner());
if (CachedOwnerPawn)
{
CachedCapsule = CachedOwnerPawn->FindComponentByClass<UCapsuleComponent>();
SetUpdatedComponent(CachedOwnerPawn->GetRootComponent());
GroundZ = CachedOwnerPawn->GetActorLocation().Z;
}
}
void USpartaMoveComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!CachedOwnerPawn || !UpdatedComponent || ShouldSkipUpdate(DeltaTime))
{
return;
}
// 1. 지난 프레임에 밟고 있던 발판이 움직였다면 Pawn도 그만큼 먼저 따라갑니다.
// CharacterMovement의 Movement Base 개념을 간단하게 흉내낸 부분입니다.
ApplyGroundActorMovement();
// 2. 현재 프레임 기준으로 바닥에 있는지, 바닥 Normal이 무엇인지 다시 확인합니다.
CheckGround();
// 3. 입력 이동, 중력, 점프, 충돌, 슬라이딩을 한 번에 처리합니다.
ApplyMovement(DeltaTime);
}
void USpartaMoveComponent::SetMoveInput(const FVector2D& Value)
{
MoveInput = Value;
}
void USpartaMoveComponent::StartJump()
{
// 바닥에 있을 때만 점프를 시작합니다.
// 점프는 실제 물리 힘이 아니라 VerticalVelocity 값을 직접 올리는 방식입니다.
if (bIsGrounded)
{
VerticalVelocity = JumpPower;
bIsGrounded = false;
}
}
void USpartaMoveComponent::StopJump()
{
// 점프 키를 떼면 상승 속도를 줄여서 짧은 점프가 가능하게 합니다.
if (VerticalVelocity > JumpPower * 0.5f)
{
VerticalVelocity = JumpPower * 0.5f;
}
}
void USpartaMoveComponent::StartSprint()
{
bIsSprinting = true;
}
void USpartaMoveComponent::StopSprint()
{
bIsSprinting = false;
}
void USpartaMoveComponent::CheckGround()
{
if (!CachedOwnerPawn)
{
return;
}
UWorld* World = CachedOwnerPawn->GetWorld();
if (!World)
{
return;
}
const FVector Start = CachedOwnerPawn->GetActorLocation();
const FVector End = Start - FVector(0.f, 0.f, GroundCheckDistance);
FHitResult Hit;
FCollisionQueryParams Params(SCENE_QUERY_STAT(SpartaGroundCheck), false, CachedOwnerPawn);
// CharacterMovement의 FloorCheck를 직접 흉내내는 부분입니다.
// Pawn 중심에서 아래로 레이를 쏴서 걸을 수 있는 바닥인지 확인합니다.
//
// Hit.Normal.Z가 1에 가까우면 평평한 바닥이고,
// 0에 가까우면 수직 벽입니다.
// WalkableFloorZ 이상이면 걸을 수 있는 바닥이나 경사로 판단합니다.
const bool bHit = World->LineTraceSingleByChannel(
Hit,
Start,
End,
ECC_Visibility,
Params
);
if (bHit && Hit.Normal.Z >= WalkableFloorZ && VerticalVelocity <= 0.f)
{
bIsGrounded = true;
// 바닥의 기울기를 저장해두면 이동 벡터를 바닥면에 맞춰 보정할 수 있습니다.
GroundNormal = Hit.Normal;
// Hit.ImpactPoint.Z는 바닥 표면 높이입니다.
// Pawn의 ActorLocation은 Capsule 중심이므로 HalfHeight를 더해야 바닥 위에 서게 됩니다.
GroundZ = Hit.ImpactPoint.Z + CachedCapsule->GetScaledCapsuleHalfHeight();
// 바닥 Actor를 저장해두면, 다음 Tick에서 움직이는 발판의 이동량을 따라갈 수 있습니다.
GroundActor = Hit.GetActor();
if (GroundActor)
{
LastGroundActorLocation = GroundActor->GetActorLocation();
}
}
else
{
bIsGrounded = false;
GroundNormal = FVector::UpVector;
GroundActor = nullptr;
}
}
void USpartaMoveComponent::ApplyMovement(float DeltaTime)
{
if (!CachedOwnerPawn || !UpdatedComponent)
{
return;
}
CurrentSpeed = bIsSprinting ? MoveSpeed * SprintMultiplier : MoveSpeed;
// 바닥에 있을 때는 아래로 떨어지는 속도가 계속 누적되지 않도록 0으로 정리합니다.
// 공중이면 매 프레임 중력을 빼서 위로 올라가던 속도가 줄고, 결국 아래로 떨어지게 합니다.
if (bIsGrounded && VerticalVelocity < 0.f)
{
VerticalVelocity = 0.f;
}
else if (!bIsGrounded)
{
// 공중에 있을 때만 중력을 직접 적용합니다.
VerticalVelocity -= Gravity * DeltaTime;
}
// MoveInput.X/Y를 Pawn의 Right/Forward 방향으로 변환해서 월드 이동량을 만듭니다.
// X는 좌우, Y는 앞뒤 입력입니다.
FVector WorldHorizontalDelta = CachedOwnerPawn->GetActorForwardVector() * MoveInput.Y;
WorldHorizontalDelta += CachedOwnerPawn->GetActorRightVector() * MoveInput.X;
WorldHorizontalDelta = WorldHorizontalDelta.GetClampedToMaxSize(1.f) * CurrentSpeed * DeltaTime;
if (bIsGrounded)
{
// 바닥면에 이동 벡터를 투영합니다.
// 이렇게 해야 울퉁불퉁한 바닥이나 경사에서 이동 방향이 바닥을 따라갑니다.
// 즉, 바닥 안으로 파고드는 성분은 제거하고 바닥 표면을 따라가는 성분만 남깁니다.
WorldHorizontalDelta = FVector::VectorPlaneProject(WorldHorizontalDelta, GroundNormal);
}
// 수평 입력 이동량과 수직 점프/중력 이동량을 합쳐 이번 프레임 최종 이동량을 만듭니다.
const FVector WorldVerticalDelta(0.f, 0.f, VerticalVelocity * DeltaTime);
const FVector DesiredDelta = WorldHorizontalDelta + WorldVerticalDelta;
if (DesiredDelta.IsNearlyZero())
{
return;
}
FHitResult Hit;
// Capsule을 Sweep 이동합니다.
// 단순 순간이동이 아니라 이동 경로에 벽/바닥이 있으면 Hit에 충돌 정보가 들어옵니다.
// Sweep은 물리 시뮬레이션이 아니라 충돌 쿼리입니다.
SafeMoveUpdatedComponent(DesiredDelta, UpdatedComponent->GetComponentQuat(), true, Hit);
if (!Hit.IsValidBlockingHit())
{
return;
}
if (Hit.Normal.Z >= WalkableFloorZ && VerticalVelocity <= 0.f)
{
// 충돌한 면이 걸을 수 있는 바닥이면 새로운 바닥으로 저장합니다.
bIsGrounded = true;
GroundNormal = Hit.Normal;
GroundZ = Hit.ImpactPoint.Z + CachedCapsule->GetScaledCapsuleHalfHeight();
VerticalVelocity = 0.f;
GroundActor = Hit.GetActor();
if (GroundActor)
{
LastGroundActorLocation = GroundActor->GetActorLocation();
}
}
else
{
// 벽이나 너무 가파른 경사면에 부딪힌 경우입니다.
// 이때는 바닥으로 보지 않아서 중력/낙하가 유지됩니다.
bIsGrounded = false;
GroundNormal = FVector::UpVector;
GroundActor = nullptr;
}
// 벽에 딱 붙어서 멈추지 않고, 충돌한 표면을 따라 미끄러지도록 합니다.
// 원리:
// - 원래 이동 방향에서 벽 Normal 방향으로 들어가는 성분을 제거한다.
// - 남은 이동량으로 벽면을 따라 이동한다.
// Unreal의 MovementComponent가 제공하는 기본 슬라이딩 처리입니다.
SlideAlongSurface(DesiredDelta, 1.f - Hit.Time, Hit.Normal, Hit, true);
}
void USpartaMoveComponent::ApplyGroundActorMovement()
{
if (!bIsGrounded || !GroundActor || !UpdatedComponent)
{
return;
}
const FVector CurrentGroundActorLocation = GroundActor->GetActorLocation();
const FVector GroundDelta = CurrentGroundActorLocation - LastGroundActorLocation;
LastGroundActorLocation = CurrentGroundActorLocation;
if (GroundDelta.IsNearlyZero())
{
return;
}
FHitResult Hit;
// 플레이어 입력 이동 전에, 발판이 움직인 거리만큼 Pawn을 먼저 이동시킵니다.
// 이 처리가 없으면 위아래로 움직이는 플랫폼이 Pawn 밑에서 빠져나가게 됩니다.
// 즉, "발판 위에 서 있으면 발판의 월드 이동량을 Pawn에게도 적용한다"는 뜻입니다.
SafeMoveUpdatedComponent(GroundDelta, UpdatedComponent->GetComponentQuat(), true, Hit);
}
SpartaAircraft.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "SpartaAircraft.generated.h"
class UCameraComponent;
class USpringArmComponent;
class UStaticMeshComponent;
struct FInputActionValue;
UCLASS()
class SPARTA3_4_API ASpartaAircraft : public APawn
{
GENERATED_BODY()
public:
// 6자유도 비행체 Pawn입니다.
// 현재 회전 상태를 기준으로 전/후, 좌/우, 상/하 이동하고 Yaw/Pitch/Roll을 직접 계산합니다.
ASpartaAircraft();
// 비행체의 시각/충돌 루트 컴포넌트입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components")
UStaticMeshComponent* Mesh;
// 카메라를 비행체 뒤에 배치하기 위한 SpringArm입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components")
USpringArmComponent* SpringArm;
// 실제 플레이어 시야 카메라입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components")
UCameraComponent* Camera;
// 지상 또는 일반 비행 상태에서의 기본 이동 속도입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Move")
float MoveSpeed = 800.f;
// 공중 상태에서 입력 이동 속도에 곱해지는 값입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Move")
float AirControlMultiplier = 0.4f;
// 마우스 X/Y로 적용되는 Yaw/Pitch 회전 속도입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rotation")
float LookSpeed = 90.f;
// Roll 입력에 적용되는 회전 속도입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rotation")
float RollSpeed = 120.f;
// 공중에서 매 프레임 적용할 인공 중력입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gravity")
float Gravity = 980.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gravity")
bool bEnableArtificialGravity = false;
// 아래 방향 지면 체크 거리입니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gravity")
float GroundCheckDistance = 120.f;
// 이 값 이상이면 착지 가능한 지면으로 판단합니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gravity")
float WalkableFloorZ = 0.6f;
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
public:
virtual void Tick(float DeltaTime) override;
private:
// MoveInput.X: 좌우, MoveInput.Y: 전후 입력입니다.
FVector2D MoveInput = FVector2D::ZeroVector;
// LookInput.X: Yaw, LookInput.Y: Pitch 입력입니다.
FVector2D LookInput = FVector2D::ZeroVector;
// 상하 이동 입력입니다. Space는 +1, Shift는 -1로 사용합니다.
float VerticalInput = 0.f;
// Roll 입력입니다. 마우스 휠 또는 별도 키 Axis 값으로 사용합니다.
float RollInput = 0.f;
// 인공 중력으로 누적되는 월드 Z축 속도입니다.
float FallingVelocity = 0.f;
bool bIsGrounded = false;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartMoveUp(const FInputActionValue& Value);
void StopMoveUp(const FInputActionValue& Value);
void StartMoveDown(const FInputActionValue& Value);
void StopMoveDown(const FInputActionValue& Value);
void CheckGround();
void ApplyMovement(float DeltaTime);
void ApplyRotation(float DeltaTime);
};
SpartaAircraft.cpp
#include "SpartaAircraft.h"
#include "EnhancedInputComponent.h"
#include "SpartaPlayerContrller.h"
#include "Camera/CameraComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/SpringArmComponent.h"
ASpartaAircraft::ASpartaAircraft()
{
PrimaryActorTick.bCanEverTick = true;
// StaticMesh를 루트로 사용해서 Sweep 충돌 검사를 받을 수 있게 합니다.
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("AircraftMesh"));
SetRootComponent(Mesh);
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
Mesh->SetSimulatePhysics(false);
// 비행체 뒤쪽 카메라입니다. 비행체가 회전하면 카메라도 함께 따라갑니다.
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(Mesh);
SpringArm->TargetArmLength = 600.f;
SpringArm->bUsePawnControlRotation = false;
SpringArm->bDoCollisionTest = false;
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
Camera->bUsePawnControlRotation = false;
}
void ASpartaAircraft::BeginPlay()
{
Super::BeginPlay();
}
void ASpartaAircraft::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bEnableArtificialGravity)
{
CheckGround();
}
ApplyRotation(DeltaTime);
ApplyMovement(DeltaTime);
}
void ASpartaAircraft::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (ASpartaPlayerContrller* PlayerController = Cast<ASpartaPlayerContrller>(GetController()))
{
// MoveAction(Vector2D): W/S는 전후, A/D는 좌우 이동입니다.
EnhancedInput->BindAction(PlayerController->MoveAction, ETriggerEvent::Triggered, this, &ASpartaAircraft::Move);
EnhancedInput->BindAction(PlayerController->MoveAction, ETriggerEvent::Completed, this, &ASpartaAircraft::Move);
// LookAction(Vector2D): 마우스 X는 Yaw, 마우스 Y는 Pitch 회전입니다.
EnhancedInput->BindAction(PlayerController->LookAction, ETriggerEvent::Triggered, this, &ASpartaAircraft::Look);
EnhancedInput->BindAction(PlayerController->LookAction, ETriggerEvent::Completed, this, &ASpartaAircraft::Look);
// JumpAction: Space 입력을 위쪽 이동으로 사용합니다.
EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Triggered, this, &ASpartaAircraft::StartMoveUp);
EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Completed, this, &ASpartaAircraft::StopMoveUp);
// SprintAction: Shift 입력을 아래쪽 이동으로 사용합니다.
EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Triggered, this, &ASpartaAircraft::StartMoveDown);
EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Completed, this, &ASpartaAircraft::StopMoveDown);
}
}
}
void ASpartaAircraft::Move(const FInputActionValue& Value)
{
MoveInput = Value.Get<FVector2D>();
}
void ASpartaAircraft::Look(const FInputActionValue& Value)
{
LookInput = Value.Get<FVector2D>();
}
void ASpartaAircraft::StartMoveUp(const FInputActionValue& Value)
{
if (Value.Get<bool>())
{
VerticalInput = 1.f;
bEnableArtificialGravity = false;
}
}
void ASpartaAircraft::StopMoveUp(const FInputActionValue& Value)
{
if (VerticalInput > 0.f)
{
VerticalInput = 0.f;
}
}
void ASpartaAircraft::StartMoveDown(const FInputActionValue& Value)
{
if (Value.Get<bool>())
{
VerticalInput = -1.f;
bEnableArtificialGravity = true;
}
}
void ASpartaAircraft::StopMoveDown(const FInputActionValue& Value)
{
if (VerticalInput < 0.f)
{
VerticalInput = 0.f;
}
}
void ASpartaAircraft::CheckGround()
{
UWorld* World = GetWorld();
if (!World)
{
return;
}
const FVector Start = GetActorLocation();
const FVector End = Start - FVector(0.f, 0.f, GroundCheckDistance);
FHitResult Hit;
FCollisionQueryParams Params(SCENE_QUERY_STAT(AircraftGroundCheck), false, this);
const bool bHit = World->LineTraceSingleByChannel(
Hit,
Start,
End,
ECC_Visibility,
Params
);
bIsGrounded = bHit && Hit.Normal.Z >= WalkableFloorZ && FallingVelocity <= 0.f;
if (bIsGrounded)
{
// 착지한 순간 낙하 속도를 초기화합니다.
FallingVelocity = 0.f;
}
}
void ASpartaAircraft::ApplyMovement(float DeltaTime)
{
const float ControlMultiplier = !bIsGrounded
? AirControlMultiplier
: 1.f;
const float CurrentMoveSpeed = MoveSpeed * ControlMultiplier;
// 현재 Actor 회전 기준의 로컬 축으로 이동합니다.
FVector WorldMoveDelta = GetActorForwardVector() * MoveInput.Y;
WorldMoveDelta += GetActorRightVector() * MoveInput.X;
WorldMoveDelta += GetActorUpVector() * VerticalInput;
WorldMoveDelta = WorldMoveDelta.GetClampedToMaxSize(1.f) * CurrentMoveSpeed * DeltaTime;
if (bEnableArtificialGravity && !bIsGrounded)
{
// 인공 중력은 월드 아래 방향으로 적용합니다.
FallingVelocity -= Gravity * DeltaTime;
}
if (bEnableArtificialGravity)
{
WorldMoveDelta.Z += FallingVelocity * DeltaTime;
}
if (WorldMoveDelta.IsNearlyZero())
{
return;
}
FHitResult Hit;
AddActorWorldOffset(WorldMoveDelta, true, &Hit);
if (Hit.IsValidBlockingHit())
{
if (bEnableArtificialGravity && Hit.Normal.Z >= WalkableFloorZ && FallingVelocity <= 0.f)
{
bIsGrounded = true;
FallingVelocity = 0.f;
return;
}
// 벽이나 가파른 면에 닿으면 표면을 따라 미끄러지게 이동합니다.
const FVector RemainingDelta = WorldMoveDelta * (1.f - Hit.Time);
const FVector SlideDelta = FVector::VectorPlaneProject(RemainingDelta, Hit.Normal);
AddActorWorldOffset(SlideDelta, true);
}
}
void ASpartaAircraft::ApplyRotation(float DeltaTime)
{
if (LookInput.IsNearlyZero() && FMath::IsNearlyZero(RollInput))
{
return;
}
// 6DOF 회전은 현재 비행체 로컬 축을 기준으로 누적됩니다.
const float PitchDelta = -LookInput.Y * LookSpeed * DeltaTime;
const float YawDelta = LookInput.X * LookSpeed * DeltaTime;
const float RollDelta = RollInput * RollSpeed * DeltaTime;
AddActorLocalRotation(FRotator(PitchDelta, YawDelta, RollDelta), true);
}
'언리얼 > 스파르타 클럽' 카테고리의 다른 글
| 언리얼 C++과제3 (0) | 2026.06.04 |
|---|