This repository has been archived on 2021-12-21. You can view files and clone it, but cannot push or open issues or pull requests.
Bookworm/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/DungeonGenerator.cpp

591 lines
14 KiB
C++

/*
* MIT License
*
* Copyright (c) 2019-2021 Benoit Pelletier
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "DungeonGenerator.h"
#include "Engine/World.h"
#include "Engine.h"
#include "NavigationSystem.h"
#include "ProceduralLevelStreaming.h"
#include "RoomData.h"
#include "Room.h"
#include "Door.h"
#include "RoomLevel.h"
#include "ProceduralDungeon.h"
#include "ProceduralDungeonSettings.h"
#include "ProceduralDungeonLog.h"
#include "QueueOrStack.h"
// Sets default values
ADungeonGenerator::ADungeonGenerator()
{
// 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;
GenerationType = EGenerationType::DFS;
SeedType = ESeedType::Random;
Seed = 123456789; // default Seed
bAlwaysRelevant = true;
bReplicates = true;
NetPriority = 10.0f;
NetUpdateFrequency = 10;
}
// Called when the game starts or when spawned
void ADungeonGenerator::BeginPlay()
{
Super::BeginPlay();
}
void ADungeonGenerator::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
UnloadAllRooms();
}
// Called every frame
void ADungeonGenerator::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
OnStateTick(CurrentState);
}
void ADungeonGenerator::Generate()
{
// Do it only on server, do nothing on clients
if (HasAuthority())
{
if (SeedType == ESeedType::Random)
{
Random.GenerateNewSeed();
Seed = Random.GetCurrentSeed();
}
BeginGeneration(Seed);
if (SeedType == ESeedType::AutoIncrement)
{
Seed += 123456;
}
}
}
void ADungeonGenerator::BeginGeneration_Implementation(uint32 GenerationSeed)
{
Seed = GenerationSeed;
Random.Initialize(Seed);
LogInfo(FString::Printf(TEXT("Seed: %d"), Seed));
SetState(EGenerationState::Unload);
}
void ADungeonGenerator::CreateDungeon()
{
IsInit = false;
NbInitRoom = 0;
int TriesLeft = MaxTry;
// generate level until there IsValidDungeon return true
do {
TriesLeft--;
// Reset generation data
UProceduralLevelStreaming::UniqueLevelInstanceId = 0;
ARoomLevel::Count = 0;
DispatchGenerationInit();
// Create the first room
RoomList.Empty();
URoomData* def = ChooseFirstRoomData();
if(!IsValid(def))
{
LogError("ChooseFirstRoomData returned null.");
continue;
}
URoom* root = NewObject<URoom>();
root->Init(def);
RoomList.Add(root);
// Create the list with the correct mode (depth or breadth)
TQueueOrStack<URoom*>::EMode listMode;
switch(GenerationType)
{
case EGenerationType::DFS:
listMode = TQueueOrStack<URoom*>::EMode::STACK;
break;
case EGenerationType::BFS:
listMode = TQueueOrStack<URoom*>::EMode::QUEUE;
break;
}
// Build the list of rooms
TQueueOrStack<URoom*> roomStack(listMode);
roomStack.Push(root);
URoom* currentRoom = nullptr;
URoom* newRoom = nullptr;
while(ContinueToAddRoom() && !roomStack.IsEmpty())
{
currentRoom = roomStack.Pop();
check(IsValid(currentRoom)); // currentRoom should always be valid
for(URoom* room : AddNewRooms(*currentRoom))
{
roomStack.Push(room);
}
}
} while (TriesLeft > 0 && !IsValidDungeon());
}
void ADungeonGenerator::InstantiateRoom(URoom* Room)
{
// Instantiate room
Room->Instantiate(GetWorld());
for (int i = 0; i < Room->GetConnectionCount(); i++)
{
// Get next room
URoom* r = Room->GetConnection(i).Get();
FIntVector DoorCell = Room->GetDoorWorldPosition(i);
EDoorDirection DoorRot = Room->GetDoorWorldOrientation(i);
int j = Room->GetOtherDoorIndex(i);
// Don't instantiate door if it's the parent
if (!Room->IsDoorInstanced(i))
{
TSubclassOf<ADoor> DoorClass = ChooseDoor(Room->GetRoomData(), nullptr != r ? r->GetRoomData() : nullptr);
if (DoorClass != nullptr)
{
FVector InstanceDoorPos = URoom::GetRealDoorPosition(DoorCell, DoorRot);
FRotator InstanceDoorRot = FRotator(0, -90 * (int8)DoorRot, 0);
ADoor* Door = GetWorld()->SpawnActor<ADoor>(DoorClass, InstanceDoorPos, InstanceDoorRot);
if (nullptr != Door)
{
DoorList.Add(Door);
Door->SetConnectingRooms(Room, r);
Room->SetDoorInstance(i, Door);
if(IsValid(r))
{
r->SetDoorInstance(j, Door);
}
}
else
{
LogError("Failed to spawn Door, make sure you set door actor to always spawning.");
}
}
}
}
}
TArray<URoom*> ADungeonGenerator::AddNewRooms(URoom& ParentRoom)
{
TArray<URoom*> newRooms;
int nbDoor = ParentRoom.GetRoomData()->GetNbDoor();
URoom* newRoom = nullptr;
for(int i = 0; i < nbDoor; ++i)
{
if(ParentRoom.IsConnected(i))
continue;
int nbTries = MaxRoomTry;
// Try to place a new room
do
{
nbTries--;
URoomData* def = ChooseNextRoomData(ParentRoom.GetRoomData());
if(!IsValid(def))
{
LogError("ChooseNextRoomData returned null.");
continue;
}
// Create room from roomdef and set connections with current room
newRoom = NewObject<URoom>();
newRoom->Init(def);
int doorIndex = def->RandomDoor ? Random.RandRange(0, newRoom->GetRoomData()->GetNbDoor() - 1) : 0;
// Place the room at its world position with the correct rotation
EDoorDirection parentDoorDir = ParentRoom.GetDoorWorldOrientation(i);
FIntVector newRoomPos = ParentRoom.GetDoorWorldPosition(i) + URoom::GetDirection(parentDoorDir);
newRoom->SetPositionAndRotationFromDoor(doorIndex, newRoomPos, URoom::Opposite(parentDoorDir));
// Test if it fit in the place
if(!URoom::Overlap(*newRoom, RoomList))
{
// connect the doors to all possible existing rooms
URoom::Connect(*newRoom, doorIndex, ParentRoom, i);
if(URoom::CanLoop())
{
newRoom->TryConnectToExistingDoors(RoomList);
}
RoomList.Add(newRoom);
newRooms.Add(newRoom);
DispatchRoomAdded(newRoom->GetRoomData());
}
else
{
newRoom = nullptr;
}
} while(nbTries > 0 && newRoom == nullptr);
}
return newRooms;
}
void ADungeonGenerator::LoadAllRooms()
{
// When a level is correct, load all rooms
for (int i = 0; i < RoomList.Num(); i++)
{
InstantiateRoom(RoomList[i]);
}
}
void ADungeonGenerator::UnloadAllRooms()
{
for (int i = 0; i < DoorList.Num(); i++)
{
DoorList[i]->Destroy();
}
DoorList.Empty();
for (int i = 0; i < RoomList.Num(); i++)
{
RoomList[i]->Destroy(GetWorld());
}
}
/*
* =======================================
* State Machine
* =======================================
*/
void ADungeonGenerator::SetState(EGenerationState NewState)
{
OnStateEnd(CurrentState);
CurrentState = NewState;
OnStateBegin(CurrentState);
}
void ADungeonGenerator::OnStateBegin(EGenerationState State)
{
switch (State)
{
case EGenerationState::Unload:
LogInfo("======= Begin Unload All Levels =======");
UnloadAllRooms();
NbUnloadedRoom = 0;
break;
case EGenerationState::Generation:
DispatchPreGeneration();
LogInfo("======= Begin Map Generation =======");
CreateDungeon();
break;
case EGenerationState::Load:
LogInfo("======= Begin Load All Levels =======");
LoadAllRooms();
NbLoadedRoom = 0;
break;
case EGenerationState::Initialization:
LogInfo("======= Begin Init All Levels =======");
LogInfo(FString::Printf(TEXT("Nb Room To Initialize: %d"), RoomList.Num()));
break;
default:
break;
}
}
void ADungeonGenerator::OnStateTick(EGenerationState State)
{
int tmp = 0;
switch (State)
{
case EGenerationState::Unload:
// Count nb level loaded
for (int i = 0; i < RoomList.Num(); i++)
{
if (RoomList[i]->IsInstanceUnloaded())
{
tmp++;
}
}
// Change state when all levels are loaded
if (tmp == RoomList.Num())
{
SetState(EGenerationState::Generation);
}
break;
case EGenerationState::Generation:
SetState(EGenerationState::Load);
break;
case EGenerationState::Load:
// Count nb level loaded
for (int i = 0; i < RoomList.Num(); i++)
{
if (RoomList[i]->IsInstanceLoaded())
{
tmp++;
}
}
// Change state when all levels are loaded
if (tmp == RoomList.Num())
{
SetState(EGenerationState::Initialization);
}
break;
case EGenerationState::Initialization:
// While initialization isn't done, try to initialize all rooms
if (!IsInit)
{
IsInit = true;
for (URoom* room : RoomList)
{
ARoomLevel* script = room->GetLevelScript();
if (nullptr != script)
{
IsInit &= script->IsInit;
if (!script->IsInit && !script->PendingInit)
{
NbInitRoom++;
script->Room = nullptr;
script->Init(room);
LogInfo(FString::Printf(TEXT("Room Initialization: %d/%d"), NbInitRoom, RoomList.Num()), false);
}
}
else
{
IsInit = false;
}
}
if (IsInit)
{
SetState(EGenerationState::None);
}
return;
}
break;
default:
break;
}
}
void ADungeonGenerator::OnStateEnd(EGenerationState State)
{
FTimerHandle handle;
UNavigationSystemV1* nav = nullptr;
switch (State)
{
case EGenerationState::Unload:
RoomList.Empty();
GetWorld()->FlushLevelStreaming();
GEngine->ForceGarbageCollection(true);
LogInfo("======= End Unload All Levels =======");
break;
case EGenerationState::Generation:
LogInfo("======= End Map Generation =======");
break;
case EGenerationState::Load:
LogInfo("======= End Load All Levels =======");
break;
case EGenerationState::Initialization:
LogInfo("======= End Init All Levels =======");
// Try to rebuild the navmesh
nav = UNavigationSystemV1::GetCurrent(GetWorld());
if (nullptr != nav)
{
LogInfo("Rebuild navmesh");
nav->CancelBuild();
nav->Build();
}
// Invoke Post Generation Event when initialization is done
DispatchPostGeneration();
break;
default:
break;
}
}
URoomData* ADungeonGenerator::ChooseFirstRoomData_Implementation()
{
LogError("Error: ChooseFirstRoomData not implemented");
return nullptr;
}
URoomData* ADungeonGenerator::ChooseNextRoomData_Implementation(URoomData* CurrentRoom)
{
LogError("Error: ChooseNextRoomData not implemented");
return nullptr;
}
TSubclassOf<ADoor> ADungeonGenerator::ChooseDoor_Implementation(URoomData* CurrentRoom, URoomData* NextRoom)
{
LogError("Error: ChooseDoor not implemented");
return nullptr;
}
bool ADungeonGenerator::IsValidDungeon_Implementation()
{
LogError("Error: IsValidDungeon not implemented");
return false;
}
bool ADungeonGenerator::ContinueToAddRoom_Implementation()
{
LogError("Error: ContinueToAddRoom not implemented");
return false;
}
void ADungeonGenerator::DispatchPreGeneration()
{
OnPreGeneration();
OnPreGeneration_BP();
OnPreGenerationEvent.Broadcast();
}
void ADungeonGenerator::DispatchPostGeneration()
{
OnPostGeneration();
OnPostGeneration_BP();
OnPostGenerationEvent.Broadcast();
}
void ADungeonGenerator::DispatchGenerationInit()
{
OnGenerationInit();
OnGenerationInit_BP();
OnGenerationInitEvent.Broadcast();
}
void ADungeonGenerator::DispatchRoomAdded(URoomData* NewRoom)
{
OnRoomAdded(NewRoom);
OnRoomAdded_BP(NewRoom);
OnRoomAddedEvent.Broadcast(NewRoom);
}
URoomData* ADungeonGenerator::GetRandomRoomData(TArray<URoomData*> RoomDataArray)
{
int n = Random.RandRange(0, RoomDataArray.Num() - 1);
return RoomDataArray[n];
}
URoom* ADungeonGenerator::GetRoomAt(FIntVector RoomCell)
{
return URoom::GetRoomAt(RoomCell, RoomList);
}
bool ADungeonGenerator::HasAlreadyRoomData(URoomData* RoomData)
{
return CountRoomData(RoomData) > 0;
}
bool ADungeonGenerator::HasAlreadyOneRoomDataFrom(TArray<URoomData*> RoomDataList)
{
return CountTotalRoomData(RoomDataList) > 0;
}
int ADungeonGenerator::CountRoomData(URoomData* RoomData)
{
int count = 0;
for(int i = 0; i < RoomList.Num(); i++)
{
if(RoomList[i]->GetRoomData() == RoomData)
{
count++;
}
}
return count;
}
int ADungeonGenerator::CountTotalRoomData(TArray<URoomData*> RoomDataList)
{
int count = 0;
for(int i = 0; i < RoomList.Num(); i++)
{
if(RoomDataList.Contains(RoomList[i]->GetRoomData()))
{
count++;
}
}
return count;
}
bool ADungeonGenerator::HasAlreadyRoomType(TSubclassOf<URoomData> RoomType)
{
return CountRoomType(RoomType) > 0;
}
bool ADungeonGenerator::HasAlreadyOneRoomTypeFrom(TArray<TSubclassOf<URoomData>> RoomTypeList)
{
return CountTotalRoomType(RoomTypeList) > 0;
}
int ADungeonGenerator::CountRoomType(TSubclassOf<URoomData> RoomType)
{
int count = 0;
for (int i = 0; i < RoomList.Num(); i++)
{
if (RoomList[i]->GetRoomData()->GetClass()->IsChildOf(RoomType))
{
count++;
}
}
return count;
}
int ADungeonGenerator::CountTotalRoomType(TArray<TSubclassOf<URoomData>> RoomTypeList)
{
int count = 0;
for (int i = 0; i < RoomList.Num(); i++)
{
URoomData* roomData = RoomList[i]->GetRoomData();
if (RoomTypeList.ContainsByPredicate([&roomData](const TSubclassOf<URoomData> roomType) { return roomData->GetClass()->IsChildOf(roomType); } ))
{
count++;
}
}
return count;
}
void ADungeonGenerator::SetSeed(int32 NewSeed)
{
Seed = static_cast<uint32>(NewSeed);
}
int32 ADungeonGenerator::GetSeed()
{
return static_cast<int32>(Seed);
}