Post

Unreal: 내일배움캠프 6번 과제 - 회전발판/이동 장애물

Unreal: 내일배움캠프 6번 과제 - 회전발판/이동 장애물

본래 이후에 있을 과제들까지 합쳐서 한번 더 완성도 있는 프로젝트로 만들려 했는데, 과제의 요구사항이 프로젝트의 방향성과는 맞지 않는다 판단해서 요구사항만 맞추어서 빠르게 끝내도록 하겠다.

조건

  • 퍼즐 오브젝트 설계
  • 2개 이상의 다른 기능을 하는 Actor 클래스 구현
  • Tick을 이용한 Transform 변경
    • 회전
    • 이동
    • 프레임 독립성
  • 리플렉션을 이용해 에디터에서 변경 가능하게 구현
  • 오브젝트 배치 및 테스트 추가 도전 과제로는 2개가 더 있다.
  • 타이머 활용
    • Tick대신 사용하여 퍼포먼스 개선
    • 시간 이후 작동하는 로직 구현
  • 랜덤 활용
    • 시작시 임의의 좌표에 여러 액터 배치
    • 속도, 범위나 각도등을 랜덤하게 설정

      레벨 구현

기존에 언리얼 블루프린트 기초 시간에 활용했던 에셋들이 있다. 당시 간단하게 블루프린트를 이용한 캐릭터와 구조를 만들었는데, 이를 이용해 미래 도시같은 느낌의 간단한 레벨을 만들어 보겠다. 먼저 별도의 레벨을 구성해주자.

대략적인 형태는 갖추었다. 캐릭터 조작 구현은 다음 과제에 있어서 별도의 프로젝트로 이어 만들 예정이니 이번엔 전에 만들어놓았던 로봇 캐릭터를 이용해주도록 하겠다.

회전/이동 발판

회전하는, 그리고 이동하는 발판을 만들기 위해 RotatingFloor와 Elevator라는 C++클래스를 만들었다. 각각 회전하는 발판과 플레이어를 감지해 올라가는 발판의 역할을 할것이다. Elevator에서는 충돌을 감지할 EventBox와 캐릭터의 충돌에 따라 OnOvelapBegin()/OnOverlapEnd()를 호출해 올라갈지 내려갈지 결정하고, Tick에서 그에 따라 캐릭터가 있으면 위로 MoveSpeed 속도로 이동해 MaxRange만큼까지 올라가고, 캐릭터가 없으면 충돌이 사라지면 원점으로 다시 내려온다. 모든 이동은 DeltaTime을 곱해 프레임독립성을 보장한다.

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
//Elevator.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Elevator.generated.h"

class UStaticMeshComponent;
class UBoxComponent;

UCLASS()
class ONLINELEARNINGKIT_API AElevator : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AElevator();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	UStaticMeshComponent* StaticComp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	UBoxComponent* EventBox;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	int MoveSpeed;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	int MaxRange;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	
	bool IsElevating;
	FVector StartLocation;

	UFUNCTION()
	void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	UFUNCTION()
	void OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);


public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};
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
// Elevator.cpp
#include "Elevator.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"

// Sets default values
AElevator::AElevator()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	StaticComp = CreateDefaultSubobject<UStaticMeshComponent>("StaticMesh");
	RootComponent = StaticComp;
	EventBox = CreateDefaultSubobject<UBoxComponent>("EventBox");
	EventBox->SetupAttachment(StaticComp);
	IsElevating = false;
}

// Called when the game starts or when spawned
void AElevator::BeginPlay()
{
	Super::BeginPlay();
	StartLocation = GetActorLocation();
	EventBox->OnComponentBeginOverlap.AddDynamic(this, &AElevator::OnOverlapBegin);
	EventBox->OnComponentEndOverlap.AddDynamic(this, &AElevator::OnOverlapEnd);
}

void AElevator::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	UE_LOG(LogTemp, Warning, TEXT("Overlap Begin!"));
	IsElevating = true;
}

void AElevator::OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	UE_LOG(LogTemp, Warning, TEXT("Overlap end!"));
	IsElevating = false;
}

// Called every frame
void AElevator::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (IsElevating) {
		if (GetActorLocation().Z < StartLocation.Z + MaxRange) {
			AddActorWorldOffset(FVector(0, 0, MoveSpeed * DeltaTime));
		}
	}
	else {
		if (GetActorLocation().Z > StartLocation.Z) {
			AddActorWorldOffset(FVector(0, 0, -MoveSpeed * DeltaTime));
		}
	}
}

