이번 섹션의 주된 목표는 다음과 같다.
- 레벨에 무한히 스폰하는 기믹을 제작한다.
- 이번 플레이 기믹 = 스폰과 애셋에 관해서 다뤄본다.
- 무한 맵 생성을 위한 기믹 액터의 설계한다
- 애셋 매니저를 활용한 애셋 관리 방법을 학습한다.
- 액터의 스폰과 약참조 포인터의 사용법을 실습한다.
스테이지
스테이지의 경우 플레이어와 NPC가 1:1로 겨루는 장소이다.
스테이지는 4개의 상태를 가지고 있으며 순서대로 진행된다.
- READY : 플레이어의 입장을 처리한다 ( 입장 대기 중 )
- FIGHT : 대전을 진행한다 (플레이어와 싸우는 NPC를 소환)
- REWARD : 대전 진행 후 보상을 받는 단계임 (플레이어가 이겼을 시에 아이템 상자 스폰)
- NEXT : 다음 스테이지로 이동을 처리한다. (플레이어가 다음으로 이동할 수 있도록 처리)
이러한 순서로 무한으로 진행될 예정이다.
준비 단계
여기서는 스테이지 중앙에 위치한 트리거를 준비할 것이다. 플레이어가 트리거에 진입하게 된다면 다음 단계로 이동하게 된다. Overlap 이벤트로 처리할 것이다.
대전 단계
플리어가 못 나가게 스테이지의 모든 문을 닫고서 대전을 NPC를 스폰한다. NPC가 사망할 경우 보상단계로 진입힌다.
보상 선택 단계
정해진 위치의 4개의 상자에서 아이템을 랜덤하게 생성하는 과정을 수행한다.
문제는 아이템은 랜덤하게 생성이 된다는 점이다.
랜덤성을 부여하는 방법으로 아이템을 반환하겠다는 것을 의미한다.
이 중 하나만 선택을 하게 된다면 ,알아서 다음으로 넘어가게 만들 예정이다.
기믹
- 스테이지 가운데에 설치한 트리거 볼륨을 감지처리하는 과정을 거칠 것임. (Overlap 이벤트에 따른 처리)
- 각 4개의 문에 설치한 4개의 트리거 볼륨을 감지처리하는 과정을 할 것임.
- 상태별로 설정할 문의 회전 설정을 할 것임
- 플레이어가 들어왔을 시, NPC를 스폰해주는 기능을 넣어줘야 한다.
- 대전이 끝나면 보상으로 주어질 아이템 상자의 스폰 기능을 또 넣어주기는 해야 함.
- 이제 내가 어느쪽으로 들어갔다고 전제한다면 이를 또 스폰해주는 기능이 필요함.
- 추가로 이제 NPC가 죽었다고 한다면, 이 아이템 상자들을 스폰할 수 있게끔 전환 처리 해야 함.
- 아이템 상자의 하나를 오버랩을 했다면 모두 없애주는 기능을 해야 함.
아이템 상자에 있어 랜덤하게 보상을 만들기 위해 에셋 매니저에 대해 알 필요가 있다.
애셋 매니저란?
언리얼이 제공하는 애셋을 관리하는 싱글톤 클래스를 의미한다.
이제 이 언리얼 엔진이 초기화가 될 때 제공이 되며, 애셋 정보를 요청해서 받을 수가 있다!
이 때 PrimaryAssetId를 통해서 프로젝트 내부에 애셋의 주소를 얻을 수가 있다.
지금까지는 레퍼런스의 복사를 통해서 애셋을 끌어오는 구조를 띄었었음.
그래서 수동으로 복붙하는 구조였는데,,,
이제는 이 ID를 통해서 애셋을 가져올 수가 있음. → 지정한 태그와 이름의 키로 구성이 되어있음.
(그렇다는 것은 애초에 이제 그냥 일일히 이름으로 끌고와서 안해도 되고 그냥 태그로 뚝딱 처리하면 된다는 것)
기믹 제작
ABStageGimmick 이라는 Actor를 만들어서 여기서 위에서 설계한 내용을 구현할 예정이다.
protected:
UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AlloPrivateAccess = "true"))
TObjectPtr<class UStaticMeshComponent> Stage;
UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AlloPrivateAccess = "true"))
TObjectPtr<class UBoxComponent> StageTrigger;
UFUNCTION()
void OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);
- RootComponent의 역할을 담당하게 될 Stage
- 스테이지에 입장을 하였을 시, 감지를 진행하게 스테이지 트리거
- 그 다음 우리가 이전에 아이템 박스에서 만들어 봤던 Overlap을 처리할 때 Dynamic으로 바인딩 할 함수를 선언한다.
//Gate Section
protected:
UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AlloPrivateAccess = "true"))
TMap<FName, TObjectPtr<class UStaticMeshComponent>> Gates;
UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
TArray<TObjectPtr<class UBoxComponent>> GateTriggers;
UFUNCTION()
void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);
다음으로는 이제 각 문별로 이제 각각 기믹을 설정을 해주는 것이 필요한데, 문에 달리게 될 StaticMesh와 트리거들을 달아두고 각 문에 Overlap을 하게 되었을 시에 이제 동작할 함수를 넣는다.
생성자에서 초기화 해주자.
AABStageGimmick::AABStageGimmick()
{
Stage = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Stage"));
RootComponent = Stage;
static ConstructorHelpers::FObjectFinder<UStaticMesh> StageMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Stages/SM_SQUARE.SM_SQUARE'"));
if (StageMeshRef.Object) {
Stage->SetStaticMesh(StageMeshRef.Object);
}
StageTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("StageTrigger"));
StageTrigger->SetBoxExtent(FVector(775.0f, 775.0f, 300.0f));
StageTrigger->SetupAttachment(Stage);
StageTrigger->SetRelativeLocation(FVector(0.0f, 0.0f, 250.0f));
StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
StageTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnStageTriggerBeginOverlap);
static FName GateSockets[] = { TEXT("+XGate"), TEXT("-XGate"), TEXT("+YGate"), TEXT("-YGate") };
static ConstructorHelpers::FObjectFinder<UStaticMesh> GateMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_GATE.SM_GATE'"));
for (FName GateSocket : GateSockets) {
//Door Creation
UStaticMeshComponent* Gate = CreateDefaultSubobject<UStaticMeshComponent>(GateSocket);
Gate->SetStaticMesh(GateMeshRef.Object);
Gate->SetupAttachment(Stage, GateSocket);
Gate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
Gate->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
Gates.Add(GateSocket, Gate);
//Door Trigger Creation
FName TriggerName = *GateSocket.ToString().Append(TEXT("Trigger"));
UBoxComponent* GateTrigger = CreateDefaultSubobject<UBoxComponent>(TriggerName);
GateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.0f));
GateTrigger->SetupAttachment(Stage, GateSocket);
GateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
GateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnGateTriggerBeginOverlap);
GateTrigger->ComponentTags.Add(GateSocket);
GateTriggers.Add(GateTrigger);
}
}
여기서는 어차피 문을 4개를 통틀어서 만들어주기 때문에, 반복문을 사용해서 초기화 구문을 사용했고, 한가지 특이한 점이라고 한다면, 문의 위치를 동서남북으로 따로 지정을 해줬다보니, 다음과 같이 소켓으로 정보를 받아놨다는 점.
그리고 이제 문을 자체적으로 만들어주고, 그 다음 Trigger에 대한 정보를 넣어주는 형태로 뭐 솔직히 크게 다를 점은 없기는 하다만..
조금 다른 것이라고 한다면, SetupAttachment에서 Socket에 관한 정보를 넣어줬다는 것.
이는 이제 이 소켓의 정보를 가지고 위치적인 부분을 지정해서 해당 위치에 자연스럽게 넣어줄 수 있다는 것.
상태
말한대로 스테이지에는 4가지의 상태가 있다 이걸 enum class로 정의하자.
UENUM(BlueprintType)
enum class EStageState : uint8 {
READY = 0,
FIGHT,
REWARD,
NEXT
};
이제 이 상태를 내부적으로 스테이지가 가지게 될 것이고, 이제 Set을 통해서 지정을 할 것임.
일단 알다시피 State의 경우에는 총 4가지인데 들어오는 상태에 따라서 또 이를 조절하는 것이 필요하기야는 함.
그렇다 해서 switch case를 사용하는 것은 진짜로 미친 짓.
물론 해도 되지만 상태가 늘어날때마다 보기 싫은 코드들이 나열될 것임.
델리게이트를 이용하면 된다.
Wrapper Struct를 만들자.
DECLARE_DELEGATE(FOnStageChangedDelegate);
USTRUCT(BlueprintType)
struct FStageChangedDelegateWrapper {
GENERATED_BODY()
FStageChangedDelegateWrapper() {}
FStageChangedDelegateWrapper(const FOnStageChangedDelegate& InStageDelegate) : StageDeleagate(InStageDelegate) {}
FOnStageChangedDelegate StageDeleagate;
};
이를 바인딩 할 함수를 만들어주자.
UPROPERTY()
TMap<EStageState, FStageChangedDelegateWrapper> StateChangeActions;
void SetReady();
void SetFight();
void SetChoooseReward();
void SetChooseNext();
StateChangeActions.Add(EStageState::READY, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetReady)));
StateChangeActions.Add(EStageState::FIGHT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetFight)));
StateChangeActions.Add(EStageState::REWARD, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseReward)));
StateChangeActions.Add(EStageState::NEXT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseNext)));
void AABStageGimmick::SetState(EStageState InNewState)
{
CurrentState = InNewState;
if (StateChangeActions.Contains(InNewState)) {
StateChangeActions[CurrentState].StageDeleagate.ExecuteIfBound();
}
}
이렇게 SetStage를 호출하면 알아서 바인딩된 함수가 실행 될 것이다.
NPC 스폰
지금까지는 맵에 대해 다뤄봤다면 이제 전투적인 로직을 만들어보자.
protected:
UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AABCharacterNonPlayer> OpponentClass;
UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
float OpponentSpawnTime;
UFUNCTION()
void OnOpponentDestoryed(AActor* DestroyedActor);
FTimerHandle OpponentTimerHandle;
void OnOpponentSpawn();
아무래도 아무거나 스폰할 수 없도록 만들기 위해서 NPC를 상속받은 클래스만이 이제 생성이 가능하게 만들 것임.
이는 블루프린트 상에서 상속받아서 여러 NPC들을 등장시키게 만들 수 있으므로 이렇게 활용이 가능하다!
그리고 바로 스폰되는 거가 아니라 조금 뒤에 스폰이 되도록 만들어 줄 것임.
또한 이제 만약에 NPC가 죽어버리면, 보상 단계로 넘어가기 위해서 다음과 같은 함수를 지정한다.
일단 캐릭터가 스폰이 된다면 스폰 시에, 파괴가 되었을 시 수행되어야 하는 함수를 바인딩을 시켜준다.
void AABStageGimmick::OnOpponentSpawn()
{
const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f;
AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator);
AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor);
if (ABOpponentCharacter) {
ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestoryed);
}
}
void AABStageGimmick::OnOpponentDestoryed(AActor* DestroyedActor)
{
SetState(EStageState::REWARD);
}
게이트를 통과 할 때
게이트를 통과할 때 스테이지를 하나 더 생성해야하고 게이트 트리거는 없애야한다.
void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
check(OverlappedComponent->ComponentTags.Num() == 1);
FName ComponentTag = OverlappedComponent->ComponentTags[0];
FName SocketName = FName(*ComponentTag.ToString().Left(2));
check(Stage->DoesSocketExist(SocketName));
FVector NewLocation = Stage->GetSocketLocation(SocketName);
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParam(SCENE_QUERY_STAT(GateTrigger), false, this);
bool bResult = GetWorld()->OverlapMultiByObjectType(
OverlapResults,
NewLocation,
FQuat::Identity,
FCollisionObjectQueryParams::InitType::AllStaticObjects,
FCollisionShape::MakeSphere(775.0f),
CollisionQueryParam
);
if (!bResult) {
GetWorld()->SpawnActor<AABStageGimmick>(NewLocation, FRotator::ZeroRotator);
}
}
우리가 아까 각 컴포넌트 별로 태그를 달아뒀을 텐데, 만약에 태그가 없는 경우에는 뭔가 이상한 놈이 태그가 되었다는 것을 암시함.
따라서 체킹을 해주고서, 이 태그를 가져온 다음에, 옆에 2개를 잘라준다.
이러면 +X, +Y 의 형태로 남아지게 될 것임.
그럼 이 소켓이 해당 Stage의 위치적으로 존재하는 지 체크해야 함.
근데 Stage에는 있었으니까 해당 위치를 받아주고.
해당 위치에 Stage가 있을 수도 있잖슴. 그렇다면 재생성은 안하는게 좋으니까, Overlap을 검사한 뒤,
이 검사 결과를 바탕으로 새로운 Location에다가 만들어줄지 안만들어줄 시 결정하면 끝.
문이 닫히면 지정된 시간 후에 NPC를 소환하면 된다.
//Fight Section
OpponentSpawnTime = 2.0f;
OpponentClass = AABCharacterNonPlayer::StaticClass();
생성자에는 다음과 같이 초기 설정을 진행해주고,
void AABStageGimmick::SetFight()
{
StageTrigger->SetCollisionProfileName("NoCollision");
for (auto GateTrigger : GateTriggers) {
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
}
CloseAllGates();
GetWorld()->GetTimerManager().SetTimer(OpponentTimerHandle, this, &AABStageGimmick::OnOpponentSpawn, OpponentSpawnTime, false);
}
SetFight에서는 해당 시간이 지나고 난 뒤, NPC를 소환을 해주도록하자.
박스 생성
//Reward Section
protected :
UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AABItemBox> RewardBoxClass;
UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
TArray<TWeakObjectPtr<class AABItemBox>> RewardBoxes;
TMap<FName, FVector> RewardBoxLocations;
UFUNCTION()
void OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);
void SpawnRewardBoxes();
아이템 박스에 관해서 ItemBox만 가지고 놀아줘야 하므로 SubclassOf를 선언을 해줬고,
이제 ItemBox의 경우에는 TWeakObjectPtr로 선언을 해준 것을 알 수가 있음.
이는 약참조를 활용한 것인데, 애초에 스폰된 박스를 관리를 하겠다는 것이기는 하나, 문제가 일단 이 박스가 스테이지 기믹과는 막 큰 관련이 없음. → 이게 뭔 소리냐면 이 스테이지와 별개로 자기 방식대로 동작한다는 의미.
그래서 외부 로직이나 내부 로직에 의거해서 소멸이 가능함.
문제는 이걸 강참조 (TObjectPtr)로 활용하게 된다면 아직 사용중이겠거니 하고 메모리에 남겨버림.
그래서 약참조의 형태로 선언을 해주는 게 좋다는 것
액터 컴포넌트처럼 액터와 같이 동작하는 친구들은 무조건 같이가는게 맞아서 강참조가 좋은데, 아니라면 약참조가 더 좋다는 것.
void AABStageGimmick::SpawnRewardBoxes()
{
for (const auto& RewardBoxLocation : RewardBoxLocations) {
FVector WorldSpawnLocation = GetActorLocation() + RewardBoxLocation.Value + FVector(0.0f, 0.0f, 30.0f);
AActor* ItemActor = GetWorld()->SpawnActor(RewardBoxClass, &WorldSpawnLocation, &FRotator::ZeroRotator);
AABItemBox* RewardBoxActor = Cast<AABItemBox>(ItemActor);
if (RewardBoxActor) {
RewardBoxActor->Tags.Add(RewardBoxLocation.Key);
RewardBoxActor->GetTrigger()->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnRewardTriggerBeginOverlap);
RewardBoxes.Add(RewardBoxActor);
}
}
}
- 이거는 이제 각 위치별로 선언을 하고 해당 위치에 맞게 각각 박스를 스폰하고 태그를 달아준다.
- 다음에 ItemBox의 경우에는 GetTrigger를 함수로 만들어주고
- (이유는 Trigger가 protected로 되어있어서..)
- 이 트리거에 맞게 바인딩을 하는 작업을 포함시켜준다.
- 그 다음 이제 약참조를 받아주는 배열에 넣어주는 과정을 포함시킨다
void AABStageGimmick::OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
for (const auto& RewardBox : RewardBoxes) {
if (RewardBox.IsValid()) {
AABItemBox* ValidItemBox = RewardBox.Get();
AActor* OverlappedBox = OverlappedComponent->GetOwner();
if (OverlappedBox != ValidItemBox) {
ValidItemBox->Destroy();
}
}
}
SetState(EStageState::NEXT);
}
- 먼저 내부 탐색을 이어가는데, 박스가 유효하다면,
- 해당 박스를 가져오고,
- 만약 충돌한 것과 일치하지 않는다고 한다면 뽀셔 버린다.
- 이제 같은 경우에는 보상 하나를 똭 얻게 하면 된다는 것. (이거는 뭐 내부 로직에서 하니까)
- 얻을 건 얻었으니.. 열어버린다.
'Unreal Engine 5 > 강의' 카테고리의 다른 글
[UE5] AI - 행동 트리 모델 (0) | 2024.12.13 |
---|---|
[UE5] 데이터 - 게임 데이터 관리하기 (0) | 2024.12.12 |
[UE5] 아이템 - 여러 종류 아이템 획득하기 (0) | 2024.12.09 |
[UE5] 애니메이션 - 캐릭터 공격 판정 (0) | 2024.12.04 |
[UE5] 애니메이션 - 캐릭터 애니메이션과 몽타주를 이용한 연속 공격 (0) | 2024.12.03 |