🤍ref.
- 이득우의 언리얼 C++ 게임 개발의 정석, 이득우, 에이콘출판사, 2018
- Unreal Engine 4.27 Documentation
✨info
튜토리얼 교재는 4.19v을 사용하나, 4.27v으로 실습 후 게시글을 작성하였습니다.
새로 알게 된 내용과 추가로 공부한 내용 위주로 작성합니다. 배우는 과정이라 부족한 점이 많습니다.😊
1. 섹션 액터 제작
Actor
를 부모 클래스로 하는 ABSection
클래스를 만들어 무한 맵 스테이지를 제작할 수 있다.
섹션 액터가 할 일
- 섹션의 배경과 네 방향으로 캐릭터 입장을 통제하는 문 제공
- 플레이어가 섹션에 진입하면 모든 문 닫음
- 문을 닫고 일정 시간 후에 섹션 중앙에서 NPC 생성
- 문을 닫고 일정 시간 후에 아이템 상자를 섹션 내 랜덤한 위치에 생성
- 생성한 NPC가 죽으면 모든 문 개방
- 통과한 문으로 이어지는 새로운 섹션 생성
액터에 스태틱메시 컴포넌트를 선언한 후 루트로 설정하고 현재 배경 애셋을 지정한다. 예제로 제공된 애셋은 방향별로 출입문과 섹션을 이어붙일 수 있도록 소켓이 부착되어 있다.
배경의 각 출입구에 철문을 배치시켜보자. 철문마다 스태틱메시 컴포넌트를 제작해 소켓에 부착한다. 코드에 소켓 목록을 제작하고 철문을 각각 부착할 수 있다. 철문은 모두 동일한 기능을 가지므로 TArray
로 관리한다.
ABSection.h
1
2
3
4
5
6
7
8
9
10
11
12
13
class ARENABATTLE_API AABSection : public AActor
{
...
private:
// 철문 관리용 TArray
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
TArray<UStaticMeshComponent*> GateMeshes;
// 루트로 설정할 배경 메시의 스태틱 컴포넌트
UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
UStaticMeshComponent* Mesh;
};
ABSection.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
AABSection::AABSection()
{
PrimaryActorTick.bCanEverTick = false;
// 루트 컴포넌트를 Mesh로 지정
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
RootComponent = Mesh;
// 배경 애셋 경로 참조
FString AssetPath = TEXT("/Game/Book/StaticMesh/SM_SQUARE.SM_SQUARE");
// ConstructorHelpers: 애셋의 내용물을 가져올 때 사용
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 GateSokets[] = { {TEXT("+XGate")}, {TEXT("-XGate")}, {TEXT("+YGate")}, {TEXT("-YGate")} };
for (FName GateSocket : GateSokets)
{
// 배경 애셋에 소켓이 없으면 오류 로그 출력
ABCHECK(Mesh->DoesSocketExist(GateSocket));
UStaticMeshComponent* NewGate = CreateDefaultSubobject<UStaticMeshComponent>(*GateSocket.ToString());
NewGate->SetStaticMesh(SM_GATE.Object);
// NewGate의 부착 지점 설정
NewGate->SetupAttachment(RootComponent, GateSocket);
NewGate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
GateMeshes.Add(NewGate);
}
}
철문 부착 화면
2. 트리거 영역 추가
플레이어의 입장을 감지하고 출구를 선택할 때 사용할 콜리전 프리셋을 생성한다.
캐릭터만 감지하도록 트리거 프리셋을 설정한다.
ABSection.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ARENABATTLE_API AABSection : public AActor
{
...
private:
...
// 게이트별 트리거 정보를 담을 Box 컴포넌트 리스트
UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrivateAccess = true))
TArray<UBoxComponent*> GateTriggers;
// 섹션 중앙에 부착할 Box 컴포넌트
UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrivateAccess = true))
UBoxComponent* Trigger;
};
ABSection.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
AABSection::AABSection()
{
...
// 트리거 이름의 박스 컴포넌트 생성(섹션 중앙)
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 GateSokets[] = { {TEXT("+XGate")}, {TEXT("-XGate")}, {TEXT("+YGate")}, {TEXT("-YGate")} };
for (FName GateSocket : GateSokets)
{
...
// 철문에 부착할 트리거 컴포넌트 리스트 생성
UBoxComponent* NewGateTrigger = CreateDefaultSubobject<UBoxComponent>(*GateSocket.ToString().Append(TEXT("Trigger")));
NewGateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.f));
NewGateTrigger->SetupAttachment(RootComponent, GateSocket);
NewGateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
NewGateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
GateTriggers.Add(NewGateTrigger);
}
}
액터에 트리거 영역이 추가된 화면
3. 액터 로직 설계
3.1. 스테이트 머신 생성
액터의 로직을 스테이트 머신으로 설계하자.
- 준비 스테이트
- 액터의 시작 스테이트
- 중앙 트리거가 플레이어 진입을 감지하면 전투 스테이트로 전환
- 전투 스테이트
- 문을 닫고 일정 시간 뒤 NPC 소환
- 랜덤한 위치에 아이템 상자 생성
- NPC가 죽으면 완료 스테이트로 이동
- 완료 스테이트
- 닫힌 철문 개방
- 각 문에 배치한 트리거 게이트가 플레이어를 감지하면 해당 방향에 새 섹션 소환
ABSection.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
...
public:
AABSection();
// 제작 과정과 시작 스테이트(COMPLETE) 연동
virtual void OnConstruction(const FTransform& Transform) override;
private:
// 스테이트 리스트 열거
enum class ESectionState : uint8
{
READY = 0,
BATTLE,
COMPLETE
};
// 스테이트 설정
void SetState(ESectionState NewState);
ESectionState CurrentState = ESectionState::READY;
// 게이트 개방
void OperateGates(bool bOpen = true);
private:
// 전투 여부 확인
UPROPERTY(VisibleAnywhere, Category = State, Meta = (AllowPrivateAccess = true))
bool bNoBattle;
};
ABSection.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
AABSection::AABSection()
{
...
// 플레이 첫 스테이트는 전투 없이 시작하도록 false로 설정
bNoBattle = false;
}
void AABSection::BeginPlay()
{
Super::BeginPlay();
SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}
void AABSection::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
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("NoCollision"));
}
OperateGates(true);
break;
}
}
CurrentState = NewState;
}
void AABSection::OperateGates(bool bOpen)
{
for (UStaticMeshComponent* Gate : GateMeshes)
{
Gate->SetRelativeRotation(bOpen ? FRotator(0.0f, -90.0f, 0.0f) : FRotator::ZeroRotator);
}
}
OnConstruction
: 에디터 작업에서 선택한 액터의 속성이나 트랜스폼 정보가 변경될 때 미리 결과 확인
완료 스테이트 설정이 적용된 에디터 뷰포트
3.2. 섹션 액터 생성
섹션 액터 조건
READY
스테이트에서부터 시작- 가운데 트리거 영역을 활성화하고 플레이어의 진입 감지
- 플레이어 감지 시
BATTLE
스테이트로 전환하고 문 닫음
- 철문 방향에 섹션 액터가 생성되어 있는지 확인
- 이미 액터가 있다면 생성 건너뜀
박스 컴포넌트의 감지 기능을 사용해 위 내용을 구현할 수 있다.
OnComponentBeginOverlap
델리게이트에 바인딩할 함수 생성- 다이내믹 델리게이트이므로
UFUNCTION
선언
- 다이내믹 델리게이트이므로
- 모든 문의 박스 컴포넌트 델리게이트에 하나의 멤버 함수 연결
- 각 문의 기능이 모두 동일하기 때문
- 박스 컴포넌트를 구분할 수 있도록 소켓 이름으로 태그 설정
- 태그 방향으로 다음 섹션 액터 생성
ABSection.h
1
2
3
4
5
6
7
8
9
10
11
12
13
class ARENABATTLE_API AABSection : public AActor
{
...
private:
...
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);
};
ABSection.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
AABSection::AABSection()
{
...
// 박스 컴포넌트와 델리게이트 바인딩
Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnTriggerBeginOverlap);
static FName GateSokets[] = { {TEXT("+XGate")}, {TEXT("-XGate")}, {TEXT("+YGate")}, {TEXT("-YGate")} };
for (FName GateSocket : GateSokets)
{
...
// 컴포넌트 델리게이트와 게이트 함수 연결
NewGateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnGateTriggerBeginOverlap);
// 게이트 소켓명으로 컴포넌트 태그 추가
NewGateTrigger->ComponentTags.Add(GateSocket);
}
// 플레이 첫 스테이트는 전투 없이 시작하도록 false로 설정
bNoBattle = false;
}
// 박스 컴포넌트 델리게이트와 연결된 함수
void AABSection::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// 플레이어 감지 시 스테이트가 READY 상태라면 BATTLE으로 변경
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);
}
else
{
ABLOG(Warning, TEXT("New Section area is not empty"));
}
}
Leave a comment