강의를 통해 배운 내용을 바탕으로 나만의 게임을 만들어보자.
게임의 기획은 마치 엘든링과 같은 소울류로 제작할 계획이다.
기본 공격과 스킬들로 적들을 처지하고 최종 보스를 처치하면 엔딩이 나오는 것으로 마무리 할 계획이다.
밸런싱의 경우 그때마다 테스트 해보고 정하도록 하자.(기획자가 없기에...)
에셋
캐릭터 및 배경 같은 경우는 사실상 에셋을 따로 구해와야만 한다. 이 작업부터 진행하기엔 효율이 많이 떨어지고 모델링 부터 다시 배워야하기 때문이다.
에픽게임즈에서 제공하는 기본 에셋 중 맘에 드는것이 있어 이것을 활용하겠다.
https://www.fab.com/ko/listings/7d76ddf0-d9ce-4d00-939e-d72793534d01
https://www.fab.com/ko/listings/0fb3c8bf-27b0-4522-b111-52c117634681
기본 캐릭터와 배경은 위의 링크를 통해 프로젝트에 넣어준다.
캐릭터
에셋안에 들어있는 블루프린터와 애니메이션 블루프린터를 이용하면 쉽게 구현이 가능하겠지만, 그렇게 된다면 공부하는 의미가 없기 때문에 말 그대로 Mesh와 Animation만 이용해서 제작할 것이다.
구조를 다음과 같이 구성할 예정이다.
일단 움직이는 모든 객체는 APMCharacterBase를 상속받고 이는 Character 클래스이다.
그리고 Player는 APMPlayer로 Monster는 APMMonster로 기본적으로 상속받고 시작한다.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/PMCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
// Sets default values
APMCharacterBase::APMCharacterBase()
{
// 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;
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = true;
GetCapsuleComponent()->InitCapsuleSize(36.f, 83.f);
GetCapsuleComponent()->SetCollisionProfileName(TEXT("PMCapsule"));
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->JumpZVelocity = 500.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->SetWalkableFloorAngle(45.f);
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -88.0f), FRotator(0.0f, -90.0f, 0.0f));
GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);
GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));
}
그리고 Player의 같은 경우 카메라와 자신의 SkeletalMesh를 지정해주어야한다.
또한 입력을 받아줘야하기 때문에 함수들을 바인딩 해주자.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/PMPlayer.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "InputMappingContext.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
APMPlayer::APMPlayer()
{
static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/ParagonSparrow/Characters/Heroes/Sparrow/Skins/Rogue/Meshes/Sparrow_Rogue.Sparrow_Rogue'"));
if (CharacterMeshRef.Object)
{
GetMesh()->SetSkeletalMesh(CharacterMeshRef.Object);
}
static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("/Script/Engine.AnimBlueprint'/Game/ProjectM/Animation/BP_PMPlayerAnimation.BP_PMPlayerAnimation_C'"));
if (AnimInstanceClassRef.Class)
{
GetMesh()->SetAnimInstanceClass(AnimInstanceClassRef.Class);
}
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 700.f;
CameraBoom->bUsePawnControlRotation = true;
CameraBoom->TargetOffset = FVector(0.f, 0.f, 120.f);
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom);
FollowCamera->bUsePawnControlRotation = false;
static ConstructorHelpers::FObjectFinder<UInputMappingContext> MappingContextRef(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/ProjectM/Input/IMC_PlayerInput.IMC_PlayerInput'"));
if (MappingContextRef.Object)
{
MappingContext = MappingContextRef.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> MoveActionRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ProjectM/Input/Actions/IA_Move.IA_Move'"));
if (MoveActionRef.Object)
{
MoveAction = MoveActionRef.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> LookActionRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ProjectM/Input/Actions/IA_Look.IA_Look'"));
if (LookActionRef.Object)
{
LookAction = LookActionRef.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> JumpActionRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ProjectM/Input/Actions/IA_Jump.IA_Jump'"));
if (JumpActionRef.Object)
{
JumpAction = JumpActionRef.Object;
}
}
void APMPlayer::BeginPlay()
{
Super::BeginPlay();
APlayerController* PlayerController = Cast<APlayerController>(GetController());
if (PlayerController)
{
EnableInput(PlayerController);
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->ClearAllMappings();
Subsystem->AddMappingContext(MappingContext, 0);
}
}
}
void APMPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInput = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APMPlayer::Move);
EnhancedInput->BindAction(LookAction, ETriggerEvent::Triggered, this, &APMPlayer::Look);
}
void APMPlayer::Move(const FInputActionValue& Value)
{
// 현재 입력값을 저장
TargetMovementInput = Value.Get<FVector2D>();
// 입력값 보간
CurrentMovementInput = FMath::Vector2DInterpTo(CurrentMovementInput, TargetMovementInput, GetWorld()->GetDeltaSeconds(), InterpolationSpeed);
const FRotator Rotation = GetController()->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, CurrentMovementInput.X);
AddMovementInput(RightDirection, CurrentMovementInput.Y);
}
void APMPlayer::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
const float LookSensitivity = 0.7f; // 감도 조정
AddControllerYawInput(LookAxisVector.X * LookSensitivity);
AddControllerPitchInput(LookAxisVector.Y * LookSensitivity);
}
캐릭터는 이렇게 하면 상하좌우 카메라 움직임까지 전부 다 할 수 있게 된다.
이제 게임모드에서 Defualt를 설정해주면 된다.
애니메이션
나는 캐릭터를 움직일 때, a를 누른다고 왼쪽을 바라보게 만들지 않을 것이다. a를 누르면 캐릭터 기준 왼쪽으로 움직이는 것은 맞지만 여전히 ControlRotation에 따라 방향을 바라보고 있다.
따라서 애니메이션도 상하좌우로 움직일 때를 맞춰 재생해줘야한다.
BlendSpace를 이용해 애니메이션을 선택하도록 하자.
그것을 하기 위해서는 각종 정보들을 AnimInstance로 부터 가져와야한다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "PMPlayerAnimInstance.generated.h"
/**
*
*/
UCLASS()
class PROJECTM_API UPMPlayerAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
UPMPlayerAnimInstance();
protected:
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
TObjectPtr<class APMPlayer> Owner;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
TObjectPtr<class UCharacterMovementComponent> Movement;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
FVector Velocity;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
float Speed;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
uint8 IsInAir : 1;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
float Pitch;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
float Roll;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
float Yaw;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
FRotator RotationLastTick;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
float YawDelta;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
uint8 IsAccelerating : 1;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
uint8 IsAttacking : 1;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
int32 CurrentAttacking;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
uint8 FullBody : 1;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
float MoveDirectionX;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Character)
float MoveDirectionY;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Animation/PMPlayerAnimInstance.h"
#include "GameFramework/Character.h"
#include "Character/PMPlayer.h"
#include "GameFramework/CharacterMovementComponent.h"
UPMPlayerAnimInstance::UPMPlayerAnimInstance()
{
YawDelta = 0.f;
}
void UPMPlayerAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
Owner = Cast<APMPlayer>(GetOwningActor());
if (Owner)
{
Movement = Owner->GetCharacterMovement();
}
}
void UPMPlayerAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
// IsValid
if (Owner == nullptr) return;
// Set IsInAir
IsInAir = Movement->IsFalling();
// Set Speed
Velocity = Movement->Velocity;
Speed = Velocity.Length();
// Set Roll Pith Yaw
{
FRotator BAR = Owner->GetBaseAimRotation();
FRotator CurrentRotation = Owner->GetActorRotation();
FRotator Delta = (BAR - CurrentRotation).GetNormalized();
Pitch = Delta.Pitch;
Yaw = Delta.Yaw;
Roll = Delta.Roll;
}
// Setting Yaw Delta for Leans
{
FRotator CurrentRotation = Owner->GetActorRotation();
FRotator Delta = (RotationLastTick - CurrentRotation).GetNormalized();
float TempYaw = Delta.Yaw;
YawDelta = FMath::FInterpTo(YawDelta, ((TempYaw / DeltaSeconds) / 12), DeltaSeconds, 6.f);
RotationLastTick = CurrentRotation;
}
// Set Accelerating
{
float Acclerate = Movement->GetCurrentAcceleration().Length();
IsAccelerating = Acclerate > 0;
}
// Set IsFullBody
{
FullBody = GetCurveValue(TEXT("FullBody")) > 0;
}
if (!Velocity.IsNearlyZero())
{
// Velocity To Local Space
FVector LocalMovementDirection = Owner->GetActorTransform().InverseTransformVector(Velocity.GetSafeNormal());
// Set LocalMovementDirection X Y
MoveDirectionY = LocalMovementDirection.Y;
MoveDirectionX = LocalMovementDirection.X;
UE_LOG(LogTemp, Log, TEXT("Local Movement Direction: %s"), *LocalMovementDirection.ToString());
}
}
이렇게 매 프레임마다 애니메이션에 사용될 값들을 갱신해준다.
애님 그래프는 간단하다.
지금은 점프 애니메이션을 고려하지 않았기 때문에 Ground일때만 Locomotion을 지정해줬다.
Jog 류는 전부 다 비슷한 Blend로 되어있다.
따라서 한 부분만 확인하자.
캐릭터를 기준으로 어느방향으로 이동하고 있는지 Y값과 X값을 이용해
lef right fwd bwd를 골라주면 된다.
이때, 애니메이션을 자연스럽게 하기 위해 조금씩 프레임을 remove해서 사용했다.
각각의 상태 변화는 다음 조건과 같다.
- Idle > JogStart : 공중에 있지 않으며, 가속도가 존재할 때
- JosStart > Jog : JogStart 애니메이션이 끝나면 자동으로 넘어감
- JogStart > JogStop : Speed 가 0 일 때
- Jog > JogStop : 가속도가 존재하지 않을 때
- JogStop > JogStart : 가속도가 존재할 때
- JogStop > Idle : JogStop 애니메이션이 끝나면 자동으로 넘어감
결과는 이러하다.
'Unreal Engine 5 > ProjectM' 카테고리의 다른 글
[UE5] ProjectM - 공격 애니메이션과 화살 발사 (0) | 2025.01.03 |
---|---|
[UE5] ProjectM - 점프와 타이머, 카메라 범위 제한 (1) | 2024.12.24 |