Infinite Fighter 개발일지 (2)

유영준·2023년 3월 19일
0

UE5 UNSEEN

목록 보기
2/18
post-thumbnail

2023.03.13 ~ 2023.03.19 개발일지

오늘은 캐릭터를 구축하는데 필요한 에셋과 애니메이션을 찾고, 이동을 구현해주었다

에셋 및 애니메이션 탐색 및 캐릭터 이동 구현

에셋 및 애니메이션 탐색

애자일한 방식으로 개발하기 위해 처음부터 모든 에셋을 다 구하는 방식을 택하지 않았다

2주동안 캐릭터의 움직임을 구현하고, 가능하다면 공격까지 구현하는 것을 목표로 잡았기에 먼저는 무기 에셋, 이동 모션, 공격 모션 등을 탐색했다

무기 에셋

갓 오브 워 like 를 표방하는 만큼, 무기는 갓오브워의 리바이어던 도끼를 사용했다

이는 Sketchfab 에서 쉽게 구할 수 있었다

Sky_Hunter의 리바이어던 도끼


이동 모션

다음으로는 이동모션을 탐색했다 이동에는 걷기, 조깅, 스프린트, 실드를 든채로 걷기 등의 애니메이션이 필요했고, 캐릭터가 이동중일때는 정면 방향을 바라보게 구현할 예정이었기에 하체만 움직이는 방식이 필요했다

이동의 대부분은 Advanced locomotion system 을 사용했고, 실드를 든채로 움직이는 모션은 파라곤의 스틸 캐릭터에서 가져왔다

LongmireLocomotion의 Advanced Locomotion System V4

Epic Games 의 Paragon: Steel


공격 모션

공격 모션에는 무기를 들고 있지 않을때 사용할 주먹을 이용한 공격과, 무기를 들고 있을때 휘두르는 모션 2종류가 필요했다 이번에도 파라곤의 힘을 빌렸다 주먹을 이용한 공격은 아까와 같은 Steel, 도끼 공격은 키메라 캐릭터에서 가져왔다

Epic Games의 Paragon: Khaimera


캐릭터 이동 구현

애니메이션 리타겟팅

Advanced locomotion system(이하 ALS)의 애니메이션은 기본적으로 UE4 마네킹과 스켈레톤을 공유한다 Paragon의 캐릭터들은 리타켓을 위한 파일 Rig을 보유하고 있다 Rig를 통한 리타켓팅 방식은 언리얼 엔진 4의 방식이기 때문에 언리얼 엔진 4에서 리타겟 후 추출해주었다(언리얼 엔진 5는 IKRetargeter 가 새롭게 생겼다)

UE4 마네킹의 스켈레톤을 열고 Retarget Manager에서 UE4 마네킹을 생성 후 선택한다

사용할 파라곤 에셋또한 UE4 마네킹을 선택해주도록 한다

리타겟 하고 싶은 애니메이션을 선택 후 리타겟 버튼을 눌러준다

이렇게 리타게팅한 애니메이션들은 추출 후 UE5 프로젝트에 임포트 시켜주었다


블랜드 스페이스를 통한 애니메이션 설정

언리얼 엔진에는 블랜드 스페이스라는 애니메이션을 설정 값에 따라 자동으로 변환해주는 기능이 존재한다 이를 통해 방향마다 이동모션을 넣는 방식이 아닌, 하나의 블랜드 스페이스에 모션을 넣어주고 특정 값마다 모션이 변하도록 설정해 줄 것이다

블랜드 스페이스는 애니메이션 카테고리에서 생성 가능하다

이때 2가지 축에 변수를 지정해 줄 수 있는데, Horizontal Axis에는 Direction을 지정해주고 -180에서 180의 Value를 가지고 하고, Vertical에는 Speed를 지정해주고 0 ~ 600의 Value 값을 가지게 했다

