오늘은 지금껏 있던 맵을 코드로서 다시 구현해주고, 맵을 통과하면 새로운 맵이 나오게끔 설정을 할 예정이다
그동안은 SM_SQUARE 메시를 맵으로서 사용해왔다
이번에는 맵 또한 액터로 구현해 생성되게끔 하려고 한다
먼저 SM_SQUARE 메시를 보자
메시에 자체적으로 8개의 소켓이 있다
이때 Gate 명이 붙은 소캣을 통해 문을 달아줄 것이고, +X, +Y 등의 소켓은 새롭게 맵이 생성될 위치를 나타낸다
이번에는 문으로 사용할 에셋을 살펴보자
문을 보면 한쪽으로 피벗이 몰려있기 때문에 최종적으로 부착할 위치는 -80.5 만큼 이동한 지점이 된다
문은 동서남북 4곳에 모두 생성되며 모두 동일하게 작동할 것이므로 배열에 넣어 관리하도록 하겠다
ABSection.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "GameFramework/Actor.h"
#include "ABSection.generated.h"
UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AABSection();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
private:
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
UStaticMeshComponent* Mesh;
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
TArray<UStaticMeshComponent*> GateMeshes;
};
ABSection.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ABSection.h"
// Sets default values
AABSection::AABSection()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
RootComponent = Mesh;
static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_SQUARE
(TEXT("/Game/Book/StaticMesh/SM_SQUARE.SM_SQUARE"));
if (SM_SQUARE.Succeeded())
Mesh->SetStaticMesh(SM_SQUARE.Object);
static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_GATE
(TEXT("/Game/Book/StaticMesh/SM_GATE.SM_GATE"));
static TArray<FName> GateSockets = Mesh->GetAllSocketNames();
for (auto GateSocket : GateSockets)
{
if (GateSocket.GetStringLength() > 2)
{
UStaticMeshComponent* NewGate = CreateDefaultSubobject<UStaticMeshComponent>(*GateSocket.ToString());
NewGate->SetStaticMesh(SM_GATE.Object);
NewGate->SetupAttachment(RootComponent, GateSocket);
NewGate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
GateMeshes.Add(NewGate);
}
}
}
문을 소캣에 부착하는 방법은 SM_SQUARE
에 있는 모든 소캣을 배열에 담고, 그 중 2글자를 초과하는 부분만 검출해서 넣어주는 방식을 사용했다
맵에 입장을 하게 되면 상대방이 나오고, 문에 가까이 가게 되면 새로운 맵이 나오게끔 설정을 해주어야 한다
이를 위해 먼저ABCharacter
만 검출하는 콜리전 트리거를 만들어주자
이제 맵 중간, 문 주위에 트리거를 설치하고, 각각 상황에 맞게 문이 기능할 수 있도록 준비, 전투, 완료 상태를 만들도록 하겠다
ABSection.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "GameFramework/Actor.h"
#include "ABSection.generated.h"
UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AABSection();
virtual void OnConstruction(const FTransform& Transform) override;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
private:
enum class ESectionState : uint8
{
READY = 0,
BATTLE = 1,
COMPLETE = 2
};
void SetState(ESectionState NewState);
ESectionState CurrentState = ESectionState::READY;
void OperateGate(bool bOpen = true);
UFUNCTION()
void OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult);
UFUNCTION()
void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
public:
private:
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
UStaticMeshComponent* Mesh;
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
TArray<UStaticMeshComponent*> GateMeshes;
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
TArray<UBoxComponent*> GateTriggers;
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
UBoxComponent* Trigger;
UPROPERTY(EditAnywhere, Category = State, Meta = (AllowPrivateAcces = true))
bool bNoBattle;
};
ABSection.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ABSection.h"
// Sets default values
AABSection::AABSection()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
RootComponent = Mesh;
static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_SQUARE
(TEXT("/Game/Book/StaticMesh/SM_SQUARE.SM_SQUARE"));
if (SM_SQUARE.Succeeded())
Mesh->SetStaticMesh(SM_SQUARE.Object);
Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
Trigger->SetBoxExtent(FVector(775.0f, 775.0f, 300.0f));
Trigger->SetupAttachment(RootComponent);
Trigger->SetRelativeLocation(FVector(0.0f, 0.0f, 250.0f));
Trigger->SetCollisionProfileName(TEXT("ABTrigger"));
Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnTriggerBeginOverlap);
static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_GATE
(TEXT("/Game/Book/StaticMesh/SM_GATE.SM_GATE"));
static TArray<FName> GateSockets = Mesh->GetAllSocketNames();
for (auto GateSocket : GateSockets)
{
if (GateSocket.GetStringLength() > 3)
{
UStaticMeshComponent* NewGate = CreateDefaultSubobject<UStaticMeshComponent>(*GateSocket.ToString());
NewGate->SetStaticMesh(SM_GATE.Object);
NewGate->SetupAttachment(RootComponent, GateSocket);
NewGate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
GateMeshes.Add(NewGate);
UBoxComponent* NewGateTrigger = CreateDefaultSubobject<UBoxComponent>(*GateSocket.ToString().Append(TEXT("Trigger")));
NewGateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.0f));
NewGateTrigger->SetupAttachment(RootComponent, GateSocket);
NewGateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
NewGateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
GateTriggers.Add(NewGateTrigger);
NewGateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnGateTriggerBeginOverlap);
NewGateTrigger->ComponentTags.Add(GateSocket);
}
}
bNoBattle = false;
}
void AABSection::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}
// Called when the game starts or when spawned
void AABSection::BeginPlay()
{
Super::BeginPlay();
SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}
void AABSection::SetState(ESectionState NewState)
{
switch (NewState)
{
case ESectionState::READY:
{
Trigger->SetCollisionProfileName(TEXT("ABTrigger"));
for (UBoxComponent* GateTrigger : GateTriggers)
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
OperateGate(true);
break;
}
case ESectionState::BATTLE:
{
Trigger->SetCollisionProfileName(TEXT("NoCollision"));
for (UBoxComponent* GateTrigger : GateTriggers)
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
OperateGate(false);
break;
}
case ESectionState::COMPLETE:
{
Trigger->SetCollisionProfileName(TEXT("NoCollision"));
for (UBoxComponent* GateTrigger : GateTriggers)
GateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
OperateGate(true);
break;
}
}
CurrentState = NewState;
}
void AABSection::OperateGate(bool bOpen)
{
for (UStaticMeshComponent* Gate : GateMeshes)
{
Gate->SetRelativeRotation(bOpen ? FRotator(0.0f, -90.0f, 0.0f) : FRotator::ZeroRotator);
}
}
void AABSection::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (CurrentState == ESectionState::READY)
SetState(ESectionState::BATTLE);
}
void AABSection::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
ABCHECK(OverlappedComponent->ComponentTags.Num() == 1);
FName ComponentTag = OverlappedComponent->ComponentTags[0];
FName SocketName = FName(*ComponentTag.ToString().Left(2));
if (!Mesh->DoesSocketExist(SocketName))
return;
FVector NewLocation = Mesh->GetSocketLocation(SocketName);
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParam(NAME_None, false, this);
FCollisionObjectQueryParams ObjectQueryParam(FCollisionObjectQueryParams::InitType::AllObjects);
bool bResult = GetWorld()->OverlapMultiByObjectType
(
OverlapResults,
NewLocation,
FQuat::Identity,
ObjectQueryParam,
FCollisionShape::MakeSphere(775.0f),
CollisionQueryParam
);
if (!bResult)
auto NewSection = GetWorld()->SpawnActor<AABSection>(NewLocation, FRotator::ZeroRotator);
}
먼저 트리거를 설정해주었고, 첫번째 맵은 기본맵이기에 전투가 일어나지 않도록 설정했다
마지막으로 생성된 방을 새롭게 들어가게 되면, 아이템 상자와 적이 나오게끔 설정해줄 것이다
이 둘은 FTimer를 사용해 추가해주도록 하겠다
ABSection.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "GameFramework/Actor.h"
#include "ABSection.generated.h"
...
private:
...
FTimerHandle SpawnNPCTimerHandle = {};
FTimerHandle SpawnItemBoxTimerHandle = {};
};
ABSection.cpp
#include "ABSection.h"
#include "ABCharacter.h"
#include "ABItem.h"
// Sets default values
AABSection::AABSection()
{
...
EnemySpawnTime = 2.0f;
ItemBoxSpawnTime = 5.0f;
}
...
void AABSection::SetState(ESectionState NewState)
{
switch (NewState)
{
case ESectionState::READY:
{
Trigger->SetCollisionProfileName(TEXT("ABTrigger"));
for (UBoxComponent* GateTrigger : GateTriggers)
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
OperateGate(true);
break;
}
case ESectionState::BATTLE:
{
Trigger->SetCollisionProfileName(TEXT("NoCollision"));
for (UBoxComponent* GateTrigger : GateTriggers)
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
OperateGate(false);
GetWorld()->GetTimerManager().SetTimer(SpawnNPCTimerHandle,
FTimerDelegate::CreateLambda([this]()->void
{
GetWorld()->SpawnActor<AABCharacter>(GetActorLocation() + FVector::UpVector * 88.0f, FRotator::ZeroRotator);
}), EnemySpawnTime, false);
GetWorld()->GetTimerManager().SetTimer(SpawnItemBoxTimerHandle,
FTimerDelegate::CreateLambda([this]() -> void
{
FVector2D RandXY = FMath::RandPointInCircle(600.0f);
GetWorld()->SpawnActor<AABItem>(GetActorLocation() + FVector(RandXY, 20.0f), FRotator::ZeroRotator);
}), ItemBoxSpawnTime, false);
break;
}
case ESectionState::COMPLETE:
{
Trigger->SetCollisionProfileName(TEXT("NoCollision"));
for (UBoxComponent* GateTrigger : GateTriggers)
GateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
OperateGate(true);
break;
}
}
CurrentState = NewState;
}
람다식을 통해 캐릭터와 아이템 상자가 생성되도록 해주었다
마지막으로 현재 상태 그대로면 내비게이션 메시가 새로운 맵에서는 작동하지 못하는데,
이는 Project Setting
-> Navigation Mesh
-> Runtime Generation
을 Dynamic
으로 바꿈으로서 해결된다
마지막으로 맵이 생성되고 적이 나오는것까지 확인해보자