본문 바로가기
언리얼 C++ 게임 개발의 정석/12. 프로젝트의 설정과 무한 맵 의 제작

12.3 무한 맵의 생성

by ftftgop3 2024. 5. 9.

섹션

레벨을 세셕이라는 단위로 나누고

섹션 클리어 시 새로운 섹션이 등장하는 무한 스테이지

 

섹션 액터 기능

섹션의 배경과 네 방향으로 캐릭터 입장을 통제하는 문 제공 

플에이어가 섹션에 진입하면 모든 문 잠금

문 잠금 시일정 시간 후 NPC, 아이템 상자 생성

모든 NPC 제거 시 모든 문 개방

통과한 문으로 이어지는 새로운 섹션 생성

 

섹션 액터 제작

이 섹션의 주요 배경 SM_SQUARE 애셋 사용

소캣에 Gate에 출입문 추가, 철문은 피벗이 왼쪽에 존재 하기 때문에 최종 위치는 Y축으로 -80.5 이동 후 배치

 

ABSection 

섹션 mesh 데이터 로드

mesh 데이터의 소캣에 게이트 배치

// 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:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

private:
	//4개의 게이트 메쉬
	UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
		TArray<UStaticMeshComponent*> GateMeshes;

	//SQUARE 메쉬
	UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
		UStaticMeshComponent* Mesh;
	
};

 

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; //루트 컴포넌트 지정

	FString AssetPath = TEXT("/Game/Book/StaticMesh/SM_SQUARE.SM_SQUARE");
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_SQUARE(*AssetPath);
	if (SM_SQUARE.Succeeded())
	{
		Mesh->SetStaticMesh(SM_SQUARE.Object);
	}
	else
	{
		ABLOG(Error, TEXT("Failed to load staticmesh asset. : %s"), *AssetPath);
	}
	//게이트 메쉬 데이터 로드
	FString GateAssetPath = TEXT("/Game/Book/StaticMesh/SM_GATE.SM_GATE");
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_GATE(*GateAssetPath);
	if (!SM_GATE.Succeeded())
	{
		ABLOG(Error, TEXT("Failed to load staticmesh asset. : %s"), *GateAssetPath);
	}
	
	static FName GateSockets[] = { {TEXT("+XGate")}, {TEXT("-XGate")}, {TEXT("+YGate")}, {TEXT("-YGate")} };
	for (FName GateSocket : GateSockets)
	{
		//mesh 데이터의 소캣 이름 확인
		ABCHECK(Mesh->DoesSocketExist(GateSocket));
		//메쉬 컴포넌트 생성
		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);
	}

}

 

ABTrigger 콜리전 프리셋 제작

플레이어 입장 및 클리어 시 출구 선택에 사용

BOX 컴포넌트를 생성 해 중앙 및 철문 영역에 배치해 사용

 

ABSection 

한개는 플레이어 입장을 감지, 나머지 4개는 게이트를 감지하는 역할로 사용

모든 트리거는 Root는 Mesh 데이터로 할당

UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
	GENERATED_BODY()
    private:
   	//게이트 트리거
	UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrivateAccess = true))
		TArray<UBoxComponent*> GateTriggers;
        
    //센셕 트리거
	UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrivateAccess = true))
		UBoxComponent* Trigger;
}

AABSection::AABSection()
{
	//중앙에 트리거 사이즈 및 배치 후 콜리전 ABTrigger 설정
	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"));
    
    static FName GateSockets[] = { {TEXT("+XGate")}, {TEXT("-XGate")}, {TEXT("+YGate")}, {TEXT("-YGate")} };
	for (FName GateSocket : GateSockets)
	{
		//mesh 데이터의 소캣 이름 확인
		ABCHECK(Mesh->DoesSocketExist(GateSocket));
		//메쉬 컴포넌트 생성
		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);

		//문에 맞게 트리거 사이즈 및 배치 후 콜리전 ABTrigger 설정
		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);
	}
}

 

섹션 엑터의 스테이트 추가 

