futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

2강: 캐릭터와 입력 시스템 #24

Closed fkdl0048 closed 4 months ago

fkdl0048 commented 4 months ago

2강: 캐릭터와 입력 시스템

액터와 컴포넌트

월드에 속한 콘텐츠의 기본 단위를 액터라고 한다. 액터는 트랜스폼을 가지며, 월드로부터 틱과 시간 서비스를 제공받는다. 이는 논리적개념일 뿐 컴포넌트를 감싸는 포장 박스에 불과하다. 실질적인 구현은 컴포넌트를 통해서 진행하고 대부분 다수의 컴포넌트를 가지고 있다. 다수의 컴포넌트를 대표하는 컴포넌트를 루트 컴포넌트라고 한다. 액터는 루트 컴포넌트를 기준으로 트랜스폼을 가지며 트랜스폼은 액터의 트랜스폼을 의미한다.

블루프린터로 액터 제작

C++액터에서 컴포넌트 제작

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AB_Fountain.generated.h"

UCLASS()
class ARENABATTLE_API AAB_Fountain : public AActor
{
 GENERATED_BODY()

public: 
 // Sets default values for this actor's properties
 AAB_Fountain();

protected:
 // Called when the game starts or when spawned
 virtual void BeginPlay() override;

public: 
 // Called every frame
 virtual void Tick(float DeltaTime) override;

 UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
 TObjectPtr<class UStaticMeshComponent> Body;

 UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
 TObjectPtr<class UStaticMeshComponent> Water;
};

아래 BodyWater는 객체타입을 에디터에서 보이게하고, 편집 가능영역을 말해준다. 또한, 블루프린트에서 읽기 쓰기가 가능하다. 카테고리는 Mesh로 설정되어 있다.

TObjectPtr라는 탬플릿 클래스를 사용

// Fill out your copyright notice in the Description page of Project Settings.

#include "Prop/AB_Fountain.h"
#include "Components//StaticMeshComponent.h"

// Sets default values
AAB_Fountain::AAB_Fountain()
{
  // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
 PrimaryActorTick.bCanEverTick = true;

 Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
 Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));

 RootComponent = Body;
 Water->SetupAttachment(Body);
 Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));

 static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
 if (BodyMeshRef.Object)
 {
  Body->SetStaticMesh(BodyMeshRef.Object);
 }

 static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
 if (WaterMeshRef.Object)
 {
  Water->SetStaticMesh(WaterMeshRef.Object);
 }
}

// Called when the game starts or when spawned
void AAB_Fountain::BeginPlay()
{
 Super::BeginPlay();

}

// Called every frame
void AAB_Fountain::Tick(float DeltaTime)
{
 Super::Tick(DeltaTime);

}

CreateDefaultSubobject를 통해 컴포넌트를 생성하고 초기화한다. (탬플릿 함수로 예제에서는 UStaticMeshComponent를 생성하고 있다.)

RootComponent는 루트 컴포넌트를 설정하고 그 하위인 WaterSetupAttachment를 통해 Body에 붙여준다. SetRelativeLocation를 통해 상대적인 위치를 설정해준다.

ConstructorHelpers를 통해 에디터에서 사용할 메쉬를 찾아서 설정해준다.

ConstructorHelpersFObjectFinder를 통해 찾아서 설정해주는데, UStaticMesh를 찾아서 설정해주고 있다. (Resource Load의 기능)

코드에 참조 주소값이 리터럴 값으로 들어가 있어서 위험해 보인다.

캐릭터의 제작

폰의 기능과 설계

폰이란, 액터를 상속받은 특별한 클래스이며, 플레이어가 빙의해 입출력을 처리하도록 설계되어 있다. 기능으론 길찾기를 사용할 수 있으며, 일반적으로 세 가지 주요 컴포넌트로 구성된다.

컴포넌트 중에서 트랜스폼이 없이 기능만 제공하는 컴포넌트를 액터컴포넌트라고 한다.

캐릭터의 기본 구조

캐릭터는 인간형 폰을 구성하도록 언리얼이 제공하는 전문 폰 클래스를 의미한다. 캐릭터는 세 가지 주요 컴포넌트로 구성되어 있다.

