/* * 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(); root->Init(def); RoomList.Add(root); // Create the list with the correct mode (depth or breadth) TQueueOrStack::EMode listMode; switch(GenerationType) { case EGenerationType::DFS: listMode = TQueueOrStack::EMode::STACK; break; case EGenerationType::BFS: listMode = TQueueOrStack::EMode::QUEUE; break; } // Build the list of rooms TQueueOrStack 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 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(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 ADungeonGenerator::AddNewRooms(URoom& ParentRoom) { TArray 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(); 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 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 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 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 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 RoomType) { return CountRoomType(RoomType) > 0; } bool ADungeonGenerator::HasAlreadyOneRoomTypeFrom(TArray> RoomTypeList) { return CountTotalRoomType(RoomTypeList) > 0; } int ADungeonGenerator::CountRoomType(TSubclassOf 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> RoomTypeList) { int count = 0; for (int i = 0; i < RoomList.Num(); i++) { URoomData* roomData = RoomList[i]->GetRoomData(); if (RoomTypeList.ContainsByPredicate([&roomData](const TSubclassOf roomType) { return roomData->GetClass()->IsChildOf(roomType); } )) { count++; } } return count; } void ADungeonGenerator::SetSeed(int32 NewSeed) { Seed = static_cast(NewSeed); } int32 ADungeonGenerator::GetSeed() { return static_cast(Seed); }