회전발판은 충돌판정까진 안만들었기에 더 간단하다. Tick에서 Rotation을 꾸준히 더해주면 된다. 마찬가지로 DeltaTime을 사용한다.

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
//RotatingFloor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "RotatingFloor.generated.h"

class UStaticMeshComponent;

UCLASS()
class ONLINELEARNINGKIT_API ARotatingFloor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ARotatingFloor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	UStaticMeshComponent* StaticComp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	int RotationSpeed;
	

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

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
//RotatingFloor.cpp
#include "RotatingFloor.h"
#include "Components/StaticMeshComponent.h"

// Sets default values
ARotatingFloor::ARotatingFloor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	StaticComp = CreateDefaultSubobject<UStaticMeshComponent>("StaticMesh");
	RootComponent = StaticComp;
}

// Called when the game starts or when spawned
void ARotatingFloor::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ARotatingFloor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	AddActorLocalRotation(FRotator(0, RotationSpeed * DeltaTime, 0));
}

Elevator의 이동속도와 거리, RotatingFloor의 회전속도는 UPROPERTY 메크로를 설정해놨고, 블루프린트 에디터/디테일 패널에서도 값을 수정할 수 있다. 그 수정값에 따라 실행중에도 잘 작동함을 확인할 수 있다. 일단 서로 다른 값들을 활용해 마지막 발판까지 올라갈 수 있도록 설정해보았다.

Timer활용

타이머를 활용하면 시간에 따른 특별한 로직을 구현하거나 Tick에서 매번 계산해야했던 일부 코드들을 Tick()에서 분리함으로서 프레임률에 주는 부하를 일부 줄일 수 있다. 지금 수준의 간단한 예시에서는 Tick에 몰아넣더라도 성능에 부하가 큰편은 아니나 연습을 위해 발판을 하나 더 만들어보자. 앞뒤로 주기적으로 움직이는 발판인 MovingFloor를 만들었다.

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
//MovingFloor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MovingFloor.generated.h"

UCLASS()
class ONLINELEARNINGKIT_API AMovingFloor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AMovingFloor();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	UStaticMeshComponent* StaticComp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	int MoveSpeed;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	int MaxRange;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	bool IsGoingFront;
	FVector StartLocation;
	double MovedDistance;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

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
//MovingFloor.cpp


#include "MovingFloor.h"

// Sets default values
AMovingFloor::AMovingFloor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	StaticComp = CreateDefaultSubobject<UStaticMeshComponent>("StaticMesh");
	RootComponent = StaticComp;
}

// Called when the game starts or when spawned
void AMovingFloor::BeginPlay()
{
	Super::BeginPlay();
	StartLocation = GetActorLocation();
	IsGoingFront = true;
	MovedDistance = 0;
}

// Called every frame
void AMovingFloor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (IsGoingFront) {
		if (MovedDistance > MaxRange) {
			IsGoingFront = false;
		}
		else {
			AddActorLocalOffset(FVector(MoveSpeed * DeltaTime, 0, 0));
			MovedDistance += MoveSpeed * DeltaTime;
		}
	}
	else {
		if (MovedDistance <= 0) {
			IsGoingFront = true;
		}
		else {
			AddActorLocalOffset(FVector(-MoveSpeed * DeltaTime, 0, 0));
			MovedDistance -= MoveSpeed * DeltaTime;
		}
	}

}

Tick에서 일정주기마다 검사를 하는 방식을 통해 특정 거리에 도달하면 작동하는 방식으로 구현했다. 매 프레임마다 검사를 한다는 점에서 비용이 높은 방식이다. 타이머를 통해서도 이 방식을 구현할 수 있겠다.

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
//MovingFloor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MovingFloor.generated.h"

UCLASS()
class ONLINELEARNINGKIT_API AMovingFloor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AMovingFloor();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	UStaticMeshComponent* StaticComp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	int MoveSpeed;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RotatingFloor)
	int MaxRange;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	int Direction;
	FVector StartLocation;
	FTimerHandle FlipTimer;

	void FlipDirection();
public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};
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
//MovingFloor.cpp


#include "MovingFloor.h"

// Sets default values
AMovingFloor::AMovingFloor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	StaticComp = CreateDefaultSubobject<UStaticMeshComponent>("StaticMesh");
	RootComponent = StaticComp;
}

// Called when the game starts or when spawned
void AMovingFloor::BeginPlay()
{
	Super::BeginPlay();
	StartLocation = GetActorLocation();
	Direction = 1;
	GetWorld()->GetTimerManager().SetTimer(FlipTimer,this, &AMovingFloor::FlipDirection,static_cast<float>(MaxRange) / MoveSpeed, true);
}

