블루프린트 없이 c++로 무한 맵 만들기

유영준·2023년 1월 24일
0
post-thumbnail

오늘은 지금껏 있던 맵을 코드로서 다시 구현해주고, 맵을 통과하면 새로운 맵이 나오게끔 설정을 할 예정이다

c++로 맵 만들기

그동안은 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 GenerationDynamic으로 바꿈으로서 해결된다


마지막으로 맵이 생성되고 적이 나오는것까지 확인해보자

profile
토비폭스가 되고픈 게임 개발자

0개의 댓글