1. Ready State : 문을 열어놓고 대기, 플레이어 진입 시 전투 스테이지로 이동

2. Battle Sate : 문을 닫고 일정 시간 지나면 NPC 및 아이템 상자 소환

모든 NCP가 죽으면 완료 스테이지로 전환

3. Complete state : 닫힌 문을 열고 각 게이트의 플레이어 감지 시 해당 문을 열고 새로운 섹션 생성

 

UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
	GENERATED_BODY()
    private:
	enum class ESectionState : uint8
	{
		READY = 0,
		BATTLE,
		COMPLETE
	};

	void SetState(ESectionState NewState);
	ESectionState CurrentState = ESectionState::READY;
	//모든 게이트 열기 및 닫기 
	void OperateGates(bool bOpen = true);
    
    private:
    //시작 시 전투 없이 이동 가능하게 하는 변수
	UPROPERTY(EditAnywhere, Category = State, Meta = (AllowPrivateAccess = true))
		bool bNoBattle;
}
AABSection::AABSection()
{
	bNoBattle = false;
}
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"));
		}
		//게이트 오픈
		OperateGates(true);
		break;
	}
	case ESectionState::BATTLE:
	{
		//입장 트리거 비활성화, 게이트 트리거 비활성화
		Trigger->SetCollisionProfileName(TEXT("NoCollision"));
		for (UBoxComponent* GateTrigger : GateTriggers)
		{
			GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
		}
		//게이트 닫음
		OperateGates(false);
		break;
	}
	case ESectionState::COMPLETE:
	{
		//입장 트리거 비활성화, 게이트 트리거 활성화
		Trigger->SetCollisionProfileName(TEXT("NoCollision"));
		for (UBoxComponent* GateTrigger : GateTriggers)
		{
			GateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
		}
		//게이트 열기
		OperateGates(true);
		break;
	}
	}

	CurrentState = NewState;
}

void AABSection::OperateGates(bool bOpen)
{
	//true 시 회전, false 회전 초기화
	for (UStaticMeshComponent* Gate : GateMeshes)
	{
		Gate->SetRelativeRotation(bOpen ? FRotator(0.0f, -90.0f, 0.0f) : FRotator::ZeroRotator);
	}
}

 

OnConstruction 기능

에디터에 작업에서 속성 이나 정보 값을 수정 시 해당 엑터의 OnConstruction 실행

액터와 컴포넌트 속성을 작업 중인 레벨에서 바로 확인이 가능

 

에디터에서 bNoBattle 값을 변화 하면 READY, COMPLETE 상태로 변경

UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
	GENERATED_BODY()
    public:
	virtual void OnConstruction(const FTransform& Transform) override;
}

void AABSection::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);
	SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}

 

ABSection 

각 트리거 컴포넌트의 델리게이트에 함수 등록

캐릭터 엑터 감지 시 배틀 상태로 전환

 

게이트 트리거에는 태그(소캣 이름) 과 델리게이트 함수 등록

해당 게이트에 연결된 섹션 존재 여부를 확인 해 섹션 중복 생성을 못하게 처리

충돌 결과에 따라 섹션 생성 및 섹션 유지 하게 변경

UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
	GENERATED_BODY()
	
	public:
    //트리거의 델리게이트에 함수 등록 
	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);
}

AABSection::AABSection()
{	
	//트리게 델리게이트에 함수 등록
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnTriggerBeginOverlap);
    for (FName GateSocket : GateSockets)
	{
    	//게이트 트리거 델리게이트에 함수 등록
		NewGateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnGateTriggerBeginOverlap);
		//게이트 트리거에 태그 이름 지정( 무슨 게이트에 이벤트 발생 했는지 확인)
		NewGateTrigger->ComponentTags.Add(GateSocket);
	}
}

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);
	//태그 정보 값으로 mesh 소캣에 맞는 이름인지 확인
	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);
	}
	else
	{
		ABLOG(Warning, TEXT("New section area is not empty."));
	}
}