// Called every frame
void AMovingFloor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	AddActorLocalOffset(FVector(MoveSpeed * DeltaTime * Direction, 0, 0));
}

void AMovingFloor::FlipDirection() {
	Direction *= -1;
}

FTimerHandle을 이용해 매 프레임 검사하는게 아닌, 일정 시간이 지나면 반대로 진행방향을 바꿔주도록 구현했다. 타이머를 사용해야 하기에 타이머로 등록할 FTimerHandle FlipTimer와 그 타이머와 바인드될 함수인 FlipDirection()을 이용해 AddActorLocalOffset의 방향만 바꿔준다. 이에 따라 MoveDistance를 매 프레임마다 계산할 필요가 없어졌고, Tick에는 그저 앞으로 가는 코드만 있으면 반복하며 오고가는 발판을 만들 수 있다. 프레임과 타이머의 종료시점이 같다는 보장은 할 수 없기에, 약간의 오차는 있을 수도 있겠다. 이 외에도 시간이 지나면 객체를 delete함으로서 사라지는 발판, 일정시간동안만 열려있는 문같이 시간에 따른 로직을 만들 수도 있을 것이다.

랜덤 적용

다음으로 랜덤을 이용한 기능들을 구현해보자. 퍼즐같은 경로/방법의 연결성이 중요한 구조를 만들고자 한다면 랜덤 배치는 퍼즐 동선을 해치기에 적절하지 않을 것이다. 퍼즐 요소의 배치가 이상해서 깰 수 없는 퍼즐이 만들어지면 안되지 않겠는가? 원래대로면 절차적 생성 기법이 들어가야 하겠지만, 간단한 배치 방법 정도라도 일단 만들어보자. 먼저 간단한 수치 조절부터 만들어보자.

1
2
3
4
5
6
7
//RotatingFloor.cpp
// Called when the game starts or when spawned
void ARotatingFloor::BeginPlay()
{
	Super::BeginPlay();
	RotationSpeed = FMath::RandRange(RotationSpeed*0.5, RotationSpeed * 1.5);
}

BeginPlay()시점에 RotatingFloor의 회전 속도를 랜덤값으로 조정하도록 설정하였다. 기존에 설정해놓은 값을 기준으로 50%-150% 구간 안의 랜덤한 값으로 회전하게 될 것이다. 비슷한 구조로 Elevator, MovingFloor의 이동속도도 조정되도록 해놓았다. 그럼 랜덤 배치는 어떻게 만들까? 랜덤하게 경로를 생성해줄 RouteManager를 만들었다. 배치할 수 있는 엑터의 목록을 에디터에서 추가할 수 있도록 미리 해두었고, RouteManager는 자신의 위치에서부터 랜덤하게 같은 발판이 연속되지 않도록 정해진 횟수만큼 배치를 반복한다. 배치하는 지점은 SpawnCursor를 이용해 추적하고, 미리 입력된 배치한 액터로 이동 가능한 거리만큼씩 Spawn Cursor를 움직이면서 배치하기에 플레이어가 시도하면 넘어갈 수 있는 경로만 생성된다. 맨 마지막에는 도착점을 나타내는 다른 발판 액터를 배치하면서 마무리된다.

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
//RouteManager.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Elevator.h"
#include "RotatingFloor.h"
#include "MovingFloor.h"
#include "RouteManager.generated.h"

class UCapsuleComponent;

USTRUCT()
struct FActorInfo {
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, Category = RouteManager)	//actor to spawn
	TSubclassOf<class AActor> Actor;

	UPROPERTY(EditAnywhere, Category = RouteManager)	//how far from the spawn point when spawning
	int PlacePosition = 0;

	UPROPERTY(EditAnywhere, Category = RouteManager)	//next actor's spawn offset
	int AfterOffset = 0;

	UPROPERTY(EditAnywhere, Category = RouteManager)	//whether to spawn in ordinary process
	bool UsingNormalSpawn = true;

	UPROPERTY(EditAnywhere, Category = RouteManager)	//position used instead of the offset
	FVector PositionSubstitution = {0,0,0};

	UPROPERTY(EditAnywhere, Category = RouteManager)	//unit vector used instead of the existing direction.
	FVector DirectionSubstitution = {0,0,1};

	UPROPERTY(EditAnywhere, Category = RouteManager)	//whether the next direction can be any direction;
	bool AnyNextDirection = false;

	UPROPERTY(EditAnywhere, Category = RouteManager)	//How far player can move using the actor;
	int PathLength = 0;
};

