섹션
레벨을 세셕이라는 단위로 나누고
섹션 클리어 시 새로운 섹션이 등장하는 무한 스테이지
섹션 액터 기능
섹션의 배경과 네 방향으로 캐릭터 입장을 통제하는 문 제공
플에이어가 섹션에 진입하면 모든 문 잠금
문 잠금 시일정 시간 후 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."));
}
}
'언리얼 C++ 게임 개발의 정석 > 12. 프로젝트의 설정과 무한 맵 의 제작' 카테고리의 다른 글
12.4 내비게이션 메시 시스템 설정 (0) | 2024.05.15 |
---|---|
12.2 INI 설정과 애셋의 지연 로딩 (0) | 2024.05.06 |
12.1 프로젝트 정리 와 모듈 추가 (0) | 2024.05.04 |