private:
 /** The main skeletal mesh associated with this Character (optional sub-object). */
 UPROPERTY(Category=Character, VisibleAnywhere, BlueprintReadOnly, meta=(AllowPrivateAccess = "true"))
 TObjectPtr<USkeletalMeshComponent> Mesh;

실제로 Character 클래스에서 위에서 C++로 커스텀한 컴포넌트와 같은 형태로 선언되어 있다.

C++ 캐릭터 생성 실습

UCLASS()
class ARENABATTLE_API AABCharacterBase : public ACharacter
{
 GENERATED_BODY()

public:
 // Sets default values for this character's properties
 AABCharacterBase();

};

생성자를 통해서만 초기화하기 위해 기본적인 코드는 삭제하고 진행한다. ACharacter를 상속받았기에 해당 클래스에 접근하여 데이터를 세팅한다.

#include "Character/ABCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"

// Sets default values
AABCharacterBase::AABCharacterBase()
{
 // Pawn
 bUseControllerRotationPitch = false;
 bUseControllerRotationYaw = false;
 bUseControllerRotationRoll = false;

 // Capsule
 GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
 GetCapsuleComponent()->SetCollisionProfileName(TEXT("Pawn"));

 // Movement
 GetCharacterMovement()->bOrientRotationToMovement = true;
 GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
 GetCharacterMovement()->JumpZVelocity = 700.f;
 GetCharacterMovement()->AirControl = 0.35f;
 GetCharacterMovement()->MaxWalkSpeed = 500.f;
 GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
 GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;

 // Mesh
 GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -100.0f), FRotator(0.0f, -90.0f, 0.0f));
 GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);
 GetMesh()->SetCollisionProfileName(TEXT("CharacterMesh"));

 static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple'"));
 if (CharacterMeshRef.Object)
 {
  GetMesh()->SetSkeletalMesh(CharacterMeshRef.Object);
 }

 static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("/Game/Characters/Mannequins/Animations/ABP_Quinn.ABP_Quinn_C"));
 if (AnimInstanceClassRef.Class)
 {
  GetMesh()->SetAnimInstanceClass(AnimInstanceClassRef.Class);
 }
}

너무 깊게 들어가지말고, 쉽게 ACharacter에 있는 멤버변수에 접근하여 세팅해주는 코드이다. 순서대로 앞서 말한 구조를 따라간다.

입력 시스템 개요

플레이어의 입력은 컨트롤러를 통해 폰으로 전달됨 (우선순위를 컨트롤러가 가진다.) 여기서 입력을 컨트롤러가 처리할 수도, 폰이 처리할 수도 있는데, 일반적으로는 폰이 처리하도록 설정

다양한 입력에 대해서 컨트롤러가 개개인의 폰에 대해서 처리하다보면 결합도가 높아지고 의존성이 방대해짐.

이번 강의에선 향상된 입력 시스템을 사용한다. 이는 사용자의 입력 설정 변경에 유연하게 대처할 수 있도록 구조를 재수립한다. 사용자의 입력 처리를 네 단계로 세분화하고 각 설정을 독립적인 에셋으로 대체한다. (원래는 유연한 입력을 대체하기 힘듬)

향상된 입력시스템 동작 구성

사용자가 기본적으로 입력을 진행하면 입력 매핑 컨텍스트라고 하는 것을 통해서 입력과 연결이 되고, 입력에 따라 액션이 수행된다. 이 액션에서 입력에 대해서 재가공할 것인지 결정하고 트리거를 활성화한다. 이후 게임로직에 맞게 수행된다.

게임로직의 부담을 줄여주고 (비즈니스 로직과 입력 로직을 분리) 유연한 입력 처리를 가능하게 한다.

유니티의 New Input System과 비슷한 구조로 보인다.

입력 시스템 실습

UCLASS()
class ARENABATTLE_API AABCharacterPlayer : public AABCharacterBase
{
 GENERATED_BODY()
public:
 AABCharacterPlayer();

protected:
 virtual void BeginPlay() override; // 입력 시스템 초기화를 위해 (캐스팅)

public:
 virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
  // 각 키에 대한 액션 바인딩 함수

// Input
protected:
 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputMappingContext> DefaultMappingContext;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> JumpAction;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> MoveAction;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> LookAction;