UCLASS()
class ONLINELEARNINGKIT_API ARouteManager : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ARouteManager();

protected:
	UPROPERTY(EditAnywhere, Category = RouteManager)
	UCapsuleComponent* CapsuleComp;

	UPROPERTY(EditAnywhere, Category = RouteManager)
	TArray<FActorInfo> RouteActors;
	UPROPERTY(EditAnywhere, Category = RouteManager)
	TSubclassOf<class AActor> FinishPoint;

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
public:	

};
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
68
69
70
71
72
73
//RouteManager.cpp
#include "RouteManager.h"
#include "Components/CapsuleComponent.h"
#include "Algo/RandomShuffle.h"

// Sets default values
ARouteManager::ARouteManager()
{
	CapsuleComp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComp"));
	CapsuleComp->SetCollisionProfileName(FName("NoCollision"));
	RootComponent = CapsuleComp;
	
}

// Called when the game starts or when spawned
void ARouteManager::BeginPlay()
{
	Super::BeginPlay();

	//0:Elevator, 1:Moving Floor, 2:Rotating Floor
	int ActorNums = FMath::RandRange(4,6);
	FVector SpawnCursor = GetActorLocation();
	FVector Direction(0, 1, 0);
	int Offset = 0;
	UE_LOG(LogTemp, Warning, TEXT("Repeating for: %d"), ActorNums);
	for (int i = 0; i < ActorNums; i++) {

		//Choose random actor to spawn
		int which;
		if (i == 0) {
			which = FMath::RandRange(0, RouteActors.Num() - 1);
		}
		else {
			int NewNum = FMath::RandRange(0, RouteActors.Num() - 2);
			which = (NewNum < which ? NewNum : NewNum + 1);
		}
		UE_LOG(LogTemp, Log, TEXT("step %d's result: %d"), i, which);
		if (RouteActors[which].Actor == nullptr) {
			break;
		}

		DrawDebugLine(GetWorld(), SpawnCursor, SpawnCursor + Direction * Offset, FColor::Cyan, false, 60);
		SpawnCursor += Direction * Offset;	//Offset not to overlap the formal actor
		FVector SpawnPosition = (RouteActors[which].UsingNormalSpawn ? RouteActors[which].PlacePosition * Direction : RouteActors[which].PositionSubstitution);
		GetWorld()->SpawnActor<AActor>(RouteActors[which].Actor, SpawnCursor + SpawnPosition, FRotationMatrix::MakeFromX(Direction).Rotator());
		UE_LOG(LogTemp, Warning, TEXT("Spawned an actor at: %f %f %f"), (SpawnCursor + SpawnPosition).X, (SpawnCursor + SpawnPosition).Y, (SpawnCursor + SpawnPosition).Z);
		UE_LOG(LogTemp, Warning, TEXT("SpawnCursor: %f %f %f"), SpawnCursor.X, SpawnCursor.Y, SpawnCursor.Z);
		UE_LOG(LogTemp, Warning, TEXT("SpawnPosition: %f %f %f"), SpawnPosition.X, SpawnPosition.Y, SpawnPosition.Z);
		UE_LOG(LogTemp, Warning, TEXT("Direction: %f %f %f"), Direction.X, Direction.Y, Direction.Z);
		UE_LOG(LogTemp, Warning, TEXT("Offset: %d"), Offset);
		

		//draw debug line and setup info for the next spawn
		FVector Path = (RouteActors[which].UsingNormalSpawn ? Direction : RouteActors[which].DirectionSubstitution) * RouteActors[which].PathLength;
		DrawDebugLine(GetWorld(), SpawnCursor, SpawnCursor + Path, FColor::Red, false, 60);
		UE_LOG(LogTemp, Warning, TEXT("Drawed line from %f %f %f to %f %f %f"), SpawnCursor.X, SpawnCursor.Y, SpawnCursor.Z, (SpawnCursor + Path).X, (SpawnCursor + Path).Y, (SpawnCursor + Path).Z);
		SpawnCursor += Path;
		if (RouteActors[which].AnyNextDirection) {
			FVector NewDirrection[4] = { { 1,0,0 }, { 0,1,0 }, {-1,0,0 }, { 0,-1,0 } };
			Direction = NewDirrection[FMath::RandRange(0, 3)];
		}
		else {
			int NewDirection = FMath::RandRange(-1, 1);
			Direction = NewDirection == 0 ? Direction : NewDirection * FVector(Direction.Y, Direction.X, 0);
		}
		Offset = RouteActors[which].AfterOffset;
	}
	//spawn the floor at the endpoint
	if (FinishPoint != nullptr) {
		GetWorld()->SpawnActor<AActor>(FinishPoint, SpawnCursor + Direction * Offset, FRotator(0, 0, 0));
		UE_LOG(LogTemp, Warning, TEXT("Final Floor spawned"));
	}
}

