시행착오는 너무 많은 일이 있어서 정리 못할 것 같기에 생략합니다..
회전 기능이 있는 Actor (Rotating Actor)
이동 기능이 있는 Actor (Moving Actor)
프레임 독립성 - 이동/회전 시 반드시 DeltaTime 활용 (프레임에 따라 차이나지 않게)
리플렉션 적용
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MovingItem.generated.h"
UCLASS()
class CH3_API AMovingItem : public AActor
{
GENERATED_BODY()
public:
AMovingItem();
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Components")
USceneComponent* SceneRoot;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Components")
UStaticMeshComponent* StaticMeshComp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Properties")
float MoveSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Properties")
float MaxRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Properties")
FVector MoveDirection;
FVector StartLocation;
public:
virtual void Tick(float DeltaTime) override;
};
#include "MovingItem.h"
AMovingItem::AMovingItem()
{
PrimaryActorTick.bCanEverTick = true;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Star_B.SM_Star_B"));
if (MeshAsset.Succeeded()) {
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Star_B.M_Star_B"));
if (MaterialAsset.Succeeded()) {
StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
}
MoveSpeed = 200.0f;
MaxRange = 500.0f;
MoveDirection = FVector(1.0f, 0.0f, 0.0f);
}
void AMovingItem::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
if (!MoveDirection.IsNearlyZero()) {
MoveDirection.Normalize();
}
}
void AMovingItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (MoveSpeed > 0.0f) {
FVector CurrentLocation = GetActorLocation();
CurrentLocation += MoveDirection * MoveSpeed * DeltaTime;
SetActorLocation(CurrentLocation);
float DistanceFromStart = FVector::Distance(StartLocation, CurrentLocation);
if (DistanceFromStart >= MaxRange) {
MoveDirection = -MoveDirection;
}
}
}
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "RotatingItem.generated.h"
UCLASS()
class CH3_API ARotatingItem : public AActor
{
GENERATED_BODY()
public:
ARotatingItem();
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Components")
USceneComponent* SceneRoot;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Components")
UStaticMeshComponent* StaticMeshComp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Properties")
float RotationSpeed;
private:
FVector StartLocation;
};
#include "RotatingItem.h"
ARotatingItem::ARotatingItem()
{
PrimaryActorTick.bCanEverTick = true;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Coin_A.SM_Coin_A"));
if (MeshAsset.Succeeded()) {
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Coin_A.M_Coin_A"));
if (MaterialAsset.Succeeded()) {
StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
}
RotationSpeed = 90.0f;
}
void ARotatingItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!FMath::IsNearlyZero(RotationSpeed)) {
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}
}
Pawn 클래스 및 컴포넌트 구성
CapsuleComponent (또는 Box/Sphere 중 택 1)
SkeletalMeshComponent
SpringArmComponent
CameraComponent
MyPawn.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "MyPawn.generated.h"
class UCapsuleComponent;
class USkeletalMeshComponent;
class USpringArmComponent; // 스프링 암 관련 클래스 헤더
class UCameraComponent; // 카메라 관련 클래스 미리 선언
class UInputMappingContext; // IMC 관련 미리 선언
class UInputAction; // IA 관련 미리 선언
struct FInputActionValue; // Enhanced Input에서 액션 값을 받을 때 사용하는 구조체
UCLASS()
class CH3_API AMyPawn : public APawn
{
GENERATED_BODY()
public:
AMyPawn();
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; // 입력 바인딩을 처리할 함수
// 1. 컴포넌트 선언
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UCapsuleComponent* CapsuleComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USkeletalMeshComponent* MeshComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
USpringArmComponent* SpringArmComp; // 스프링 암 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComp; // 카메라 컴포넌트
// 2. 입력 매핑 및 액션 선언
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputMappingContext* InputMappingContext; // 에디터에서 세팅할 IMC
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* MoveAction; // IA_Move를 지정할 변수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* LookAction; // IA_Look를 지정할 변수
// 3. 커스텀 이동/회전 속도
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MoveSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float RotationSpeed;
public:
virtual void Tick(float DeltaTime) override;
private:
void Move(const FInputActionValue& Value); // IA_Move를 처리할 함수 원형
void Look(const FInputActionValue& Value); // IA_Look를 처리할 함수 원형
};
#include "MyPawn.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Camera/CameraComponent.h" // 카메라 구현이 필요한 경우라서 include
#include "GameFramework/SpringArmComponent.h"// 스프링 암 구현이 필요한 경우라서 include
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h" // Enhanced Input System의 Local Player Subsystem을 사용
AMyPawn::AMyPawn()
{
PrimaryActorTick.bCanEverTick = true;
MoveSpeed = 600.0f;
RotationSpeed = 90.0f;
// 1. 컴포넌트 생성 및 계층 구조 설정
CapsuleComp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComp"));
SetRootComponent(CapsuleComp);
MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("MeshComp"));
MeshComp->SetupAttachment(RootComponent);
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm")); // 스프링 암 생성
SpringArmComp->SetupAttachment(RootComponent); // 스프링 암을 루트 컴포넌트에 부착
SpringArmComp->TargetArmLength = 300.0f; // 캐릭터와 카메라 사이의 거리 기본값 300으로 설정
SpringArmComp->bUsePawnControlRotation = false; // 커스텀 회전을 사용할 것이므로 false로 설정합니다.
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera")); // 카메라 컴포넌트 생성
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName); // SpringArmComponent에 끝부분 소켓 위치에다가 카메라를 붙여달라
CameraComp->bUsePawnControlRotation = false; // 카메라는 스프링 암의 회전을 따르므로 PawnControlRotation은 꺼둠
// 2. Physics 설정 (물리 대신 코드로 제어)
CapsuleComp->SetSimulatePhysics(false);
MeshComp->SetSimulatePhysics(false);
}
void AMyPawn::BeginPlay()
{
Super::BeginPlay();
// IMC 활성화 (Pawn 내부에서 Controller를 통해 로컬 플레이어 서브시스템에 접근합니다)
if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
if (InputMappingContext)
{
Subsystem->AddMappingContext(InputMappingContext, 0); // Subsystem을 통해 우리가 할당한 IMC를 활성화
}
}
}
}
void AMyPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent)) // Enhanced InputComponent로 캐스팅
{
if (MoveAction)
{
EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyPawn::Move); // 키가 눌려서 이벤트가 발생했을 때 Move를 연결시키겠다
}
if (LookAction)
{
EnhancedInput->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMyPawn::Look); // 이벤트랑 함수를 연결하는 핵심 코드
}
}
}
void AMyPawn::Move(const FInputActionValue& Value)
{
const FVector2D MoveInput = Value.Get<FVector2D>(); // Enhanced Input에서 액션 값을 2차원 벡터 형태로 입력받기
// 프레임 독립성을 보장하기 위한 DeltaTime 확보
float DeltaTime = GetWorld()->GetDeltaSeconds();
// 평면 상에서의 이동: LocalOffset을 사용하면 폰의 현재 Forward/Right 기준으로 이동하게 됩니다.
if (!FMath::IsNearlyZero(MoveInput.X)) // X값이 0이 아닐 때
{
AddActorLocalOffset(FVector(MoveInput.X * MoveSpeed * DeltaTime, 0.0f, 0.0f));
}
if (!FMath::IsNearlyZero(MoveInput.Y)) // Y값이 0이 아닐 때
{
AddActorLocalOffset(FVector(0.0f, MoveInput.Y * MoveSpeed * DeltaTime, 0.0f));
}
}
void AMyPawn::Look(const FInputActionValue& Value)
{
FVector2D LookInput = Value.Get<FVector2D>();
float DeltaTime = GetWorld()->GetDeltaSeconds();
// 좌우 마우스 입력(X)으로 Pawn 자체의 Yaw(수평 회전)를 회전시킵니다.
if (!FMath::IsNearlyZero(LookInput.X))
{
AddActorLocalRotation(FRotator(0.0f, LookInput.X * RotationSpeed * DeltaTime, 0.0f));
}
// 상하 마우스 입력(Y)으로 SpringArm의 Pitch(수직 회전)를 회전시킵니다. (캡슐 전체가 고개 숙이는 것 방지)
if (!FMath::IsNearlyZero(LookInput.Y))
{
SpringArmComp->AddLocalRotation(FRotator(LookInput.Y * RotationSpeed * DeltaTime, 0.0f, 0.0f));
}
}
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "MyGameMode.generated.h"
UCLASS()
class CH3_API AMyGameMode : public AGameMode
{
GENERATED_BODY()
public:
AMyGameMode();
};
#include "MyGameMode.h"
#include "MyPawn.h" // 폰 클래스 헤더 추가
AMyGameMode::AMyGameMode()
{
DefaultPawnClass = AMyPawn::StaticClass(); // 클래스 정보를 넘겨서 저장을 해준다
}
Pawn 클래스 기반으로 구현하기 위해 없었던 기능들을 사용해야 했음
- UCapsuleComponent / USkeletalMeshComponent
이유 : Character 클래스는 이미 캡슐과 메쉬가 내부적으로 완성되어 있지만, Pawn 클래스는 아무것도 없으므로 형태를 잡아주고 충돌을 처리할 컴포넌트들을 직접 #include하고 생성해야함
- SetSimulatePhysics(false)
이유: 요구사항에 "Simulate Physics를 false로 설정"하라는 조건이 있어 추가.코드(Move, Look 함수)로만 다루겠다는 뜻
- GetWorld()->GetDeltaSeconds()
이유 : Tick(float DeltaTime) 함수의 매개변수나 언리얼의 CharacterMovementComponent가 알아서 프레임을 맞춰줬었는데 입력 이벤트 바인딩 함수(Move, Look) 안에서는 DeltaTime을 직접 가져와야 프레임 속도와 관계없이 일정한 속도와 회전량을 유지할 수 있음
- AddActorLocalOffset(FVector)
이유 : Character가 사용하는 AddMovementInput()은 Pawn에서 동작하지 않았음.따라서 지금 폰이 바라보는 방향을 기준으로 지정한 벡터 크기만큼 직접 이동시키라는 뜻으로 사용
- AddActorLocalRotation(FRotator) & AddLocalRotation()
이유 : 주의사항에 엔진 기본 회전 함수(AddControllerYawInput 등)를 쓰지 말라는 조건이 있어서 Pawn 액터 자체의 Yaw를 줘놓고, 카메라를 포함하고 있는 SpringArm의 Pitch을 주고 회전 제어함
트러블 슈팅....?
기존에 배웠던 IMC 처럼 IMC 들어가서 각각 IA들 매핑해주기
IA_Move(모두 Swizzle Input Axis Values 해주기)
W
S = XZY로 설정 & Negate로 입력값 뒤집기
A = Negate로 입력값 뒤집기
D
IA_Look - Mouse XY 2D-Axis & Negate로 입력값 뒤집기(Y만 체크)
이렇게 했는데 W를 눌렀을 때 앞으로 안 가고 오른쪽으로 이동하는 오류 생김
W 키: Modifier 아무것도 없음 (X축에 1이 들어와서 앞으로 이동)
S 키: Negate 1개 (X축에 -1이 들어와서 뒤로 이동)
D 키: Swizzle input axis values 1개 (X로 들어온 값을 Y축 1로 바꿔서 오른쪽으로 이동)
A 키: Swizzle input axis values + Negate 2개 (X로 들어온 값을 Y축 -1로 바꿔서 왼쪽으로 이동)
IA_Look : 처음 상태 그대로 돌려놓기