속도가 올라갈수록 움직임이 빨라지고, Direction에 따라 그쪽 방향을 가는 방식으로 구현을 할 예정이다 (-180일 시 뒤로 움직이도록 구현)

각각의 애니메이션을 위치에 맞게 추가해주었다

속도 0에 Idle 자세, 50에 걷기, 400에 뛰기, 600에 질주하는 모션을 넣어주었다

컨트롤을 누르고 마우스를 움직이면 블랜딩이 어떻게 작동하는지 볼 수 있는데 결과물은 이와 같다


무기 애니메이션과 확인하기

먼저 받은 fbx 파일을 프로젝트에 임포트해주었다

임포트하게 되면 텍스처 정보가 연결되어있지 않게 되는데, 텍스처 파일또한 임포트 하고 연결해주면 된다

언리얼 엔진의 기본 마네킹에는 무기를 장착할 수 있는 소켓이 존재한다 이 소켓에 무기를 추가할 것인데, 스켈레톤에서 임시로 미리보기를 할 수 있다

Weapon R 소켓에 무기를 추가해보자

추가 후 소켓의 위치를 적절하게 조절하며 자연스러운 위치를 찾아주면 된다

내 경우에는 Relative location (X=-13.5, Y=4.5, Z=8.5) / Relative Rotation (Pitch=-14, Yaw=170, Roll=-1) 의 값을 가진다


캐릭터 프로그래밍

캐릭터 클래스를 상속받는 클래스 IFCharacter를 생성해주었다 일단은 기본적인 input과 카메라, 스프링암을 선언해주었고, input 액션이 할당될 함수들을 생성해주었다. 이때 헤더에는 최대한 #include를 사용하지 않고, 전방선언을 해주었다

IFCharacter.h

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "IFCharacter.generated.h"

UCLASS()
class INFINITEFIGHTER_API AIFCharacter : public ACharacter
{
	GENERATED_BODY()

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

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

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

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

private:
	UPROPERTY(VisibleAnywhere, Category = Input)
	class UInputMappingContext* DefaultContext;

	UPROPERTY(VisibleAnywhere, Category = Input)
	class UInputAction* MoveAction;

	UPROPERTY(VisibleAnywhere, Category = Input)
	class UInputAction* LookAction;

	UPROPERTY(VisibleAnywhere, Category = Input)
	class UInputAction* SprintAction;

	UPROPERTY(VisibleAnywhere, Category = Camera)
	class UCameraComponent* Camera;

	UPROPERTY(VisibleAnywhere, Category = Camera)
	class USpringArmComponent* SpringArm;

	void Move(const FInputActionValue& Value);

	void Look(const FInputActionValue& Value);

	void Sprint();
};

c++ 파일에서는 ObjectFinder를 통해 소스에서 파일을 불러왔고, 부드러운 카메라의 전환을 위해 스프링암에 Lag를 설정해주었다 Input은 Enhanced Input System을 사용해주었다

IFCharacter.cpp

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