 void Move(const FInputActionValue& Value);
 void Look(const FInputActionValue& Value);

  // 각 액션에 대해서 UPROPERTY로 선언하여 CPP에서 접근할 수 있도록 설정 (액션과 키 바인딩을 위해)
  // 각 액션에 할당할 함수도 선언 (Enhaned Input System에서는 매개변수를 통해 입력값을 받아올 수 있다.)

// Camera
protected:
 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class USpringArmComponent> CameraBoom;

 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UCameraComponent> FollowCamera;
};
AABCharacterPlayer::AABCharacterPlayer()
{
 // Camera
 CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
 CameraBoom->SetupAttachment(RootComponent);
 CameraBoom->TargetArmLength = 400.0f;
 CameraBoom->bUsePawnControlRotation = true;

 FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
 FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
 FollowCamera->bUsePawnControlRotation = false;

 // Input
 static ConstructorHelpers::FObjectFinder<UInputMappingContext> InputMappingContextRef(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/ArenaBattle/Input/IMC_Default.IMC_Default'"));
 if (nullptr != InputMappingContextRef.Object)
 {
  DefaultMappingContext = InputMappingContextRef.Object;
 }
 else
 {
  UE_LOG(LogTemp, Error, TEXT("Failed to find IMC_Default"));

 }

 static ConstructorHelpers::FObjectFinder<UInputAction> InputActionMoveRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Move.IA_Move'"));
 if (nullptr != InputActionMoveRef.Object)
 {
  MoveAction = InputActionMoveRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UInputAction> InputActionJumpRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Jump.IA_Jump'"));
 if (nullptr != InputActionJumpRef.Object)
 {
  JumpAction = InputActionJumpRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UInputAction> InputActionLookRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Look.IA_Look'"));
 if (nullptr != InputActionLookRef.Object)
 {
  LookAction = InputActionLookRef.Object;
 }
}

void AABCharacterPlayer::BeginPlay()
{
 Super::BeginPlay();

 // 컨트롤러가 플레이어 컨트롤러 대상으로 설계된 전용 캐릭터 클래스이기에 castChecked를 사용하여 캐스팅합니다.
 APlayerController* PlayerController = CastChecked<APlayerController>(GetController());
 if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
 {
  Subsystem->AddMappingContext(DefaultMappingContext, 0);
  //Subsystem->RemoveMappingContext(DefaultMappingContext); // 런타임에 매핑 컨텍스트를 제거할 수도 있습니다.
 }

}

void AABCharacterPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
 Super::SetupPlayerInputComponent(PlayerInputComponent);

 UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);// 반드시 EnhancedInputComponent로 캐스팅해야 합니다.

 EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
 EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
 EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Move);
 EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Look);
 // 각 키 입력에 대한 액션을 바인딩합니다.
 // ex 점프 액션의 Triggered 이벤트에 Jump 함수를 바인딩합니다. Completed 이벤트에 StopJumping 함수를 바인딩합니다.
 // (점프는 `ACharacter` 클래스에 정의된 함수를 매핑하지만, Move와 Look는 `AABCharacterPlayer` 클래스로 연결되기에 직접 구현
}

void AABCharacterPlayer::Move(const FInputActionValue& Value)
{
 FVector2D MovementVector = Value.Get<FVector2D>();

 const FRotator Rotation = Controller->GetControlRotation();
 const FRotator YawRotation(0, Rotation.Yaw, 0);

 const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
 const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

 AddMovementInput(ForwardDirection, MovementVector.X);
 AddMovementInput(RightDirection, MovementVector.Y);
}

void AABCharacterPlayer::Look(const FInputActionValue& Value)
{
 FVector2D LookAxisVector = Value.Get<FVector2D>();

 AddControllerYawInput(LookAxisVector.X);
 AddControllerPitchInput(LookAxisVector.Y);
}

마찬가지로 ConstructorHelpers을 통해 사용할 Input 매핑 컨텍스트, 각 액션을 가져온다.

추가적으로 Action에선 Extension처럼 각 액션에 대한 처리를 확장할 수 있다.

정리