완전 무작위적인 경로 생성이기 때문에 확률에 따라 특정 방향으로만 길어져 맵을 넘어가는 형태도 가능은 하겠지만 반복횟수를 6회까지로 막아놨기에 맵을 넘어가는 일은 당장은 없다. 반복 횟수를 더 늘려도 사진과 같이 쭉 이어지는 장애물 경로가 만들어진다. 위치에 대한 조건과 배치각도의 유연성, 배치될 액터들간의 확률 조정등도 넣으면 더 완성도있는 랜덤 경로 생성이 될 수도 있을것이다. image

오늘의 경험: 레벨 구성에 드는 노력과 랜덤 생성

학습과정 초반에 구성했던 것에 새로운 기능들을 추가하는 방식으로 해봤는데, 레벨을 구성하는 것은 생각보다 아이디어와 노력을 필요로 하는 일인 것을 다시 느낀다. 개발자 입장에서 기능적 데모를 위해서는 간단히 폴리곤들로만 플레이 가능한 레벨을 구성하는 것도 괜찮겠지만, 실제로 컨셉과 테마를 가지고 플레이어가 몰입할 수 있는 구조를 기획하고 만들어내는건 생각보다 많은 노력을 필요로 한다. 이번에 기존 무료 에셋을 배치해 적당한 환경을 만드는 것만 해도 원하는만큼 SF스럽지 않다는 점에서 적절한 에셋들의 선택과 배치, 활용은 다르게 연습해야 하는 일이겠다고 생각이 든다. 한편 단순 랜덤 배치로 맘에 드는 결과물을 만드는 것도 꽤나 난관일 수도 있다는 생각이 들었다. 처음에는 시작점과 도착점을 잇는 경로에 대해 랜덤한 배치를 통해 이동 가능한 경로를 구성하는 코드를 구성하려고 했다. 대략적인 흐름은 다음과 같았다.

  1. 시작점과 도착점 사이의 거리를 벡터로 계산하고, 각 성분이 플레이어가 이동할 수 있을만큼만 남을 때까지 배치할 수 있는 액터들을 배열에 넣어놓는다.
  2. 배열을 무작위 순서로 섞는다.
  3. 액터를 레벨에 스폰한다.

방법론 자체는 크게 문제는 없다. 각 액터를 통한 이동을 수학적 벡터로 생각할 때, 모든 벡터의 총합이 시작점에서 도착점 사이의 벡터와 같다면 이동이 가능한 것일 테니 말이다. 문제가 있다면 지금 액터의 배치간에 플레이어가 이동할 수 없는 조건이 있다는 것이다. 엘레베이터가 연속되서 2개가 위아래로 배치되면 플레이어가 갈아탈 수 있을까? 아니면 경로를 더 복잡하게 만들기 위해서 정방향이 아닌 역방향, 돌아서 가는 길까지 생성한다면 특정 방향으로 진행하게 액터를 배치 후 곧바로 반대방향으로 가게 할 액터를 배치해도 될까? 플레이어의 이동방법 자체가 제약으로 작용하기에 엑터의 배치에도 제약조건이 붙어야하고, 이는 단순 무작위 셔플이 적용될 수 없게 만든다. 경우에 따라서는 아예 이동이 불가능한 경로도 존재할 것이다. 시작점 바로 위쪽 멀리로 도착점이 존재해 Elevator만 여러개 배치해야한다면 단순 배치로는 해결이 안될 것이다. 때문에 제출할 결과물에선 도착 지점도 랜덤으로 생성하도록 바꾸었다. 오히려 시작점과 도착점을 정해놓은 강한 규칙이 경로 생성 과정을 많이 망가뜨렸다는 생각이 들기도 한다. 완전 랜덤으로 생성하면서도 자연스럽고 의도된듯한 구조가 만들어지길 원한다면 지금보다 제약조건을 더 만들고 반영할 수 있는 구조를 만들거나, 몇가지 배치 조합을 미리 프리팹처럼 만들어 놓는 방법도 생각해 볼 수 있겠다.

This post is licensed under CC BY 4.0 by the author.