#include "IFCharacter.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"

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

	// Setting the inputs
	static ConstructorHelpers::FObjectFinder<UInputMappingContext>IMC_DEFAULT
	(TEXT("/Game/InFiniteFighter/Input/IMC_Default.IMC_Default"));
	if (IMC_DEFAULT.Succeeded())
		DefaultContext = IMC_DEFAULT.Object;

	static ConstructorHelpers::FObjectFinder<UInputAction>IA_MOVE
	(TEXT("/Game/InFiniteFighter/Input/Actions/IA_Move.IA_Move"));
	if (IA_MOVE.Succeeded())
		MoveAction = IA_MOVE.Object;

	static ConstructorHelpers::FObjectFinder<UInputAction>IA_LOOK
	(TEXT("/Game/InFiniteFighter/Input/Actions/IA_Look.IA_Look"));
	if (IA_LOOK.Succeeded())
		LookAction = IA_LOOK.Object;

	static ConstructorHelpers::FObjectFinder<UInputAction>IA_SPRINT
	(TEXT("/Game/InFiniteFighter/Input/Actions/IA_Sprint.IA_Sprint"));
	if (IA_SPRINT.Succeeded())
		SprintAction = IA_SPRINT.Object;

	// creating parts for character (springarm, camera)
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SPRING_ARM"));
	SpringArm->SetupAttachment(GetMesh(), "spine_03");

	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("CAMERA"));
	Camera->SetupAttachment(SpringArm);

	// positioning the skeletal mesh
	GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -90.0f), FRotator(0.0f, -90.0f, 0.0f));

	// setting spring arm
	SpringArm->ProbeSize                = 16.0f;
	SpringArm->TargetArmLength          = 150.0f;
	SpringArm->SocketOffset             = FVector(0.0f, 60.0f, 10.0f);
	SpringArm->bUsePawnControlRotation  = true;
	SpringArm->bInheritPitch		    = true;
	SpringArm->bInheritYaw			    = true;
	SpringArm->bInheritRoll			    = true;
	// enabling camera lag for a smoother look
	SpringArm->bEnableCameraLag		    = true;
	SpringArm->bEnableCameraRotationLag = true;
	SpringArm->CameraLagMaxDistance     = 40.0f;
	SpringArm->CameraLagSpeed           = 5.0f;
	SpringArm->CameraRotationLagSpeed   = 5.0f;
	bUseControllerRotationYaw           = false;

	GetCharacterMovement()->MaxWalkSpeed	      		  = 400.0f;
	GetCharacterMovement()->bOrientRotationToMovement	  = false;
	GetCharacterMovement()->bUseControllerDesiredRotation = true;
	GetCharacterMovement()->RotationRate				  = FRotator(0.0f, 480.0f, 0.0f);

	// setting the mesh and animation
	static ConstructorHelpers::FObjectFinder<USkeletalMesh>SKM_MESH
	(TEXT("/Game/InFiniteFighter/Characters/Mannequin_UE4/Meshes/SK_Mannequin.SK_Mannequin"));
	if (SKM_MESH.Succeeded())
		GetMesh()->SetSkeletalMesh(SKM_MESH.Object);

	GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);

	static ConstructorHelpers::FClassFinder<UAnimInstance>CHARACTER_ANIM
	(TEXT("/Game/InFiniteFighter/Characters/IFCharacterAnimBlueprint.IFCharacterAnimBlueprint_C"));
	if (CHARACTER_ANIM.Succeeded())
		GetMesh()->SetAnimInstanceClass(CHARACTER_ANIM.Class);
}

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

	// Adding mapping context to Enhanced Input Subsystem
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* SubSystem =
			ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
			SubSystem->AddMappingContext(DefaultContext, 0);
	}
}

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

	// stops Sprint once character is slowed down
	// 속도가 줄어들때 call 되는 함수를 찾아보자.. Tick 에 넣는거 너무 느낌 없다
	if (GetVelocity().Size() <= 400.0f)
		GetCharacterMovement()->MaxWalkSpeed = 400.0f;

}

// Called to bind functionality to input
void AIFCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EnhancedInputComponent->BindAction(MoveAction,   ETriggerEvent::Triggered, this, &AIFCharacter::Move);
		EnhancedInputComponent->BindAction(LookAction,   ETriggerEvent::Triggered, this, &AIFCharacter::Look);
		EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Triggered, this, &AIFCharacter::Sprint);
	}

}

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

	GetCharacterMovement()->bOrientRotationToMovement = false;
	GetCharacterMovement()->bUseControllerDesiredRotation = true;

	if (Controller != nullptr)
	{
		// find out which way is forward
		const FRotator Rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);

		// get forward vector
		const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

		// get right vector 
		const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

		// scale movement input based on desired speed
		const FVector ScaledMovementInput = (ForwardDirection * MovementVector.Y + RightDirection * MovementVector.X).GetSafeNormal();

		// add movement 
		// Input에 따른 변환이 있도록 수정(키보드와 패드 구분해서 적용)
		if (MovementVector.Size() < 2)
			AddMovementInput(ScaledMovementInput);
		else
			AddMovementInput(ScaledMovementInput, MovementVector.Size() / 50);

	}
}

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

	// Camera can rotate around the character only when character is not moving
	if (GetVelocity().Size() == 0.0f)
	{
		GetCharacterMovement()->bOrientRotationToMovement     = true;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
	}
	else
	{
		GetCharacterMovement()->bOrientRotationToMovement     = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = true;
	}

	// Adding Camera movement
	if (Controller != nullptr)
	{
		AddControllerYawInput(LookAxisVector.X);
		AddControllerPitchInput(LookAxisVector.Y);
	}
}

