2023.05.01 ~ 2023.05.07 개발일지
Build 파일에 PublicIncludePaths.AddRange
를 통해 경로를 추가하고, C++클래스들을 폴더 별로 나누어 정리하였다
InfiniteFighter.Build.cs
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class InfiniteFighter : ModuleRules
{
public InfiniteFighter(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(new string[] { "InfiniteFighter" });
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore",
"HeadMountedDisplay", "EnhancedInput", "AnimGraphRuntime", "CommonInput", "UMG" });
}
}
비주얼 스튜디오와 엔진 두 곳에서 모두 잘 적용되는 것을 확인하였다
이전까지는 게임모드에서 캐릭터를 불러올때 헤더파일을 참조하여 불러오는 방식을 사용했는데,
캐릭터 클래스가 커짐에 따라 컴파일 시간을 줄이기 위해 헤더 파일을 참조하지 않고 ClassFinder를 통해 불러오게 되었다
IFGameMode.cpp
#include "IFGameMode.h"
AIFGameMode::AIFGameMode()
{
static ConstructorHelpers::FClassFinder<APawn> ThirdPersonClassRef
(TEXT("/Script/InfiniteFighter.IFCharacter"));
if(ThirdPersonClassRef.Succeeded())
DefaultPawnClass = ThirdPersonClassRef.Class;
}
특정 상황이 되었을때, 캐릭터가 적을 한번에 처형 시키는 기술을 구현하고자 하였다
먼저 애니메이션을 준비했다
밟는 대상이 캐릭터, 밟히는 대상을 적으로 설정하여 몽타주를 실행시켜줄 예정이다
이때 둘 사이의 거리는 75만큼의 차이가 나고 캐릭터와 적이 같은 시간에 지정된 위치에 애니메이션을 실행하여야 했다
이를 위해 언리얼 엔진의 모션 워핑을 사용하였다
모션 워핑을 적용하기 위해서는 먼저 플러그인과 모듈을 추가해주어야 한다
에디터에서 모션 워핑을 추가해주고, Build 파일에서 MotionWarping
을 추가해준다
다음은 적 캐릭터를 구축하였다
이때 처형 모션을 할 수 있는 영역인 WarpCollision
을 설정하고,
그 영역 안에서 처형 모현을 실행할 때 이동할 지점인 WarpPoint
를 설정했다
또 캐릭터에는 같이 모션을 플레이할 Target이라는 변수를 두고, WarpCollision에 들어가면 캐릭터의 Target이 본인이 되도록 지정했다
마지막으로 애니메이션을 실행할때는 실행중 매쉬나 콜리전이 겹쳐지는 것을 방지하기 위해 적의 Collision 세팅을 바꿔주었다
AIFEnemy.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/IFEnemy.h"
#include "Components/BoxComponent.h"
#include "Components/SceneComponent.h"
#include "Components/CapsuleComponent.h"
#include "Character/IFCharacter.h"
#include "IFEnemyAnimInstance.h"
// Sets default values
AIFEnemy::AIFEnemy()
{
// 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;
GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -90.0f), FRotator(0.0f, -90.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>ENEMY_ANIM
(TEXT("/Game/InFiniteFighter/AI/EnemyAnimBlueprint.EnemyAnimBlueprint_C"));
if (ENEMY_ANIM.Succeeded())
GetMesh()->SetAnimInstanceClass(ENEMY_ANIM.Class);
// setting the point and box for motion warping
WarpPoint = CreateDefaultSubobject<USceneComponent>(TEXT("WARP_POINT"));
WarpPoint->SetRelativeLocation(FVector(75.0f, 0.0f, 0.0f));
WarpPoint->SetupAttachment(RootComponent);
WarpCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("WARP_COLLISION"));
WarpCollision->SetRelativeLocation(FVector(125.0f, 0.0f, 0.0f));
WarpCollision->SetBoxExtent(FVector(75.0f, 75.0f, 32.0f));
WarpCollision->SetupAttachment(RootComponent);
}
// Called when the game starts or when spawned
void AIFEnemy::BeginPlay()
{
Super::BeginPlay();
WarpCollision->OnComponentBeginOverlap.AddDynamic(this, &AIFEnemy::OverlapBegin);
WarpCollision->OnComponentEndOverlap. AddDynamic(this, &AIFEnemy::OverlapEnd);
}
void AIFEnemy::PostInitializeComponents()
{
Super::PostInitializeComponents();
AnimInstance = Cast<UIFEnemyAnimInstance>(GetMesh()->GetAnimInstance());
}
void AIFEnemy::OverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
AIFCharacter* PlayerCharacter = Cast<AIFCharacter>(OtherActor);
// If the cast is successful and the player's Target variable is not already set
if (PlayerCharacter != nullptr && PlayerCharacter->Target == nullptr)
{
// Set the player's Target variable to be this enemy
PlayerCharacter->Target = this;
}
}
void AIFEnemy::OverlapEnd(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
AIFCharacter* PlayerCharacter = Cast<AIFCharacter>(OtherActor);
// If the cast is successful and the player's Target variable is set to this
if (PlayerCharacter != nullptr && PlayerCharacter->Target == this)
{
// Set the player's Target variable to be nullptr
PlayerCharacter->Target = nullptr;
}
}
void AIFEnemy::PlayExecuteVictim()
{
// set capsule to ignore pawn so it doesn't go throw floor
GetCapsuleComponent()->SetCollisionProfileName("IgnoreOnlyPawn");
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// set mesh to overlap all so it doesn't block anything
GetMesh()->SetCollisionProfileName("OverlapAll");
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
AnimInstance->PlayExecuteVictimMontage();
}
캐릭터에는 모션 워핑이 가능하게 하는 모션 워핑 컴포넌트와 그 헤더를 추가시켜주었다
애니메이션 몽타주에서는 모션워핑이 일어날 구간을 설정해주어야 한다
초반부에 잡는 지점까지를 모션 워핑으로 두고, 타겟을 지정해주었다
타겟이 없으면 모션 워핑이 제대로 되지 않는다
Execute
함수를 실행 시킬때는 AddOrUpadteWarpTargetFromTransform을 통해 우리가 지정한 WarpPoint의 값을 받아와 이동하게 구현하였다
AIFCharacter.cpp
void AIFCharacter::Execute()
{
if (Target != nullptr)
{
Sheathe();
AnimInstance->SetAxeHolding(false);
AnimInstance->SetDrawState(false);
AnimInstance->PlayExecuteMontage();
Target->PlayExecuteVictim();
MotionWarpingComponent->AddOrUpdateWarpTargetFromTransform(TEXT("Target"), Target->WarpPoint->GetComponentTransform());
LevelSequencePlayer->Play();
}
}
여기까지 실행했을 때, 두 캐릭터가 합을 맞추며 애니메이션이 잘 나갔지만 카메라가 캐릭터의 뒤를 비추고 있고,
애니메이션이 실행되는 도중에 캐릭터가 움직일 수 있었기에 의도한 대로의 모습이 나오지 않았다
그래서 애니메이션이 실행되는 과정을 컷씬으로 만들며 자연스럽게 게임플레이와 컷씬을 연결시키기로 했다
이를 위해 시퀀서를 사용하였다
레벨 시퀀스를 생성하고, 카메라 트랙을 추가해주었다
카메라를 우클릭 후 Spawnable
에서 Possessable
로 바꾸고,
캐릭터의 자식으로 배치함과 동시에 위치를 현재 카메라의 위치로 두었다
Camera Cut 옵션에서 Can Blend 옵션을 활성화해 이전 카메라에서 시네카메라로 자연스럽게 블랜딩 되도록 설정해주었다
상단에 작은 삼각형을 눌러 블랜딩을 조절할 수 있다
이후에는 카메라의 키 프레임을 추가하며 박력있는 카메라 무브먼트를 만들었다
이렇게 처음 시퀀서를 만들게 되면, 카메라 액터 말고도 시퀀서를 실행해주는 액터가 생성되는데,
우리는 코드에서 실행할 것이기 때문에 그 액터는 삭제해 주었다
코드를 통해 시퀀서를 호출하려면 먼저 두가지 모듈 LevelSequence
와 MovieScene
을 추가해주어야 한다
시퀀서는 실질적으로 플레이되는 트랙인 LevelSequence
와 시퀀스를 실행시키는 LevelSequencePlayer
가 필요하다
두가지를 모두 캐릭터 헤더에 추가시켜주고, LevelSequence
는 ObjectFinder
를 통해 만들어둔 시퀀스를 할당하고,
LevelSequencePlayer
는 ULevelSequencePlayer의 함수인 CreateLevelSequencePlayer
를 통해 생성해주었다
AIFCharacter.cpp
void AIFCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
...
FMovieSceneSequencePlaybackSettings Settings;
Settings.bDisableLookAtInput = true;
Settings.bDisableMovementInput = true;
Settings.bHideHud = true;
ALevelSequenceActor* SequenceActor;
LevelSequencePlayer = ULevelSequencePlayer::CreateLevelSequencePlayer(GetWorld(), LevelSequence, Settings, SequenceActor);
}
시퀀서를 설정할 때 Setting 항목에서 인풋과 UI를 감추는 설정을 추가해주었다
마지막으로 완성된 모습을 보자