void AIFCharacter::Sprint()
{
	GetCharacterMovement()->MaxWalkSpeed = 600.0f;
}

다음은 AnimInstance 클래스를 만들어 아까 블랜드 스페이스때 만들어둔 변수 Speed와 Direction에 설정해줄 값을 만든다

IFCharacterAnimInstance.h

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

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "IFCharacterAnimInstance.generated.h"

/**
 * 
 */
UCLASS()
class INFINITEFIGHTER_API UIFCharacterAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
public:
	UIFCharacterAnimInstance();
	virtual void NativeUpdateAnimation(float DeltaSeconds) override;

private:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
	float CharacterDirection;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
	float CharacterSpeed;
};

이때 변수들을 애니메이션 블루프린트를 통해 조작해야하기 때문에 BlueprintReadOnly, Meta=(AllowPrivateAccess=true) 를 넣어주었다

IFCharacterAnimInstance.cpp

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

#include "IFCharacterAnimInstance.h"

UIFCharacterAnimInstance::UIFCharacterAnimInstance()
{
	CharacterDirection = 0.0f;
	CharacterSpeed	   = 0.0f;
}

void UIFCharacterAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeUpdateAnimation(DeltaSeconds);

	auto Pawn = TryGetPawnOwner();

	if (::IsValid(Pawn))
	{
		// get speed
		CharacterSpeed     = Pawn->GetVelocity().Size();

		// get direction
		CharacterDirection = this->CalculateDirection(Pawn->GetVelocity(), Pawn->GetActorRotation());
	}
}

두 변수는 블랜더 스페이스에 연결해 캐릭터가 움직이는 값으로 넣어주었다

마지막으로 게임모드에서 방금 만든 IFCharacter 클래스가 기본 캐릭터가 되도록 설정해주었다

현재까지 제작 상태 (움짤 프레임을 너무 낮게해서 어지럽다….)

개선점

  • 먼저 멈추게 되었을때 애니메이션이 살짝 부자연스럽게 멈추게 된다 이 부분을 조금 더 공부하고 개선하고 싶다
  • 불필요하게 계속 검증을 하도록 동작된 코드들이 있다 (Tick에서 지속적으로 속도 체크, Look함수에서 이동중인지 아닌지 체크) 이것들을 그 순간에만 호출되는 함수를 만들던 찾던 해서 개선 할 수 있을 것 같다
  • 현재 게임패드로 입력을 받으면 MovementVector의 사이즈가 최대 50이 나오고, 키보드로 입력하게 되면 1로만 나오게 된다 그래서 MovementVector의 사이즈의 차이를 두고 입력값을 다르게 받도록 임시방편으로 두었는데, 이를 키보드를 입력받을 시, 게임패드를 입력받을 시 상황을 나누도록 하고 싶다 (Common Input Subsystem에 있는 GetCurrentInputType같은… 아무리 찾아도 Enhanced Input 에는 없는거같다)

다음주에 할 일

  • 이번주의 내용 다듬기
  • 납도, 발도 모션 추가
  • 공격모션 추가
profile
토비폭스가 되고픈 게임 개발자

0개의 댓글