Ajout du projet Depths sur Git
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "BoundsParams.h"
|
||||
|
||||
FBoxMinAndMax FBoundsParams::GetBox() const
|
||||
{
|
||||
return FBoxMinAndMax(
|
||||
FIntVector(
|
||||
(bLimitMinX) ? -MinX : INT32_MIN,
|
||||
(bLimitMinY) ? -MinY : INT32_MIN,
|
||||
(bLimitMinZ) ? -MinZ : INT32_MIN
|
||||
),
|
||||
FIntVector(
|
||||
(bLimitMaxX) ? MaxX + 1 : INT32_MAX,
|
||||
(bLimitMaxY) ? MaxY + 1 : INT32_MAX,
|
||||
(bLimitMaxZ) ? MaxZ + 1 : INT32_MAX
|
||||
)
|
||||
);
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Components/DeterministicRandomComponent.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "DungeonBlueprintLibrary.h"
|
||||
#include "Room.h"
|
||||
#include "Interfaces/RoomActorGuid.h"
|
||||
#include "DungeonGeneratorBase.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Utility functions to xor 2 guids
|
||||
FGuid operator^(const FGuid& A, const FGuid& B)
|
||||
{
|
||||
return FGuid(A.A ^ B.A, A.B ^ B.B, A.C ^ B.C, A.D ^ B.D);
|
||||
}
|
||||
|
||||
void operator^=(FGuid& A, const FGuid& B)
|
||||
{
|
||||
A = A ^ B;
|
||||
}
|
||||
} //namespace
|
||||
|
||||
UDeterministicRandomComponent::UDeterministicRandomComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UDeterministicRandomComponent::OnRegister()
|
||||
{
|
||||
Super::OnRegister();
|
||||
|
||||
AActor* OwnerActor = GetOwner();
|
||||
const int32 Seed = GenerateDeterministicSeed(OwnerActor);
|
||||
Random.Initialize(Seed);
|
||||
DungeonLog_Debug("[%s] Initial seed set to: %d.", *GetNameSafe(OwnerActor), Random.GetInitialSeed());
|
||||
}
|
||||
|
||||
// *WARNING*: Updating the algorithm may break systems relying on this generated seed.
|
||||
// The best approach is to not rely on this generated seed.
|
||||
int32 UDeterministicRandomComponent::GenerateDeterministicSeed(AActor* Actor)
|
||||
{
|
||||
FGuid Guid;
|
||||
int64 Salt = 0;
|
||||
|
||||
// Use the guid of the actor if available.
|
||||
UObject* GuidImplementer = IRoomActorGuid::GetImplementer(Actor);
|
||||
if (IsValid(GuidImplementer))
|
||||
{
|
||||
Guid = IRoomActorGuid::Execute_GetGuid(GuidImplementer);
|
||||
}
|
||||
|
||||
// Get the room ID as the salt if available.
|
||||
if (const URoom* OwningRoom = UDungeonBlueprintLibrary::GetOwningRoom(Actor))
|
||||
{
|
||||
Salt = OwningRoom->GetRoomID();
|
||||
|
||||
// Will also use the generator guid and seed if relevant.
|
||||
if (const ADungeonGeneratorBase* Generator = OwningRoom->Generator())
|
||||
{
|
||||
Guid ^= Generator->GetGuid();
|
||||
|
||||
const int64 Seed = static_cast<int64>(Generator->GetSeed());
|
||||
Salt ^= Seed << 32;
|
||||
}
|
||||
}
|
||||
|
||||
return Random::Guid2Seed(Guid, Salt);
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Components/DoorComponent.h"
|
||||
#include "Room.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "RoomConnection.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "DungeonGenerator.h"
|
||||
#include "DoorType.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "Utils/ReplicationUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
|
||||
UDoorComponent::UDoorComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
bTickInEditor = true;
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UDoorComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
FDoRepLifetimeParams Params;
|
||||
Params.bIsPushBased = true;
|
||||
DOREPLIFETIME_WITH_PARAMS(UDoorComponent, RoomConnection, Params);
|
||||
}
|
||||
|
||||
// Called every frame
|
||||
void UDoorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
const URoom* RoomA = GetRoomA();
|
||||
const URoom* RoomB = GetRoomB();
|
||||
|
||||
// Tells if the door actor has been spawned by the dungeon generator or not.
|
||||
// At least one of the room is valid when spawned by the dungeon generator.
|
||||
// Both rooms are invalid if door has been spawned by another way.
|
||||
const bool bSpawnedByDungeon = IsValid(RoomA) || IsValid(RoomB);
|
||||
|
||||
// The door manages itself its own visibility only when it has been spawned by the dungeon generator.
|
||||
// If the door is placed in a RoomLevel or spawned by the user in other means, it is the responsibility
|
||||
// of the RoomLevel or the user to manage the door's visibility.
|
||||
if (bSpawnedByDungeon)
|
||||
{
|
||||
const bool bRoomAVisible = IsValid(RoomA) && RoomA->IsVisible();
|
||||
const bool bRoomBVisible = IsValid(RoomB) && RoomB->IsVisible();
|
||||
|
||||
// Update door visibility
|
||||
// A door is hidden ONLY when ALL those conditions are met:
|
||||
// - The Room Culling is enabled.
|
||||
// - The door is not `Always Visible`.
|
||||
// - Both connected rooms are not visible.
|
||||
// @TODO: this should not work with multiplayer games, because bHidden is replicated!
|
||||
// It works only because it is updated each frame on clients too!
|
||||
// The behavior will change if bHidden is updated once in a wile by an event instead!
|
||||
// So, I should find another way to hide the actor... (avoiding if possible RootComponent::SetVisible)
|
||||
AActor* Owner = GetOwner();
|
||||
if (IsValid(Owner))
|
||||
{
|
||||
Owner->SetActorHiddenInGame(Dungeon::OcclusionCulling()
|
||||
&& !bAlwaysVisible
|
||||
&& !(bRoomAVisible || bRoomBVisible)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update door's lock state
|
||||
// A door is locked when ALL those conditions are met:
|
||||
// - The door is not `Always Unlocked`.
|
||||
// - The user tells the door should be locked.
|
||||
// - The door is spawned by the dungeon generator AND one of the connected rooms is locked or missing.
|
||||
const bool bPrevLocked = bLocked;
|
||||
const bool bRoomALocked = !IsValid(RoomA) || RoomA->IsLocked();
|
||||
const bool bRoomBLocked = !IsValid(RoomB) || RoomB->IsLocked();
|
||||
bLocked = !bAlwaysUnlocked && (ShouldBeLocked() || (bSpawnedByDungeon && (bRoomALocked || bRoomBLocked)));
|
||||
|
||||
if (bLocked != bPrevLocked)
|
||||
{
|
||||
DungeonLog_Debug("Door %s locked: %d", *GetNameSafe(this), bLocked);
|
||||
OnDoorLock(bLocked);
|
||||
OnDoorLock_BP(bLocked);
|
||||
OnDoorLocked.Broadcast(this, bLocked);
|
||||
}
|
||||
|
||||
// Update door's open state
|
||||
const bool bPrevIsOpen = bIsOpen;
|
||||
bIsOpen = ShouldBeOpen() && !bLocked;
|
||||
if (bIsOpen != bPrevIsOpen)
|
||||
{
|
||||
DungeonLog_Debug("Door %s open: %d", *GetNameSafe(this), bIsOpen);
|
||||
OnDoorOpen(bIsOpen);
|
||||
OnDoorOpen_BP(bIsOpen);
|
||||
OnDoorOpened.Broadcast(this, bIsOpen);
|
||||
}
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
// TODO: Place it in an editor module of the plugin
|
||||
if (Dungeon::DrawDebug() && GetWorld()->WorldType == EWorldType::EditorPreview)
|
||||
{
|
||||
FDoorDef DoorDef;
|
||||
DoorDef.Direction = EDoorDirection::NbDirection;
|
||||
DoorDef.Type = Type;
|
||||
FDoorDef::DrawDebug(GetWorld(), DoorDef, FVector::ZeroVector);
|
||||
}
|
||||
#endif // ENABLE_DRAW_DEBUG
|
||||
}
|
||||
|
||||
void UDoorComponent::SetRoomConnection_Implementation(URoomConnection* InRoomConnection)
|
||||
{
|
||||
check(OwnerHasAuthority());
|
||||
SET_COMPONENT_REPLICATED_PROPERTY_VALUE(RoomConnection, InRoomConnection);
|
||||
}
|
||||
|
||||
void UDoorComponent::Open(bool bOpen)
|
||||
{
|
||||
if (!OwnerHasAuthority())
|
||||
return;
|
||||
|
||||
if (!IsValid(RoomConnection))
|
||||
return;
|
||||
|
||||
RoomConnection->SetDoorOpen(bOpen);
|
||||
}
|
||||
|
||||
void UDoorComponent::Lock(bool bLock)
|
||||
{
|
||||
if (!OwnerHasAuthority())
|
||||
return;
|
||||
|
||||
if (!IsValid(RoomConnection))
|
||||
return;
|
||||
|
||||
RoomConnection->SetDoorLocked(bLock);
|
||||
}
|
||||
|
||||
bool UDoorComponent::ShouldBeOpen() const
|
||||
{
|
||||
if (!IsValid(RoomConnection))
|
||||
return false;
|
||||
|
||||
return RoomConnection->IsDoorOpen();
|
||||
}
|
||||
|
||||
bool UDoorComponent::ShouldBeLocked() const
|
||||
{
|
||||
if (!IsValid(RoomConnection))
|
||||
return false;
|
||||
|
||||
return RoomConnection->IsDoorLocked();
|
||||
}
|
||||
|
||||
URoom* UDoorComponent::GetRoomA() const
|
||||
{
|
||||
if (!IsValid(RoomConnection))
|
||||
return nullptr;
|
||||
|
||||
return RoomConnection->GetRoomA().Get();
|
||||
}
|
||||
|
||||
URoom* UDoorComponent::GetRoomB() const
|
||||
{
|
||||
if (!IsValid(RoomConnection))
|
||||
return nullptr;
|
||||
|
||||
return RoomConnection->GetRoomB().Get();
|
||||
}
|
||||
|
||||
bool UDoorComponent::OwnerHasAuthority() const
|
||||
{
|
||||
AActor* Owner = GetOwner();
|
||||
if (!IsValid(Owner))
|
||||
return false;
|
||||
|
||||
return Owner->HasAuthority();
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Components/RoomObserverComponent.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "RoomLevel.h"
|
||||
|
||||
URoomObserverComponent::URoomObserverComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void URoomObserverComponent::OnRoomEnter_Implementation(ARoomLevel* RoomLevel)
|
||||
{
|
||||
DungeonLog_Debug("[Observer] '%s' Enters Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
|
||||
BindToLevel(RoomLevel, true);
|
||||
}
|
||||
|
||||
void URoomObserverComponent::OnRoomExit_Implementation(ARoomLevel* RoomLevel)
|
||||
{
|
||||
DungeonLog_Debug("[Observer] '%s' Exits Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
|
||||
BindToLevel(RoomLevel, false);
|
||||
}
|
||||
|
||||
void URoomObserverComponent::OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Actor)
|
||||
{
|
||||
// Just forward the call to the delegate.
|
||||
ActorEnterRoomEvent.Broadcast(RoomLevel, Actor);
|
||||
}
|
||||
|
||||
void URoomObserverComponent::OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Actor)
|
||||
{
|
||||
// Just forward the call to the delegate.
|
||||
ActorExitRoomEvent.Broadcast(RoomLevel, Actor);
|
||||
}
|
||||
|
||||
void URoomObserverComponent::BindToLevel(ARoomLevel* RoomLevel, bool Bind)
|
||||
{
|
||||
if (BoundLevels.Contains(RoomLevel) == Bind)
|
||||
return;
|
||||
|
||||
if (!IsValid(RoomLevel))
|
||||
return;
|
||||
|
||||
if (Bind)
|
||||
{
|
||||
RoomLevel->ActorEnterRoomEvent.AddDynamic(this, &URoomObserverComponent::OnActorEnterRoom);
|
||||
RoomLevel->ActorExitRoomEvent.AddDynamic(this, &URoomObserverComponent::OnActorExitRoom);
|
||||
BoundLevels.Add(RoomLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
RoomLevel->ActorEnterRoomEvent.RemoveDynamic(this, &URoomObserverComponent::OnActorEnterRoom);
|
||||
RoomLevel->ActorExitRoomEvent.RemoveDynamic(this, &URoomObserverComponent::OnActorExitRoom);
|
||||
BoundLevels.Remove(RoomLevel);
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Components/SimpleGuidComponent.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
|
||||
#if GUID_DEBUG_ENABLED
|
||||
#define LOG_GUID_INFO(...) DungeonLog_InfoSilent(##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_GUID_INFO(...)
|
||||
#endif
|
||||
|
||||
USimpleGuidComponent::USimpleGuidComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] Construct Component", *GetNameSafe(GetOwner()));
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::OnRegister()
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] Register Component", *GetNameSafe(GetOwner()));
|
||||
Super::OnRegister();
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
if (!Guid.IsValid() && IsValid(GetOwner()))
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
Guid = GetOwner()->GetActorGuid();
|
||||
LOG_GUID_INFO("-- Guid Not Valid! Retrieving Guid from actor: %s", *Guid.ToString());
|
||||
#else
|
||||
LOG_GUID_INFO("-- Guid Not Valid! Can't retreive guid from actor!");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
FGuid USimpleGuidComponent::GetGuid_Implementation() const
|
||||
{
|
||||
return Guid;
|
||||
}
|
||||
|
||||
bool USimpleGuidComponent::ShouldSaveActor_Implementation() const
|
||||
{
|
||||
return bSaveActorInDungeon;
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::Serialize(FArchive& Ar)
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] Serialize(Ar) Component (%s)", *GetNameSafe(GetOwner()), Ar.IsLoading() ? TEXT("load") : TEXT("save"));
|
||||
Super::Serialize(Ar);
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::Serialize(FStructuredArchive::FRecord Record)
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] Serialize(Record) Component", *GetNameSafe(GetOwner()));
|
||||
Super::Serialize(Record);
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
#if GUID_DEBUG_ENABLED
|
||||
void USimpleGuidComponent::PostInitProperties()
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] PostInitProperties Component", *GetNameSafe(GetOwner()));
|
||||
Super::PostInitProperties();
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::PreSave(FObjectPreSaveContext SaveContext)
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] PreSave Component", *GetNameSafe(GetOwner()));
|
||||
Super::PreSave(SaveContext);
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::PostLoad()
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] PostLoad Component", *GetNameSafe(GetOwner()));
|
||||
Super::PostLoad();
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::OnComponentCreated()
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] Created Component", *GetNameSafe(GetOwner()));
|
||||
Super::OnComponentCreated();
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::InitializeComponent()
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] Initialize Component", *GetNameSafe(GetOwner()));
|
||||
Super::InitializeComponent();
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
|
||||
void USimpleGuidComponent::BeginPlay()
|
||||
{
|
||||
LOG_GUID_INFO("[%s.SimpleGuidComponent] BeginPlay Component", *GetNameSafe(GetOwner()));
|
||||
Super::BeginPlay();
|
||||
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
|
||||
}
|
||||
#endif
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Components/StaticRoomObserverComponent.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "Engine/Level.h"
|
||||
|
||||
UStaticRoomObserverComponent::UStaticRoomObserverComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UStaticRoomObserverComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
BindToLevel(true);
|
||||
}
|
||||
|
||||
void UStaticRoomObserverComponent::EndPlay(EEndPlayReason::Type Reason)
|
||||
{
|
||||
Super::EndPlay(Reason);
|
||||
BindToLevel(false);
|
||||
}
|
||||
|
||||
void UStaticRoomObserverComponent::OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Actor)
|
||||
{
|
||||
// Just forward the call to the delegate.
|
||||
ActorEnterRoomEvent.Broadcast(RoomLevel, Actor);
|
||||
}
|
||||
|
||||
void UStaticRoomObserverComponent::OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Actor)
|
||||
{
|
||||
// Just forward the call to the delegate.
|
||||
ActorExitRoomEvent.Broadcast(RoomLevel, Actor);
|
||||
}
|
||||
|
||||
void UStaticRoomObserverComponent::BindToLevel(bool Bind)
|
||||
{
|
||||
if (bBound == Bind)
|
||||
return;
|
||||
|
||||
ULevel* Level = GetComponentLevel();
|
||||
check(IsValid(Level));
|
||||
ARoomLevel* RoomLevel = Cast<ARoomLevel>(Level->GetLevelScriptActor());
|
||||
if (!IsValid(RoomLevel))
|
||||
return;
|
||||
|
||||
if (Bind)
|
||||
{
|
||||
RoomLevel->ActorEnterRoomEvent.AddDynamic(this, &UStaticRoomObserverComponent::OnActorEnterRoom);
|
||||
RoomLevel->ActorExitRoomEvent.AddDynamic(this, &UStaticRoomObserverComponent::OnActorExitRoom);
|
||||
}
|
||||
else
|
||||
{
|
||||
RoomLevel->ActorEnterRoomEvent.RemoveDynamic(this, &UStaticRoomObserverComponent::OnActorEnterRoom);
|
||||
RoomLevel->ActorExitRoomEvent.RemoveDynamic(this, &UStaticRoomObserverComponent::OnActorExitRoom);
|
||||
}
|
||||
bBound = Bind;
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Components/StaticRoomVisibilityComponent.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "RoomLevel.h"
|
||||
|
||||
UStaticRoomVisibilityComponent::UStaticRoomVisibilityComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
UpdateVisibility();
|
||||
RegisterVisibilityDelegate(GetOwnerRoomLevel(), true);
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::EndPlay(EEndPlayReason::Type Reason)
|
||||
{
|
||||
Super::EndPlay(Reason);
|
||||
RegisterVisibilityDelegate(GetOwnerRoomLevel(), false);
|
||||
}
|
||||
|
||||
bool UStaticRoomVisibilityComponent::IsVisible()
|
||||
{
|
||||
return (Dungeon::OcclusionCulling() && Dungeon::OccludeDynamicActors()) ? VisibilityEnablers.Num() > 0 : true;
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::SetVisible(UObject* Owner, bool Visible)
|
||||
{
|
||||
const bool bOldVisible = IsVisible();
|
||||
if (Visible)
|
||||
VisibilityEnablers.Add(Owner);
|
||||
else
|
||||
VisibilityEnablers.Remove(Owner);
|
||||
|
||||
const bool bNewVisible = IsVisible();
|
||||
DungeonLog_InfoSilent("Visibility of '%s' Changed: %d (before: %d)", *GetNameSafe(GetOwner()), bNewVisible, bOldVisible);
|
||||
if (bOldVisible == bNewVisible)
|
||||
return;
|
||||
|
||||
UpdateVisibility();
|
||||
DungeonLog_InfoSilent("Dispatch Room Visibility Event of '%s'", *GetNameSafe(GetOwner()));
|
||||
OnRoomVisibilityChanged.Broadcast(GetOwner(), bNewVisible);
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::ResetVisible(UObject* Owner)
|
||||
{
|
||||
SetVisible(Owner, false);
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::SetVisibilityMode(EVisibilityMode Mode)
|
||||
{
|
||||
VisibilityMode = Mode;
|
||||
UpdateVisibility();
|
||||
}
|
||||
|
||||
ARoomLevel* UStaticRoomVisibilityComponent::GetOwnerRoomLevel() const
|
||||
{
|
||||
ULevel* Level = GetOwner()->GetLevel();
|
||||
if (!IsValid(Level))
|
||||
return nullptr;
|
||||
|
||||
return Cast<ARoomLevel>(Level->GetLevelScriptActor());
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::UpdateVisibility()
|
||||
{
|
||||
CleanEnablers();
|
||||
|
||||
AActor* Actor = GetOwner();
|
||||
if (!IsValid(Actor))
|
||||
return;
|
||||
|
||||
// Can't use Actor->SetActorHiddenInGame() because it is replicated over network.
|
||||
// So instead we use the Root->SetVisibility() and propagate it to its children.
|
||||
// TODO: try to use something better than that (non-replicated but actor-wide).
|
||||
USceneComponent* Root = Actor->GetRootComponent();
|
||||
if (!IsValid(Root))
|
||||
return;
|
||||
|
||||
switch (VisibilityMode)
|
||||
{
|
||||
case EVisibilityMode::Default:
|
||||
Root->SetVisibility(IsVisible(), true);
|
||||
break;
|
||||
case EVisibilityMode::ForceHidden:
|
||||
Root->SetVisibility(false, true);
|
||||
break;
|
||||
case EVisibilityMode::ForceVisible:
|
||||
Root->SetVisibility(true, true);
|
||||
break;
|
||||
case EVisibilityMode::Custom:
|
||||
// The user handles the visibility
|
||||
break;
|
||||
default:
|
||||
checkNoEntry();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::RegisterVisibilityDelegate(ARoomLevel* RoomLevel, bool Register)
|
||||
{
|
||||
if (!IsValid(RoomLevel))
|
||||
return;
|
||||
|
||||
if (Register)
|
||||
RoomLevel->VisibilityChangedEvent.AddDynamic(this, &UStaticRoomVisibilityComponent::RoomVisibilityChanged);
|
||||
else
|
||||
RoomLevel->VisibilityChangedEvent.RemoveDynamic(this, &UStaticRoomVisibilityComponent::RoomVisibilityChanged);
|
||||
|
||||
SetVisible(RoomLevel, RoomLevel->IsVisible());
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::RoomVisibilityChanged(ARoomLevel* RoomLevel, bool IsVisible)
|
||||
{
|
||||
DungeonLog_InfoSilent("[%s] Room '%s' Visibility Changed: %d", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel), IsVisible);
|
||||
SetVisible(RoomLevel, IsVisible);
|
||||
}
|
||||
|
||||
void UStaticRoomVisibilityComponent::CleanEnablers()
|
||||
{
|
||||
TSet<TWeakObjectPtr<UObject>> ObjPtrToRemove;
|
||||
for (TWeakObjectPtr<UObject> ObjPtr : VisibilityEnablers)
|
||||
{
|
||||
if (!ObjPtr.IsValid())
|
||||
ObjPtrToRemove.Add(ObjPtr);
|
||||
}
|
||||
|
||||
for (TWeakObjectPtr<UObject> ObjPtr : ObjPtrToRemove)
|
||||
{
|
||||
VisibilityEnablers.Remove(ObjPtr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Door.h"
|
||||
#include "DoorType.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Components/DoorComponent.h"
|
||||
|
||||
ADoor::ADoor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
bReplicates = true;
|
||||
bAlwaysRelevant = true; // prevent the doors from despawning on clients when server's player is too far
|
||||
NetDormancy = ENetDormancy::DORM_DormantAll;
|
||||
|
||||
DefaultSceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComponent"));
|
||||
RootComponent = DefaultSceneComponent;
|
||||
|
||||
DoorComponent = CreateDefaultSubobject<UDoorComponent>(TEXT("DoorComponent"));
|
||||
}
|
||||
|
||||
void ADoor::PostInitializeComponents()
|
||||
{
|
||||
Super::PostInitializeComponents();
|
||||
|
||||
if (!IsValid(DoorComponent))
|
||||
return;
|
||||
|
||||
// Forward the component events to the actor events for retro-compatibility
|
||||
DoorComponent->OnDoorLocked.AddDynamic(this, &ADoor::DispatchDoorLock);
|
||||
DoorComponent->OnDoorOpened.AddDynamic(this, &ADoor::DispatchDoorOpen);
|
||||
}
|
||||
|
||||
bool ADoor::IsLocked() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return false;
|
||||
|
||||
return DoorComponent->IsLocked();
|
||||
}
|
||||
|
||||
bool ADoor::IsOpen() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return false;
|
||||
|
||||
return DoorComponent->IsOpen();
|
||||
}
|
||||
|
||||
void ADoor::Open(bool bOpen)
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return;
|
||||
|
||||
DoorComponent->Open(bOpen);
|
||||
}
|
||||
|
||||
void ADoor::Lock(bool bLock)
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return;
|
||||
|
||||
DoorComponent->Lock(bLock);
|
||||
}
|
||||
|
||||
bool ADoor::ShouldBeOpened() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return false;
|
||||
|
||||
return DoorComponent->ShouldBeOpen();
|
||||
}
|
||||
|
||||
bool ADoor::ShouldBeLocked() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return false;
|
||||
|
||||
return DoorComponent->ShouldBeLocked();
|
||||
}
|
||||
|
||||
const UDoorType* ADoor::GetDoorType() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return nullptr;
|
||||
|
||||
return DoorComponent->GetDoorType();
|
||||
}
|
||||
|
||||
URoom* ADoor::GetRoomA() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return nullptr;
|
||||
|
||||
return DoorComponent->GetRoomA();
|
||||
}
|
||||
|
||||
URoom* ADoor::GetRoomB() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return nullptr;
|
||||
|
||||
return DoorComponent->GetRoomB();
|
||||
}
|
||||
|
||||
void ADoor::DispatchDoorLock(UDoorComponent* Component, bool IsLocked)
|
||||
{
|
||||
if (IsLocked)
|
||||
{
|
||||
OnDoorLock();
|
||||
OnDoorLock_BP();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDoorUnlock();
|
||||
OnDoorUnlock_BP();
|
||||
}
|
||||
}
|
||||
|
||||
void ADoor::DispatchDoorOpen(UDoorComponent* Component, bool IsOpened)
|
||||
{
|
||||
if (IsOpened)
|
||||
{
|
||||
OnDoorOpen();
|
||||
OnDoorOpen_BP();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDoorClose();
|
||||
OnDoorClose_BP();
|
||||
}
|
||||
}
|
||||
|
||||
bool ADoor::GetAlwaysVisible() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return false;
|
||||
|
||||
return DoorComponent->IsAlwaysVisible();
|
||||
}
|
||||
|
||||
bool ADoor::GetAlwaysUnlocked() const
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return false;
|
||||
|
||||
return DoorComponent->IsAlwaysUnlocked();
|
||||
}
|
||||
|
||||
void ADoor::SetAlwaysVisible(bool bInAlwaysVisible)
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return;
|
||||
|
||||
DoorComponent->SetAlwaysVisible(bInAlwaysVisible);
|
||||
}
|
||||
|
||||
void ADoor::SetAlwaysUnlocked(bool bInAlwaysUnlocked)
|
||||
{
|
||||
if (!IsValid(DoorComponent))
|
||||
return;
|
||||
|
||||
DoorComponent->SetAlwaysUnlocked(bInAlwaysUnlocked);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "DoorType.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
|
||||
UDoorType::UDoorType()
|
||||
: UDataAsset()
|
||||
{
|
||||
Size = Dungeon::DefaultDoorSize();
|
||||
Offset = Dungeon::DoorOffset();
|
||||
#if WITH_EDITORONLY_DATA
|
||||
Color = FColor::Blue;
|
||||
Description = FText::FromString(TEXT("No Description"));
|
||||
#endif
|
||||
bCompatibleWithItself = true;
|
||||
}
|
||||
|
||||
FVector UDoorType::GetSize(const UDoorType* DoorType)
|
||||
{
|
||||
return IsValid(DoorType) ? DoorType->Size : Dungeon::DefaultDoorSize();
|
||||
}
|
||||
|
||||
float UDoorType::GetOffset(const UDoorType* DoorType)
|
||||
{
|
||||
return IsValid(DoorType) ? DoorType->Offset : Dungeon::DoorOffset();
|
||||
}
|
||||
|
||||
FColor UDoorType::GetColor(const UDoorType* DoorType)
|
||||
{
|
||||
return IsValid(DoorType) ? DoorType->Color : Dungeon::DefaultDoorColor();
|
||||
}
|
||||
|
||||
bool UDoorType::AreCompatible(const UDoorType* A, const UDoorType* B)
|
||||
{
|
||||
// If both are null, they are compatible
|
||||
if (!IsValid(A) && !IsValid(B))
|
||||
return true;
|
||||
|
||||
// If only one of them is null, they are not compatible
|
||||
if (!IsValid(A) || !IsValid(B))
|
||||
return false;
|
||||
|
||||
if (A == B)
|
||||
return A->bCompatibleWithItself;
|
||||
return A->Compatibility.Contains(B) || B->Compatibility.Contains(A);
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "DungeonBlueprintLibrary.h"
|
||||
#include "Interfaces/DoorInterface.h"
|
||||
#include "DoorType.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "Room.h"
|
||||
#include "RoomCustomData.h"
|
||||
#include "Engine/Engine.h" // GEngine
|
||||
|
||||
bool UDungeonBlueprintLibrary::IsDoorOfType(const TSubclassOf<AActor> DoorClass, const UDoorType* DoorType)
|
||||
{
|
||||
AActor* Door = DoorClass.GetDefaultObject();
|
||||
if (!IsValid(Door))
|
||||
return false;
|
||||
|
||||
UObject* Implementer = ActorUtils::GetInterfaceImplementer<UDoorInterface>(Door);
|
||||
if (!IsValid(Implementer))
|
||||
return DoorType == nullptr;
|
||||
|
||||
const UDoorType* ActualDoorType = IDoorInterface::Execute_GetDoorType(Implementer);
|
||||
return ActualDoorType == DoorType;
|
||||
}
|
||||
|
||||
bool UDungeonBlueprintLibrary::CompareDataTableRows(const FDataTableRowHandle& A, const FDataTableRowHandle& B)
|
||||
{
|
||||
return A == B;
|
||||
}
|
||||
|
||||
const ARoomLevel* UDungeonBlueprintLibrary::GetLevelScript(const AActor* Target)
|
||||
{
|
||||
if (!IsValid(Target))
|
||||
return nullptr;
|
||||
|
||||
if (const ARoomLevel* SelfLevel = Cast<ARoomLevel>(Target))
|
||||
{
|
||||
return SelfLevel;
|
||||
}
|
||||
|
||||
ULevel* Level = Target->GetLevel();
|
||||
if (!IsValid(Level))
|
||||
return nullptr;
|
||||
|
||||
ARoomLevel* RoomLevel = Cast<ARoomLevel>(Level->GetLevelScriptActor());
|
||||
if (!IsValid(RoomLevel))
|
||||
return nullptr;
|
||||
|
||||
return RoomLevel;
|
||||
}
|
||||
|
||||
URoom* UDungeonBlueprintLibrary::GetOwningRoom(const AActor* Target)
|
||||
{
|
||||
if (const ARoomLevel* SelfLevel = GetLevelScript(Target))
|
||||
{
|
||||
return SelfLevel->GetRoom();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UDungeonBlueprintLibrary::GetOwningRoomCustomData(const AActor* Target, TSubclassOf<URoomCustomData> CustomDataClass, URoomCustomData*& CustomData)
|
||||
{
|
||||
CustomData = nullptr;
|
||||
URoom* OwningRoom = GetOwningRoom(Target);
|
||||
if (!IsValid(OwningRoom))
|
||||
return false;
|
||||
|
||||
OwningRoom->GetCustomData(CustomDataClass, CustomData);
|
||||
return IsValid(CustomData);
|
||||
}
|
||||
|
||||
const URoomData* UDungeonBlueprintLibrary::GetLevelRoomData(const AActor* Target)
|
||||
{
|
||||
if (const ARoomLevel* SelfLevel = GetLevelScript(Target))
|
||||
{
|
||||
return SelfLevel->GetRoomData();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FDoorDef UDungeonBlueprintLibrary::DoorDef_GetOpposite(const FDoorDef& DoorDef)
|
||||
{
|
||||
return (DoorDef) ? DoorDef.GetOpposite() : DoorDef;
|
||||
}
|
||||
|
||||
// ===== Plugin Settings Accessors =====
|
||||
|
||||
FIntVector UDungeonBlueprintLibrary::IntVector_Next(const FIntVector& Vector, const EDoorDirection& Direction)
|
||||
{
|
||||
return Vector + ToIntVector(Direction);
|
||||
}
|
||||
|
||||
FIntVector UDungeonBlueprintLibrary::IntVector_Rotate(const FIntVector& Vector, const EDoorDirection& Direction)
|
||||
{
|
||||
return Rotate(Vector, Direction);
|
||||
}
|
||||
|
||||
FIntVector UDungeonBlueprintLibrary::Dungeon_TransformPosition(const FIntVector& LocalPos, const FIntVector& Translation, const EDoorDirection& Rotation)
|
||||
{
|
||||
return Transform(LocalPos, Translation, Rotation);
|
||||
}
|
||||
|
||||
FIntVector UDungeonBlueprintLibrary::Dungeon_InverseTransformPosition(const FIntVector& DungeonPos, const FIntVector& Translation, const EDoorDirection& Rotation)
|
||||
{
|
||||
return InverseTransform(DungeonPos, Translation, Rotation);
|
||||
}
|
||||
|
||||
FDoorDef UDungeonBlueprintLibrary::Dungeon_TransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation)
|
||||
{
|
||||
return FDoorDef::Transform(DoorDef, Translation, Rotation);
|
||||
}
|
||||
|
||||
FDoorDef UDungeonBlueprintLibrary::Dungeon_InverseTransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation)
|
||||
{
|
||||
return FDoorDef::InverseTransform(DoorDef, Translation, Rotation);
|
||||
}
|
||||
|
||||
FIntVector UDungeonBlueprintLibrary::IntVector_Add(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
return A + B;
|
||||
}
|
||||
|
||||
FIntVector UDungeonBlueprintLibrary::IntVector_Subtract(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
return A - B;
|
||||
}
|
||||
|
||||
bool UDungeonBlueprintLibrary::IntVector_Equal(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
return A == B;
|
||||
}
|
||||
|
||||
bool UDungeonBlueprintLibrary::IntVector_NotEqual(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
return A != B;
|
||||
}
|
||||
|
||||
FVector UDungeonBlueprintLibrary::Settings_RoomUnit()
|
||||
{
|
||||
return Dungeon::RoomUnit();
|
||||
}
|
||||
|
||||
FVector UDungeonBlueprintLibrary::Settings_DefaultDoorSize()
|
||||
{
|
||||
return Dungeon::DefaultDoorSize();
|
||||
}
|
||||
|
||||
float UDungeonBlueprintLibrary::Settings_DoorOffset()
|
||||
{
|
||||
return Dungeon::DoorOffset();
|
||||
}
|
||||
|
||||
bool UDungeonBlueprintLibrary::Settings_OcclusionCulling()
|
||||
{
|
||||
return Dungeon::OcclusionCulling();
|
||||
}
|
||||
|
||||
void UDungeonBlueprintLibrary::Settings_SetOcclusionCulling(bool Enable)
|
||||
{
|
||||
Dungeon::EnableOcclusionCulling(Enable);
|
||||
}
|
||||
|
||||
int32 UDungeonBlueprintLibrary::Settings_OcclusionDistance()
|
||||
{
|
||||
return Dungeon::OcclusionDistance();
|
||||
}
|
||||
|
||||
void UDungeonBlueprintLibrary::Settings_SetOcclusionDistance(int32 Distance)
|
||||
{
|
||||
Dungeon::SetOcclusionDistance(Distance);
|
||||
}
|
||||
|
||||
bool UDungeonBlueprintLibrary::Settings_OccludeDynamicActors()
|
||||
{
|
||||
return Dungeon::OccludeDynamicActors();
|
||||
}
|
||||
|
||||
// ===== Gameplay Utility Functions =====
|
||||
|
||||
void UDungeonBlueprintLibrary::Spectate(APlayerController* Controller, bool DestroyPawn)
|
||||
{
|
||||
if (!Controller)
|
||||
return;
|
||||
|
||||
if (!Controller->HasAuthority())
|
||||
return;
|
||||
|
||||
APawn* PreviousPawn = Controller->GetPawn();
|
||||
|
||||
Controller->PlayerState->SetIsSpectator(true);
|
||||
Controller->ChangeState(NAME_Spectating);
|
||||
Controller->bPlayerIsWaiting = true;
|
||||
Controller->ClientGotoState(NAME_Spectating);
|
||||
|
||||
if (DestroyPawn && IsValid(PreviousPawn))
|
||||
{
|
||||
PreviousPawn->Destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "DungeonGenerator.h"
|
||||
#include "RoomData.h"
|
||||
#include "Room.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "DungeonGraph.h"
|
||||
|
||||
// Sets default values
|
||||
ADungeonGenerator::ADungeonGenerator()
|
||||
: Super()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
GenerationType = EGenerationType::DFS;
|
||||
}
|
||||
|
||||
bool ADungeonGenerator::CreateDungeon_Implementation()
|
||||
{
|
||||
// Only server generate the dungeon
|
||||
// DungeonGraph will be replicated to all clients
|
||||
if (!HasAuthority())
|
||||
return false;
|
||||
|
||||
switch (CurrentState)
|
||||
{
|
||||
case EState::Idle:
|
||||
DungeonLog_Debug("--- Idle State");
|
||||
// Maybe move from plugin settings to generator's variable?
|
||||
CurrentTriesLeft = Dungeon::MaxGenerationTryBeforeGivingUp();
|
||||
CurrentState = EState::Initializing;
|
||||
// No break to execute immediatly the Initializing state
|
||||
case EState::Initializing: {
|
||||
DungeonLog_Debug("--- Initializing State");
|
||||
--CurrentTriesLeft;
|
||||
|
||||
// Reset generation data
|
||||
StartNewDungeon();
|
||||
|
||||
// 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;
|
||||
default:
|
||||
DungeonLog_Error("GenerationType value is not supported.");
|
||||
return false;
|
||||
}
|
||||
|
||||
URoomData* def = ChooseFirstRoomData();
|
||||
if (!IsValid(def))
|
||||
{
|
||||
DungeonLog_Error("ChooseFirstRoomData returned null.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create the first room
|
||||
URoom* root = CreateRoomInstance(def);
|
||||
AddRoomToDungeon(root, /*DoorsToConnect = */ {}, /*bFailIfNotConnected = */ false);
|
||||
|
||||
// Build the list of rooms
|
||||
PendingRooms.SetMode(listMode);
|
||||
PendingRooms.Push(root);
|
||||
|
||||
CurrentState = EState::AddingRooms;
|
||||
}
|
||||
// No break to execute immediatly the AddingRooms state
|
||||
}
|
||||
case EState::AddingRooms: {
|
||||
DungeonLog_Debug("--- AddingRooms State");
|
||||
|
||||
TArray<URoom*> NewRooms;
|
||||
int BatchCount = RoomBatchSize;
|
||||
while (!PendingRooms.IsEmpty() && BatchCount > 0)
|
||||
{
|
||||
--BatchCount;
|
||||
URoom* CurrentRoom = PendingRooms.Pop();
|
||||
check(IsValid(CurrentRoom)); // CurrentRoom should always be valid
|
||||
|
||||
if (!AddNewRooms(*CurrentRoom, NewRooms))
|
||||
{
|
||||
// Stop generation here
|
||||
DungeonLog_Debug("--- Stopping generation as AddNewRooms returned false.");
|
||||
PendingRooms.Empty();
|
||||
break;
|
||||
}
|
||||
|
||||
DungeonLog_Debug("--- %d rooms added to the dungeon.", NewRooms.Num());
|
||||
for (URoom* room : NewRooms)
|
||||
{
|
||||
PendingRooms.Push(room);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PendingRooms.IsEmpty())
|
||||
{
|
||||
DungeonLog_Debug("--- Still pending rooms, yielding.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DungeonLog_Debug("--- No more pending rooms, finalizing.");
|
||||
CurrentState = EState::Finalizing;
|
||||
}
|
||||
// Proceed to next tick
|
||||
YieldGeneration();
|
||||
break;
|
||||
}
|
||||
case EState::Finalizing:
|
||||
DungeonLog_Debug("--- Finalizing State");
|
||||
// Initialize the dungeon by eg. altering the room instances
|
||||
FinalizeDungeon();
|
||||
CurrentState = EState::Idle;
|
||||
if (!IsValidDungeon())
|
||||
{
|
||||
DungeonLog_Debug("--- Dungeon is not valid, tries left: %d", CurrentTriesLeft);
|
||||
if (CurrentTriesLeft <= 0)
|
||||
{
|
||||
DungeonLog_Error("Generated dungeon is not valid after %d tries. Make sure your ChooseNextRoomData and IsValidDungeon functions are correct.", Dungeon::MaxGenerationTryBeforeGivingUp());
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentState = EState::Initializing;
|
||||
YieldGeneration();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DungeonLog_Error("CurrentState value is not supported.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ADungeonGenerator::AddNewRooms(URoom& ParentRoom, TArray<URoom*>& AddedRooms)
|
||||
{
|
||||
check(HasAuthority());
|
||||
|
||||
int nbDoor = ParentRoom.GetRoomData()->GetNbDoor();
|
||||
if (nbDoor <= 0)
|
||||
DungeonLog_Error("The room data '%s' has no door! Nothing could be generated with it!", *GetNameSafe(ParentRoom.GetRoomData()));
|
||||
|
||||
// Cache world before loops
|
||||
const UWorld* World = GetWorld();
|
||||
const FBoxMinAndMax DungeonBounds = DungeonLimits.GetBox();
|
||||
|
||||
AddedRooms.Reset();
|
||||
bool shouldContinue = false;
|
||||
for (int i = 0; shouldContinue = ContinueToAddRoom(), i < nbDoor && shouldContinue; ++i)
|
||||
{
|
||||
if (ParentRoom.IsConnected(i))
|
||||
continue;
|
||||
|
||||
// Get the door definition in its world position and direction
|
||||
FDoorDef doorDef = ParentRoom.GetDoorDef(i);
|
||||
|
||||
// Get the door definition for the next room
|
||||
const FDoorDef newRoomDoor = doorDef.GetOpposite();
|
||||
if (!DungeonBounds.IsInside(newRoomDoor.Position))
|
||||
continue;
|
||||
|
||||
// Maybe move from plugin settings to generator's variable?
|
||||
int nbTries = Dungeon::MaxRoomPlacementTryBeforeGivingUp();
|
||||
URoom* newRoom = nullptr;
|
||||
int doorIndex = -1;
|
||||
// Try to place a new room
|
||||
do
|
||||
{
|
||||
nbTries--;
|
||||
bDiscardRoom = false;
|
||||
URoomData* roomDef = ChooseNextRoomData(ParentRoom.GetRoomData(), &ParentRoom, doorDef, doorIndex);
|
||||
if (!IsValid(roomDef))
|
||||
{
|
||||
bDiscardRoom |= bAutoDiscardRoomIfNull;
|
||||
if (bDiscardRoom)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
DungeonLog_Error("ChooseNextRoomData returned null.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (doorIndex >= roomDef->Doors.Num())
|
||||
{
|
||||
DungeonLog_Error("ChooseNextRoomData returned door index '%d' which is out of range in the RoomData '%s' door list (max: %d).", doorIndex, *roomDef->GetName(), roomDef->Doors.Num() - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all compatible door indices from the chosen room data
|
||||
TArray<int> compatibleDoors;
|
||||
roomDef->GetCompatibleDoors(doorDef, compatibleDoors);
|
||||
if (compatibleDoors.Num() <= 0)
|
||||
{
|
||||
DungeonLog_Error("ChooseNextRoomData returned room data '%s' with no compatible door (door type: '%s').", *roomDef->GetName(), *doorDef.GetTypeName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get only doors if the new room could fit in the dungeon bounds
|
||||
for (int n = compatibleDoors.Num() - 1; n >= 0; --n)
|
||||
{
|
||||
if (!roomDef->IsRoomInBounds(DungeonBounds, compatibleDoors[n], newRoomDoor))
|
||||
compatibleDoors.RemoveAt(n);
|
||||
}
|
||||
|
||||
if (compatibleDoors.Num() <= 0)
|
||||
{
|
||||
DungeonLog_Warning("ChooseNextRoomData returned room data '%s' that could not fit in dungeon bounds.", *roomDef->GetName());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (roomDef->RandomDoor || (doorIndex < 0))
|
||||
doorIndex = compatibleDoors[GetRandomStream().RandRange(0, compatibleDoors.Num() - 1)];
|
||||
else if (!compatibleDoors.Contains(doorIndex))
|
||||
{
|
||||
DungeonLog_Error("ChooseNextRoomData returned door index '%d' (RoomData '%s') which its type '%s' is not compatible with '%s'.", doorIndex, *roomDef->GetName(), *roomDef->Doors[doorIndex].GetTypeName(), *doorDef.GetTypeName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new room instance from roomdef
|
||||
newRoom = CreateRoomInstance(roomDef);
|
||||
|
||||
// Place the room at targeted door position if possible
|
||||
if (!TryPlaceRoom(newRoom, doorIndex, newRoomDoor, World))
|
||||
{
|
||||
// The object will be automatically deleted by the GC
|
||||
newRoom = nullptr;
|
||||
}
|
||||
} while (nbTries > 0 && newRoom == nullptr);
|
||||
|
||||
// If we explicitely want to not place a room, then goes to next door
|
||||
if (bDiscardRoom)
|
||||
continue;
|
||||
|
||||
// Plugin-wide setting is deprecated, will be removed in v4.0
|
||||
const bool bConnectAllDoors = bCanLoop && Dungeon::CanLoop();
|
||||
if (AddRoomToDungeon(newRoom, bConnectAllDoors ? TArray<int> {} : TArray<int> {doorIndex}))
|
||||
{
|
||||
AddedRooms.Add(newRoom);
|
||||
}
|
||||
else // No room can be placed and all placement tries exhausted
|
||||
{
|
||||
// @TODO: Find a way to move this call in AddRoomToDungeon
|
||||
OnFailedToAddRoom(ParentRoom.GetRoomData(), doorDef);
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe move from plugin settings to generator's variable?
|
||||
const bool bRoomLimitReached = Graph->Count() > Dungeon::RoomLimit();
|
||||
if (bRoomLimitReached)
|
||||
{
|
||||
DungeonLog_Warning("Dungeon has reached the room limit of %d! Check your 'Continue To Add Room' to make sure your dungeon is not in an infinite loop, or increase the room limit in the plugin settings if this is intentional.", Dungeon::RoomLimit());
|
||||
}
|
||||
|
||||
return shouldContinue && !bRoomLimitReached;
|
||||
}
|
||||
|
||||
// ===== Default Native Events Implementations =====
|
||||
|
||||
URoomData* ADungeonGenerator::ChooseFirstRoomData_Implementation()
|
||||
{
|
||||
DungeonLog_Error("Error: ChooseFirstRoomData not implemented");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
URoomData* ADungeonGenerator::ChooseNextRoomData_Implementation(const URoomData* CurrentRoom, const TScriptInterface<IReadOnlyRoom>& CurrentRoomInstance, const FDoorDef& DoorData, int& DoorIndex)
|
||||
{
|
||||
DungeonLog_Error("Error: ChooseNextRoomData not implemented");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ADungeonGenerator::IsValidDungeon_Implementation()
|
||||
{
|
||||
DungeonLog_Error("Error: IsValidDungeon not implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ADungeonGenerator::ContinueToAddRoom_Implementation()
|
||||
{
|
||||
DungeonLog_Error("Error: ContinueToAddRoom not implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===== Utility Functions (Deprectated!!!) =====
|
||||
|
||||
bool ADungeonGenerator::HasAlreadyRoomData(URoomData* RoomData)
|
||||
{
|
||||
return Graph->HasAlreadyRoomData(RoomData);
|
||||
}
|
||||
|
||||
bool ADungeonGenerator::HasAlreadyOneRoomDataFrom(TArray<URoomData*> RoomDataList)
|
||||
{
|
||||
return Graph->HasAlreadyOneRoomDataFrom(RoomDataList);
|
||||
}
|
||||
|
||||
int ADungeonGenerator::CountRoomData(URoomData* RoomData)
|
||||
{
|
||||
return Graph->CountRoomData(RoomData);
|
||||
}
|
||||
|
||||
int ADungeonGenerator::CountTotalRoomData(TArray<URoomData*> RoomDataList)
|
||||
{
|
||||
return Graph->CountTotalRoomData(RoomDataList);
|
||||
}
|
||||
|
||||
bool ADungeonGenerator::HasAlreadyRoomType(TSubclassOf<URoomData> RoomType)
|
||||
{
|
||||
return Graph->HasAlreadyRoomType(RoomType);
|
||||
}
|
||||
|
||||
bool ADungeonGenerator::HasAlreadyOneRoomTypeFrom(TArray<TSubclassOf<URoomData>> RoomTypeList)
|
||||
{
|
||||
return Graph->HasAlreadyOneRoomTypeFrom(RoomTypeList);
|
||||
}
|
||||
|
||||
int ADungeonGenerator::CountRoomType(TSubclassOf<URoomData> RoomType)
|
||||
{
|
||||
return Graph->CountRoomType(RoomType);
|
||||
}
|
||||
|
||||
int ADungeonGenerator::CountTotalRoomType(TArray<TSubclassOf<URoomData>> RoomTypeList)
|
||||
{
|
||||
return Graph->CountTotalRoomType(RoomTypeList);
|
||||
}
|
||||
|
||||
int ADungeonGenerator::GetNbRoom()
|
||||
{
|
||||
return Graph->Count();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,845 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "DungeonGraph.h"
|
||||
#include "Utils/ReplicationUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Containers/Queue.h"
|
||||
#include "DungeonGenerator.h"
|
||||
#include "Room.h"
|
||||
#include "RoomData.h"
|
||||
#include "RoomCustomData.h"
|
||||
#include "RoomConnection.h"
|
||||
#include "Door.h"
|
||||
#include "Engine/Level.h"
|
||||
#include "Engine/LevelStreamingDynamic.h"
|
||||
#include "Utils/DungeonSaveUtils.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "DungeonSettings.h"
|
||||
|
||||
UDungeonGraph::UDungeonGraph()
|
||||
: Super()
|
||||
, Octree(FVector::ZeroVector, HALF_WORLD_MAX)
|
||||
{
|
||||
}
|
||||
|
||||
void UDungeonGraph::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
FDoRepLifetimeParams Params;
|
||||
Params.bIsPushBased = true;
|
||||
DOREPLIFETIME_WITH_PARAMS(UDungeonGraph, ReplicatedRooms, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS(UDungeonGraph, RoomConnections, Params);
|
||||
}
|
||||
|
||||
bool UDungeonGraph::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
|
||||
{
|
||||
bool bWroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
|
||||
for (URoom* Room : ReplicatedRooms)
|
||||
{
|
||||
check(Room);
|
||||
bWroteSomething |= Room->ReplicateSubobject(Channel, Bunch, RepFlags);
|
||||
}
|
||||
for (URoomConnection* Conn : RoomConnections)
|
||||
{
|
||||
check(Conn);
|
||||
bWroteSomething |= Conn->ReplicateSubobject(Channel, Bunch, RepFlags);
|
||||
}
|
||||
return bWroteSomething;
|
||||
}
|
||||
|
||||
void UDungeonGraph::RegisterReplicableSubobjects(bool bRegister)
|
||||
{
|
||||
for (URoom* Room : ReplicatedRooms)
|
||||
{
|
||||
Room->RegisterAsReplicable(bRegister);
|
||||
}
|
||||
|
||||
for (URoomConnection* Conn : RoomConnections)
|
||||
{
|
||||
Conn->RegisterAsReplicable(bRegister);
|
||||
}
|
||||
}
|
||||
|
||||
URoomConnection* UDungeonGraph::GetConnectionByIndex(int32 Index) const
|
||||
{
|
||||
if (!RoomConnections.IsValidIndex(Index))
|
||||
{
|
||||
DungeonLog_WarningSilent("Invalid index %d for RoomConnections.", Index);
|
||||
return nullptr;
|
||||
}
|
||||
return RoomConnections[Index];
|
||||
}
|
||||
|
||||
bool UDungeonGraph::SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading)
|
||||
{
|
||||
SavedData = MakeUnique<FSaveData>();
|
||||
|
||||
if (!bIsLoading)
|
||||
{
|
||||
SavedData->Rooms = TArray<URoom*>(Rooms);
|
||||
SavedData->Connections = TArray<URoomConnection*>(RoomConnections);
|
||||
}
|
||||
|
||||
SerializeUObjectArray(Record, AR_FIELD_NAME("Rooms"), SavedData->Rooms, bIsLoading, this);
|
||||
SerializeUObjectArray(Record, AR_FIELD_NAME("Connections"), SavedData->Connections, bIsLoading, this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UDungeonGraph::PostLoadDungeon_Implementation()
|
||||
{
|
||||
// Load has ended, we can safely reset the saved data.
|
||||
SavedData.Reset();
|
||||
}
|
||||
|
||||
void UDungeonGraph::AddRoom(URoom* Room)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
|
||||
Rooms.Add(Room);
|
||||
UpdateBounds(Room);
|
||||
UpdateOctree(Room);
|
||||
}
|
||||
|
||||
void UDungeonGraph::InitRooms()
|
||||
{
|
||||
// We split the for loops to ensure custom data are created for all rooms before initializing them
|
||||
|
||||
// First create empty connections for remaining unconnected doors
|
||||
TArray<int32> EmptyConnections;
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
Room->GetAllEmptyConnections(EmptyConnections);
|
||||
for (int32 DoorIndex : EmptyConnections)
|
||||
{
|
||||
Connect(Room, DoorIndex, nullptr, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally we can initialize them all
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
// No need to check validity here
|
||||
const URoomData* Data = Room->GetRoomData();
|
||||
Data->InitializeRoom(Room, this);
|
||||
}
|
||||
}
|
||||
|
||||
bool UDungeonGraph::CanRoomFit(const URoom* Room) const
|
||||
{
|
||||
bool bCanFit = true;
|
||||
for (int32 i = 0; i < Room->GetSubBoundsCount() && bCanFit; ++i)
|
||||
{
|
||||
FindElementsWithBoundsTest(Octree, Room->GetSubBounds(i), [&bCanFit, Room](const FDungeonOctreeElement& Element) {
|
||||
bCanFit = false;
|
||||
});
|
||||
}
|
||||
return bCanFit;
|
||||
}
|
||||
|
||||
bool UDungeonGraph::TryConnectDoor(URoom* Room, int32 DoorIndex)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
|
||||
// Check if already connected.
|
||||
if (Room->IsConnected(DoorIndex))
|
||||
return true;
|
||||
|
||||
// Get the room in front of the door if any.
|
||||
EDoorDirection DoorDir = Room->GetDoorWorldOrientation(DoorIndex);
|
||||
FIntVector AdjacentCell = Room->GetDoorWorldPosition(DoorIndex) + ToIntVector(DoorDir);
|
||||
URoom* OtherRoom = GetRoomAt(AdjacentCell);
|
||||
if (!IsValid(OtherRoom))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the door index of the other room if any.
|
||||
int OtherDoorIndex = OtherRoom->GetDoorIndexAt(AdjacentCell, ~DoorDir);
|
||||
if (OtherDoorIndex < 0) // -1 if no door
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check door compatibility.
|
||||
const FDoorDef& ThisDoor = Room->GetRoomData()->Doors[DoorIndex];
|
||||
const FDoorDef& OtherDoor = OtherRoom->GetRoomData()->Doors[OtherDoorIndex];
|
||||
if (!FDoorDef::AreCompatible(ThisDoor, OtherDoor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finally connect the doors.
|
||||
Connect(Room, DoorIndex, OtherRoom, OtherDoorIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDungeonGraph::TryConnectToExistingDoors(URoom* Room)
|
||||
{
|
||||
bool HasConnection = false;
|
||||
for (int i = 0; i < Room->GetRoomData()->GetNbDoor(); ++i)
|
||||
{
|
||||
HasConnection |= TryConnectDoor(Room, i);
|
||||
}
|
||||
return HasConnection;
|
||||
}
|
||||
|
||||
TArray<URoom*> UDungeonGraph::GetAllRoomsOverlapping(const FBox& Box) const
|
||||
{
|
||||
TArray<URoom*> RoomsInBox;
|
||||
FindElementsWithBoundsTest(Octree, Box, [&RoomsInBox](const FDungeonOctreeElement& Element) {
|
||||
URoom* Room = Element.Room;
|
||||
RoomsInBox.AddUnique(Room);
|
||||
});
|
||||
return RoomsInBox;
|
||||
}
|
||||
|
||||
void UDungeonGraph::RetrieveRoomsFromLoadedData()
|
||||
{
|
||||
if (Rooms.Num() > 0)
|
||||
{
|
||||
DungeonLog_Error("Trying to retrieve loaded rooms while previous ones are not unloaded!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SavedData.IsValid())
|
||||
return;
|
||||
|
||||
Rooms = TArray<URoom*>(SavedData->Rooms);
|
||||
RoomConnections = TArray<URoomConnection*>(SavedData->Connections);
|
||||
|
||||
IDungeonCustomSerialization::DispatchFixupReferences(this, this);
|
||||
|
||||
RebuildBounds();
|
||||
RebuildOctree();
|
||||
}
|
||||
|
||||
void UDungeonGraph::Connect(URoom* RoomA, int32 DoorA, URoom* RoomB, int32 DoorB)
|
||||
{
|
||||
URoomConnection* NewConnection = URoomConnection::CreateConnection(RoomA, DoorA, RoomB, DoorB, this, RoomConnections.Num());
|
||||
RoomConnections.Add(NewConnection);
|
||||
DungeonLog_Debug("Connected %s (%d) to %s (%d)", *GetNameSafe(RoomA), DoorA, *GetNameSafe(RoomB), DoorB);
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(UDungeonGraph, RoomConnections, this);
|
||||
}
|
||||
|
||||
void UDungeonGraph::GetAllRoomsFromData(const URoomData* Data, TArray<URoom*>& OutRooms)
|
||||
{
|
||||
GetRoomsByPredicate(OutRooms, [Data](const URoom* Room) { return Room->GetRoomData() == Data; });
|
||||
}
|
||||
|
||||
void UDungeonGraph::GetAllRoomsFromDataList(const TArray<URoomData*>& Data, TArray<URoom*>& OutRooms)
|
||||
{
|
||||
GetRoomsByPredicate(OutRooms, [&Data](const URoom* Room) { return Data.Contains(Room->GetRoomData()); });
|
||||
}
|
||||
|
||||
const URoom* UDungeonGraph::GetFirstRoomFromData(const URoomData* Data)
|
||||
{
|
||||
return FindFirstRoomByPredicate([Data](const URoom* Room) { return Room->GetRoomData() == Data; });
|
||||
}
|
||||
|
||||
void UDungeonGraph::GetAllRoomsWithCustomData(const TSubclassOf<URoomCustomData>& CustomData, TArray<URoom*>& OutRooms)
|
||||
{
|
||||
GetRoomsByPredicate(OutRooms, [&CustomData](const URoom* Room) { return Room->HasCustomData(CustomData); });
|
||||
}
|
||||
|
||||
void UDungeonGraph::GetAllRoomsWithAllCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& OutRooms)
|
||||
{
|
||||
GetRoomsByPredicate(OutRooms, [&CustomData](const URoom* Room) {
|
||||
for (auto Datum : CustomData)
|
||||
{
|
||||
if (!Room->HasCustomData(Datum))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void UDungeonGraph::GetAllRoomsWithAnyCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& OutRooms)
|
||||
{
|
||||
GetRoomsByPredicate(OutRooms, [&CustomData](const URoom* Room) {
|
||||
for (auto Datum : CustomData)
|
||||
{
|
||||
if (Room->HasCustomData(Datum))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
URoom* UDungeonGraph::GetRandomRoom(const TArray<URoom*>& RoomList) const
|
||||
{
|
||||
if (!Generator.IsValid())
|
||||
{
|
||||
DungeonLog_Error("DungeonGraph has no Generator set.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (RoomList.Num() <= 0)
|
||||
return nullptr;
|
||||
|
||||
int32 rand = Generator->GetRandomStream().FRandRange(0, RoomList.Num() - 1);
|
||||
return RoomList[rand];
|
||||
}
|
||||
|
||||
bool UDungeonGraph::HasAlreadyRoomData(const URoomData* RoomData) const
|
||||
{
|
||||
return CountRoomData(RoomData) > 0;
|
||||
}
|
||||
|
||||
bool UDungeonGraph::HasAlreadyOneRoomDataFrom(const TArray<URoomData*>& RoomDataList) const
|
||||
{
|
||||
return CountTotalRoomData(RoomDataList) > 0;
|
||||
}
|
||||
|
||||
int UDungeonGraph::CountRoomData(const URoomData* RoomData) const
|
||||
{
|
||||
return CountRoomByPredicate([RoomData](const URoom* Room) { return Room->GetRoomData() == RoomData; });
|
||||
}
|
||||
|
||||
int UDungeonGraph::CountTotalRoomData(const TArray<URoomData*>& RoomDataList) const
|
||||
{
|
||||
return CountRoomByPredicate([&RoomDataList](const URoom* Room) { return RoomDataList.Contains(Room->GetRoomData()); });
|
||||
}
|
||||
|
||||
bool UDungeonGraph::HasAlreadyRoomType(const TSubclassOf<URoomData>& RoomType) const
|
||||
{
|
||||
return CountRoomType(RoomType) > 0;
|
||||
}
|
||||
|
||||
bool UDungeonGraph::HasAlreadyOneRoomTypeFrom(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const
|
||||
{
|
||||
return CountTotalRoomType(RoomTypeList) > 0;
|
||||
}
|
||||
|
||||
int UDungeonGraph::CountRoomType(const TSubclassOf<URoomData>& RoomType) const
|
||||
{
|
||||
return CountRoomByPredicate([&RoomType](const URoom* Room) { return Room->GetRoomData()->GetClass()->IsChildOf(RoomType); });
|
||||
}
|
||||
|
||||
int UDungeonGraph::CountTotalRoomType(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const
|
||||
{
|
||||
return CountRoomByPredicate([&RoomTypeList](const URoom* Room) {
|
||||
return RoomTypeList.ContainsByPredicate([Room](const TSubclassOf<URoomData> RoomType) {
|
||||
return Room->GetRoomData()->GetClass()->IsChildOf(RoomType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bool UDungeonGraph::HasValidPath(const URoom* From, const URoom* To, bool IgnoreLockedRooms) const
|
||||
{
|
||||
return FindPath(From, To, nullptr, IgnoreLockedRooms);
|
||||
}
|
||||
|
||||
int32 UDungeonGraph::NumberOfRoomBetween(const URoom* A, const URoom* B, bool IgnoreLockedRooms) const
|
||||
{
|
||||
TArray<const URoom*> Path;
|
||||
FindPath(A, B, &Path, IgnoreLockedRooms);
|
||||
return Path.Num();
|
||||
}
|
||||
|
||||
int32 UDungeonGraph::NumberOfRoomBetween_ReadOnly(TScriptInterface<IReadOnlyRoom> A, TScriptInterface<IReadOnlyRoom> B) const
|
||||
{
|
||||
// @TODO: That's not really safe, it should be better to make a FindPath using ReadOnlyRooms too.
|
||||
const URoom* RoomA = Cast<URoom>(A.GetObject());
|
||||
const URoom* RoomB = Cast<URoom>(B.GetObject());
|
||||
return NumberOfRoomBetween(RoomA, RoomB);
|
||||
}
|
||||
|
||||
bool UDungeonGraph::GetPathBetween(const URoom* A, const URoom* B, TArray<URoom*>& ResultPath, bool IgnoreLockedRooms) const
|
||||
{
|
||||
// @HACK: is it another alternative?
|
||||
TArray<const URoom*>& Temp = reinterpret_cast<TArray<const URoom*>&>(ResultPath);
|
||||
FindPath(A, B, &Temp, IgnoreLockedRooms);
|
||||
return ResultPath.Num() > 0;
|
||||
}
|
||||
|
||||
URoom* UDungeonGraph::GetRoomAt(FIntVector RoomCell) const
|
||||
{
|
||||
const FVector RoomUnit = UDungeonSettings::GetRoomUnit(Generator->GetSettings());
|
||||
FVector Location = Dungeon::ToWorldLocation(RoomCell, RoomUnit);
|
||||
FBox LocationBox(Location, Location + FVector::OneVector);
|
||||
|
||||
URoom* FoundRoom = nullptr;
|
||||
FindElementsWithBoundsTest(Octree, LocationBox, [&FoundRoom](const FDungeonOctreeElement& Element) {
|
||||
FoundRoom = Element.Room;
|
||||
});
|
||||
|
||||
return FoundRoom;
|
||||
}
|
||||
|
||||
FVector UDungeonGraph::GetDungeonBoundsCenter() const
|
||||
{
|
||||
FTransform Transform = Generator.IsValid() ? Generator->GetDungeonTransform() : FTransform::Identity;
|
||||
return GetDungeonBounds(Transform).Center;
|
||||
}
|
||||
|
||||
FVector UDungeonGraph::GetDungeonBoundsExtent() const
|
||||
{
|
||||
FTransform Transform = Generator.IsValid() ? Generator->GetDungeonTransform() : FTransform::Identity;
|
||||
return GetDungeonBounds(Transform).Extent;
|
||||
}
|
||||
|
||||
static bool RoomCandidatePredicate(const FRoomCandidate& A, const FRoomCandidate& B)
|
||||
{
|
||||
return A.Score > B.Score;
|
||||
}
|
||||
|
||||
bool UDungeonGraph::FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms, const FScoreCallback& CustomScore) const
|
||||
{
|
||||
SortedRooms.Empty();
|
||||
|
||||
FDoorDef TargetDoor = FromDoor.GetOpposite();
|
||||
|
||||
for (URoomData* RoomData : RoomList)
|
||||
{
|
||||
if (!IsValid(RoomData))
|
||||
continue;
|
||||
|
||||
FVoxelBounds DataBounds = RoomData->GetVoxelBounds();
|
||||
|
||||
// Try each possible door
|
||||
for (int i = 0; i < RoomData->GetNbDoor(); ++i)
|
||||
{
|
||||
const FDoorDef& Door = RoomData->Doors[i];
|
||||
|
||||
// Filter out the door candidate if not compatible with the door
|
||||
// we want to connect from.
|
||||
if (!FDoorDef::AreCompatible(TargetDoor, Door))
|
||||
continue;
|
||||
|
||||
// Compute new room placement
|
||||
const EDoorDirection RoomDirection = TargetDoor.Direction - Door.Direction;
|
||||
const FIntVector RoomLocation = TargetDoor.Position - Rotate(Door.Position, RoomDirection);
|
||||
|
||||
// Filter out the rooms that does not pass the constraints
|
||||
if (!URoomData::DoesPassAllConstraints(this, RoomData, RoomLocation, RoomDirection))
|
||||
continue;
|
||||
|
||||
FRoomCandidate Candidate;
|
||||
Candidate.Data = RoomData;
|
||||
Candidate.DoorIndex = i;
|
||||
|
||||
// Check if the new bounds placed at the target door can fit
|
||||
const FVoxelBounds NewBounds = Rotate(DataBounds, RoomDirection) + RoomLocation;
|
||||
if (!NewBounds.GetCompatibilityScore(Bounds, Candidate.Score, CustomScore))
|
||||
continue;
|
||||
|
||||
SortedRooms.HeapPush(Candidate, ::RoomCandidatePredicate);
|
||||
}
|
||||
}
|
||||
|
||||
return SortedRooms.Num() > 0;
|
||||
}
|
||||
|
||||
bool UDungeonGraph::FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms) const
|
||||
{
|
||||
return FilterAndSortRooms(RoomList, FromDoor, SortedRooms, FScoreCallback());
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent UDungeonGraph::GetDungeonBounds(const FTransform& Transform) const
|
||||
{
|
||||
const FVector RoomUnit = UDungeonSettings::GetRoomUnit(Generator.IsValid() ? Generator->SettingsOverrides : nullptr);
|
||||
return Dungeon::ToWorld(Bounds.GetBounds(), RoomUnit, Transform);
|
||||
}
|
||||
|
||||
FBoxMinAndMax UDungeonGraph::GetIntBounds() const
|
||||
{
|
||||
return Bounds.GetBounds();
|
||||
}
|
||||
|
||||
URoom* UDungeonGraph::GetRoomByIndex(int64 Index) const
|
||||
{
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
if (Room->GetRoomID() == Index)
|
||||
return Room;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UDungeonGraph::Clear()
|
||||
{
|
||||
// Call cleanup for each room
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
const URoomData* Data = Room->GetRoomData();
|
||||
check(IsValid(Data));
|
||||
Data->CleanupRoom(Room, this);
|
||||
}
|
||||
|
||||
// Clear out data
|
||||
Rooms.Empty();
|
||||
RoomConnections.Empty();
|
||||
|
||||
RebuildBounds();
|
||||
RebuildOctree();
|
||||
}
|
||||
|
||||
int UDungeonGraph::CountRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const
|
||||
{
|
||||
int count = 0;
|
||||
for (const URoom* Room : Rooms)
|
||||
{
|
||||
if (Predicate(Room))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void UDungeonGraph::GetRoomsByPredicate(TArray<URoom*>& OutRooms, TFunction<bool(const URoom*)> Predicate) const
|
||||
{
|
||||
OutRooms.Empty();
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
if (Predicate(Room))
|
||||
OutRooms.Add(Room);
|
||||
}
|
||||
}
|
||||
|
||||
const URoom* UDungeonGraph::FindFirstRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const
|
||||
{
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
if (Predicate(Room))
|
||||
return Room;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UDungeonGraph::TraverseRooms(const TSet<URoom*>& InRooms, TSet<URoom*>* OutRooms, uint32 Distance, TFunction<void(URoom*, uint32)> Func)
|
||||
{
|
||||
TSet<URoom*> openList(InRooms);
|
||||
TSet<URoom*> closedList, currentList;
|
||||
const uint32 MaxDistance = Distance;
|
||||
while (Distance > 0 && openList.Num() > 0)
|
||||
{
|
||||
for (URoom* openRoom : openList)
|
||||
closedList.Add(openRoom);
|
||||
|
||||
Swap(currentList, openList);
|
||||
openList.Empty();
|
||||
for (URoom* currentRoom : currentList)
|
||||
{
|
||||
Func(currentRoom, MaxDistance - Distance);
|
||||
for (int i = 0; i < currentRoom->GetConnectionCount(); ++i)
|
||||
{
|
||||
URoom* nextRoom = currentRoom->GetConnectedRoom(i).Get();
|
||||
if (IsValid(nextRoom) && !closedList.Contains(nextRoom))
|
||||
openList.Add(nextRoom);
|
||||
}
|
||||
}
|
||||
--Distance;
|
||||
}
|
||||
|
||||
if (OutRooms != nullptr)
|
||||
Swap(*OutRooms, closedList);
|
||||
}
|
||||
|
||||
// Do one cycle of BFS (dequeue one room from Queue, then check all its connections to add them in MarkedThis and filling ParentMap)
|
||||
// Fills OutCommon if a connection has been found in MarkedOther
|
||||
// Returns true if OutCommon had been filled
|
||||
bool BFS_Cycle(TQueue<const URoom*>& Queue, TSet<const URoom*>& MarkedThis, const TSet<const URoom*>& MarkedOther, TMap<const URoom*, const URoom*>& ParentMap, const URoom*& OutCommon, bool IgnoreLocked)
|
||||
{
|
||||
const URoom* Current = nullptr;
|
||||
const URoom* Next = nullptr;
|
||||
|
||||
Queue.Dequeue(Current);
|
||||
|
||||
for (const auto& Conn : Current->GetAllConnections())
|
||||
{
|
||||
if (!Conn.IsValid())
|
||||
continue;
|
||||
|
||||
if (!IgnoreLocked && Conn->IsDoorLocked())
|
||||
continue;
|
||||
|
||||
Next = Conn->GetOtherRoom(Current).Get();
|
||||
if (!IsValid(Next))
|
||||
continue;
|
||||
|
||||
if (!IgnoreLocked && Next->IsLocked())
|
||||
continue;
|
||||
|
||||
if (MarkedThis.Contains(Next))
|
||||
continue;
|
||||
|
||||
ParentMap.Add(Next, Current);
|
||||
|
||||
// Check intersection with other side
|
||||
if (MarkedOther.Contains(Next))
|
||||
{
|
||||
OutCommon = Next;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Queue.Enqueue(Next);
|
||||
MarkedThis.Add(Next);
|
||||
}
|
||||
}
|
||||
|
||||
return OutCommon != nullptr;
|
||||
}
|
||||
|
||||
void ReconstructPath(const URoom* Common, const TMap<const URoom*, const URoom*>& ParentsForward, const TMap<const URoom*, const URoom*>& ParentsReverse, TArray<const URoom*>& OutPath)
|
||||
{
|
||||
OutPath.Empty();
|
||||
|
||||
if (Common == nullptr)
|
||||
return;
|
||||
|
||||
// Adds the first part of the path (From -> Common)
|
||||
const URoom* const* Current = &Common;
|
||||
while ((Current = ParentsForward.Find(*Current)) != nullptr)
|
||||
{
|
||||
OutPath.EmplaceAt(0, *Current);
|
||||
}
|
||||
|
||||
// Common room between
|
||||
OutPath.Add(Common);
|
||||
|
||||
// Adds the second part of the path (Common -> To)
|
||||
Current = &Common;
|
||||
while ((Current = ParentsReverse.Find(*Current)) != nullptr)
|
||||
{
|
||||
OutPath.Add(*Current);
|
||||
}
|
||||
}
|
||||
|
||||
// Uses Bidirectional BFS to find a path between A and B
|
||||
bool UDungeonGraph::FindPath(const URoom* From, const URoom* To, TArray<const URoom*>* OutPath, bool IgnoreLocked)
|
||||
{
|
||||
if (OutPath)
|
||||
OutPath->Empty();
|
||||
|
||||
if (!IsValid(From) || !IsValid(To))
|
||||
return false;
|
||||
|
||||
// Always path between a room and itself
|
||||
if (From == To)
|
||||
{
|
||||
if (OutPath)
|
||||
OutPath->Add(From);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IgnoreLocked && (From->IsLocked() || To->IsLocked()))
|
||||
return false;
|
||||
|
||||
// Bidirectional BFS initialization
|
||||
TMap<const URoom*, const URoom*> ParentsForward, ParentsReverse;
|
||||
TSet<const URoom*> MarkedForward, MarkedReverse; // (visited rooms)
|
||||
TQueue<const URoom*> QueueForward, QueueReverse; // (rooms to visit)
|
||||
QueueForward.Enqueue(From);
|
||||
QueueReverse.Enqueue(To);
|
||||
MarkedForward.Add(From);
|
||||
MarkedReverse.Add(To);
|
||||
|
||||
// Both are filled when during either cycle an intersection is found
|
||||
const URoom* Common = nullptr;
|
||||
|
||||
// Bidirectional BFS
|
||||
while (Common == nullptr && !QueueForward.IsEmpty() && !QueueReverse.IsEmpty())
|
||||
{
|
||||
// BFS from A
|
||||
if (!BFS_Cycle(QueueForward, MarkedForward, MarkedReverse, ParentsForward, Common, IgnoreLocked))
|
||||
{
|
||||
// BFS from B if no common found
|
||||
BFS_Cycle(QueueReverse, MarkedReverse, MarkedForward, ParentsReverse, Common, IgnoreLocked);
|
||||
}
|
||||
}
|
||||
|
||||
// Intersection has been found between MarkedForward and MarkedReverse
|
||||
if (Common != nullptr && OutPath != nullptr)
|
||||
{
|
||||
ReconstructPath(Common, ParentsForward, ParentsReverse, *OutPath);
|
||||
}
|
||||
|
||||
return Common != nullptr;
|
||||
}
|
||||
|
||||
void CopyRooms(TArray<URoom*>& To, TArray<URoom*>& From)
|
||||
{
|
||||
for (URoom* Room : From)
|
||||
{
|
||||
if (Room->Instance)
|
||||
DungeonLog_Debug("[%s] Loaded Level: %s", *GetNameSafe(Room), *GetNameSafe(Room->Instance->GetLoadedLevel()));
|
||||
}
|
||||
|
||||
To = TArray<URoom*>(From);
|
||||
}
|
||||
|
||||
void UDungeonGraph::SynchronizeRooms()
|
||||
{
|
||||
AActor* Owner = GetTypedOuter<AActor>();
|
||||
if (!IsValid(Owner))
|
||||
return;
|
||||
|
||||
if (Owner->HasAuthority())
|
||||
{
|
||||
Owner->FlushNetDormancy();
|
||||
RegisterReplicableSubobjects(false);
|
||||
CopyRooms(ReplicatedRooms, Rooms);
|
||||
RegisterReplicableSubobjects(true);
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(UDungeonGraph, ReplicatedRooms, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyRooms(Rooms, ReplicatedRooms);
|
||||
RebuildBounds();
|
||||
RebuildOctree();
|
||||
DungeonLog_Debug("Synchronized Rooms from server (length: %d)", Rooms.Num());
|
||||
for (const URoom* Room : Rooms)
|
||||
{
|
||||
DungeonLog_Debug(" - %s (Data: %s Valid: %d)", *GetNameSafe(Room), *GetNameSafe(Room->GetRoomData()), IsValid(Room->GetRoomData()));
|
||||
}
|
||||
}
|
||||
|
||||
bIsDirty = false;
|
||||
}
|
||||
|
||||
bool UDungeonGraph::AreRoomsLoaded(int32& NbRoomLoaded) const
|
||||
{
|
||||
NbRoomLoaded = 0;
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
if (Room->IsInstanceLoaded())
|
||||
NbRoomLoaded++;
|
||||
}
|
||||
return NbRoomLoaded >= Rooms.Num();
|
||||
}
|
||||
|
||||
bool UDungeonGraph::AreRoomsUnloaded(int32& NbRoomUnloaded) const
|
||||
{
|
||||
NbRoomUnloaded = 0;
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
if (!IsValid(Room) || Room->IsInstanceUnloaded())
|
||||
NbRoomUnloaded++;
|
||||
}
|
||||
return NbRoomUnloaded >= Rooms.Num();
|
||||
}
|
||||
|
||||
bool UDungeonGraph::AreRoomsInitialized(int32& NbRoomInitialized) const
|
||||
{
|
||||
NbRoomInitialized = 0;
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
if (Room->IsInstanceInitialized())
|
||||
NbRoomInitialized++;
|
||||
}
|
||||
return NbRoomInitialized >= Rooms.Num();
|
||||
}
|
||||
|
||||
bool UDungeonGraph::AreRoomsReady() const
|
||||
{
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
if (!(IsValid(Room) && Room->IsReady()))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void UDungeonGraph::SpawnAllDoors()
|
||||
{
|
||||
// Spawn doors only on server
|
||||
// They will be replicated on the clients
|
||||
if (!HasAuthority())
|
||||
return;
|
||||
|
||||
checkf(Generator.IsValid(), TEXT("Spawning dungeon's doors is only available with a ADungeonGenerator outer."));
|
||||
|
||||
for (auto* RoomConnection : RoomConnections)
|
||||
{
|
||||
if (RoomConnection->IsDoorInstanced())
|
||||
continue;
|
||||
RoomConnection->InstantiateDoor(GetWorld(), Generator.Get(), Generator->UseGeneratorTransform());
|
||||
}
|
||||
}
|
||||
|
||||
void UDungeonGraph::LoadAllRooms()
|
||||
{
|
||||
// When a level is correct, load all rooms
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
Room->Instantiate(GetWorld());
|
||||
}
|
||||
|
||||
SpawnAllDoors();
|
||||
}
|
||||
|
||||
void UDungeonGraph::UnloadAllRooms()
|
||||
{
|
||||
if (HasAuthority())
|
||||
{
|
||||
for (auto* RoomConnection : RoomConnections)
|
||||
{
|
||||
RoomConnection->DestroyDoor();
|
||||
}
|
||||
}
|
||||
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
check(Room);
|
||||
Room->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void UDungeonGraph::UpdateBounds(const URoom* Room)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
Bounds += Room->GetVoxelBounds();
|
||||
}
|
||||
|
||||
void UDungeonGraph::RebuildBounds()
|
||||
{
|
||||
Bounds = FVoxelBounds();
|
||||
for (const URoom* Room : Rooms)
|
||||
{
|
||||
UpdateBounds(Room);
|
||||
}
|
||||
}
|
||||
|
||||
void UDungeonGraph::UpdateOctree(URoom* Room)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
for (int i = 0; i < Room->GetSubBoundsCount(); ++i)
|
||||
{
|
||||
Octree.AddElement(FDungeonOctreeElement(Room, i));
|
||||
}
|
||||
}
|
||||
|
||||
void UDungeonGraph::RebuildOctree()
|
||||
{
|
||||
Octree.Destroy();
|
||||
for (URoom* Room : Rooms)
|
||||
{
|
||||
UpdateOctree(Room);
|
||||
}
|
||||
}
|
||||
|
||||
void UDungeonGraph::OnRep_Rooms()
|
||||
{
|
||||
DungeonLog_Debug("Replicated Rooms Changed! (length: %d)", ReplicatedRooms.Num());
|
||||
for (int i = 0; i < ReplicatedRooms.Num(); ++i)
|
||||
{
|
||||
// Trigger Room List Changed only when all received rooms are valid
|
||||
if (!IsValid(ReplicatedRooms[i]))
|
||||
return;
|
||||
|
||||
DungeonLog_Debug("Replicated Room [%d]: %s", i, *GetNameSafe(ReplicatedRooms[i]));
|
||||
}
|
||||
|
||||
DungeonLog_Debug("Trigger Dungeon Reload!");
|
||||
MarkDirty();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "DungeonOctree.h"
|
||||
#include "Room.h"
|
||||
|
||||
FDungeonOctreeElement::FDungeonOctreeElement(URoom* Room, int32 BoxIndex)
|
||||
{
|
||||
check(IsValid(Room));
|
||||
this->Room = Room;
|
||||
Bounds = Room->GetSubBounds(BoxIndex);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "DungeonSaveProxyArchive.h"
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "DungeonSettings.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
|
||||
UDungeonSettings::UDungeonSettings()
|
||||
: Super()
|
||||
{
|
||||
RoomUnit = Dungeon::RoomUnit();
|
||||
}
|
||||
|
||||
FVector UDungeonSettings::GetRoomUnit(const UDungeonSettings* Settings)
|
||||
{
|
||||
return IsValid(Settings) ? Settings->RoomUnit : Dungeon::RoomUnit();
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Interfaces/DoorInterface.h"
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
|
||||
bool IDungeonCustomSerialization::DispatchFixupReferences(UObject* Obj, UObject* Context)
|
||||
{
|
||||
check(IsValid(Obj));
|
||||
DungeonLog_Debug("[BEGIN] Dispatch 'Fixup References' function from object '%s'.", *GetNameSafe(Obj));
|
||||
|
||||
ObjectUtils::DispatchToObjectAndSubobjects(Obj, [Context](UObject* O) {
|
||||
auto* Custom = Cast<IDungeonCustomSerialization>(O);
|
||||
if (nullptr != Custom)
|
||||
{
|
||||
Custom->FixupReferences(Context);
|
||||
}
|
||||
});
|
||||
|
||||
DungeonLog_Debug("[END] Dispatch 'Fixup References' function from object '%s'.", *GetNameSafe(Obj));
|
||||
return true;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
|
||||
void IDungeonSaveInterface::DispatchPreSaveEvent(UObject* Obj)
|
||||
{
|
||||
check(IsValid(Obj));
|
||||
DungeonLog_Debug("[BEGIN] Dispatch 'Dungeon Pre Save' events from object '%s'.", *GetNameSafe(Obj));
|
||||
|
||||
ObjectUtils::DispatchToObjectAndSubobjects(Obj, [](UObject* O) {
|
||||
if (IsValid(O) && O->Implements<UDungeonSaveInterface>())
|
||||
{
|
||||
IDungeonSaveInterface::Execute_PreSaveDungeon(O);
|
||||
}
|
||||
});
|
||||
|
||||
DungeonLog_Debug("[END] Dispatch 'Dungeon Post Load' events from object '%s'.", *GetNameSafe(Obj));
|
||||
}
|
||||
|
||||
void IDungeonSaveInterface::DispatchPostLoadEvent(UObject* Obj)
|
||||
{
|
||||
check(IsValid(Obj));
|
||||
DungeonLog_Debug("[BEGIN] Dispatch 'Dungeon Post Load' events from object '%s'.", *GetNameSafe(Obj));
|
||||
|
||||
ObjectUtils::DispatchToObjectAndSubobjects(Obj, [](UObject* O) {
|
||||
if (IsValid(O) && O->Implements<UDungeonSaveInterface>())
|
||||
{
|
||||
IDungeonSaveInterface::Execute_PostLoadDungeon(O);
|
||||
}
|
||||
});
|
||||
|
||||
DungeonLog_Debug("[END] Dispatch 'Dungeon Post Load' events from object '%s'.", *GetNameSafe(Obj));
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Interfaces/GeneratorProvider.h"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Interfaces/RoomActorGuid.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
|
||||
UObject* IRoomActorGuid::GetImplementer(AActor* Actor)
|
||||
{
|
||||
return ActorUtils::GetInterfaceImplementer<URoomActorGuid>(Actor);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Interfaces/RoomContainer.h"
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeon.h"
|
||||
#include "Developer/Settings/Public/ISettingsModule.h"
|
||||
#include "Developer/Settings/Public/ISettingsSection.h"
|
||||
#include "ProceduralDungeonSettings.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Misc/EngineVersionComparison.h"
|
||||
#include "UObject/CoreRedirects.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FProceduralDungeonModule"
|
||||
|
||||
#if WITH_EDITOR && UE_VERSION_NEWER_THAN(5, 4, 0)
|
||||
#define ACTOR_REPLACEMENT_FIX_HACK 1
|
||||
#else
|
||||
#define ACTOR_REPLACEMENT_FIX_HACK 0
|
||||
#endif
|
||||
|
||||
// ----- Hack to fix Room references issues of RoomLevel actors in PIE for UE 5.4
|
||||
#if ACTOR_REPLACEMENT_FIX_HACK
|
||||
#include "RoomLevel.h"
|
||||
#include "Room.h"
|
||||
|
||||
FDelegateHandle ObjectReplacedHandle;
|
||||
void ObjectReplaced(const FCoreUObjectDelegates::FReplacementObjectMap& ReplacementMap)
|
||||
{
|
||||
for (const auto& Pair : ReplacementMap)
|
||||
{
|
||||
ARoomLevel* OldActor = Cast<ARoomLevel>(Pair.Key);
|
||||
ARoomLevel* NewActor = Cast<ARoomLevel>(Pair.Value);
|
||||
|
||||
if (!OldActor || !NewActor)
|
||||
continue;
|
||||
|
||||
if (OldActor->HasAllFlags(EObjectFlags::RF_ClassDefaultObject) || NewActor->HasAllFlags(EObjectFlags::RF_ClassDefaultObject))
|
||||
continue;
|
||||
|
||||
URoom* RoomInstance = OldActor->GetRoom();
|
||||
OldActor->Room = nullptr;
|
||||
if (!IsValid(RoomInstance))
|
||||
continue;
|
||||
|
||||
// Fixup Room reference not properly carried over during actor replacement process
|
||||
NewActor->Init(RoomInstance);
|
||||
DungeonLog_Debug("Fixed Room reference ('%s' -> '%s')", *GetNameSafe(OldActor), *GetNameSafe(NewActor));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// ----- End Hack
|
||||
|
||||
void FProceduralDungeonModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
RegisterSettings();
|
||||
|
||||
#if ACTOR_REPLACEMENT_FIX_HACK
|
||||
ObjectReplacedHandle = FCoreUObjectDelegates::OnObjectsReinstanced.AddStatic(ObjectReplaced);
|
||||
DungeonLog_Debug("Use Actor Replacement Hack");
|
||||
#endif
|
||||
|
||||
TArray<FCoreRedirect> Redirects;
|
||||
Redirects.Emplace(ECoreRedirectFlags::Type_Property, TEXT("/Script/ProceduralDungeon.Room.RoomData"), TEXT("/Script/ProceduralDungeon.Room.SoftRoomData"));
|
||||
FCoreRedirects::AddRedirectList(Redirects, TEXT("ProceduralDungeon"));
|
||||
}
|
||||
|
||||
void FProceduralDungeonModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
if (UObjectInitialized())
|
||||
{
|
||||
UnregisterSettings();
|
||||
}
|
||||
|
||||
#if ACTOR_REPLACEMENT_FIX_HACK
|
||||
FCoreUObjectDelegates::OnObjectsReinstanced.Remove(ObjectReplacedHandle);
|
||||
#endif
|
||||
}
|
||||
|
||||
void FProceduralDungeonModule::RegisterSettings()
|
||||
{
|
||||
// Registering some settings is just a matter of exposing the default UObject of
|
||||
// your desired class, feel free to add here all those settings you want to expose
|
||||
// to your LDs or artists.
|
||||
|
||||
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
|
||||
{
|
||||
// Register the settings
|
||||
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "Procedural Dungeon",
|
||||
LOCTEXT("RuntimeGeneralSettingsName", "Procedural Dungeon"),
|
||||
LOCTEXT("RuntimeGeneralSettingsDescription", "Configuration for the Procedural Dungeon plugin"),
|
||||
GetMutableDefault<UProceduralDungeonSettings>()
|
||||
);
|
||||
|
||||
// Register the save handler to your settings, you might want to use it to
|
||||
// validate those or just act to settings changes.
|
||||
if (SettingsSection.IsValid())
|
||||
{
|
||||
SettingsSection->OnModified().BindRaw(this, &FProceduralDungeonModule::HandleSettingsSaved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FProceduralDungeonModule::UnregisterSettings()
|
||||
{
|
||||
// Ensure to unregister all of your registered settings here, hot-reload would
|
||||
// otherwise yield unexpected results.
|
||||
|
||||
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
|
||||
{
|
||||
SettingsModule->UnregisterSettings("Project", "Plugins", "Procedural Dungeon");
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for when the settings were saved.
|
||||
bool FProceduralDungeonModule::HandleSettingsSaved()
|
||||
{
|
||||
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
|
||||
bool ResaveSettings = false;
|
||||
|
||||
// You can put any validation code in here and resave the settings in case an invalid
|
||||
// value has been entered
|
||||
|
||||
if (ResaveSettings)
|
||||
{
|
||||
Settings->SaveConfig();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FProceduralDungeonModule, ProceduralDungeon)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonCustomVersion.h"
|
||||
#include "Serialization/CustomVersion.h"
|
||||
|
||||
const FGuid FProceduralDungeonCustomVersion::GUID(0x07E63959, 0x72E5DEE1, 0x7B00F72A, 0x1B442349);
|
||||
|
||||
// Register the custom version with core
|
||||
FCustomVersionRegistration GRegisterDungeonCustomVersion(FProceduralDungeonCustomVersion::GUID, FProceduralDungeonCustomVersion::LatestVersion, TEXT("ProcDungeonVer"));
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "ProceduralDungeonSettings.h"
|
||||
#include "Engine/Engine.h" // GEngine
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogProceduralDungeon);
|
||||
|
||||
namespace
|
||||
{
|
||||
bool ShowLogsOnScreen(float& _duration)
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
_duration = Settings->PrintDebugDuration;
|
||||
return Settings->OnScreenPrintDebug;
|
||||
}
|
||||
} //namespace
|
||||
|
||||
void LogOnScreen(const FString& Message, FColor Color, bool bForceOnScreen)
|
||||
{
|
||||
float Duration;
|
||||
if (::ShowLogsOnScreen(Duration) || bForceOnScreen)
|
||||
{
|
||||
GEngine->AddOnScreenDebugMessage(-1, Duration, Color, Message);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonSettings.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
|
||||
UProceduralDungeonSettings::UProceduralDungeonSettings(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
// Dungeon settings
|
||||
RoomUnit = FVector(1000, 1000, 400);
|
||||
DoorSize = FVector(40, 640, 400);
|
||||
DoorOffset = 0.0f;
|
||||
CanLoop = true;
|
||||
MaxGenerationTry = 500;
|
||||
MaxRoomPlacementTry = 10;
|
||||
RoomLimit = 100;
|
||||
|
||||
// Occlusion settings
|
||||
OcclusionCulling = true;
|
||||
//LegacyOcclusion = true;
|
||||
OcclusionDistance = 2;
|
||||
OccludeDynamicActors = true;
|
||||
|
||||
// Debug settings
|
||||
DrawDebug = true;
|
||||
bDrawOnlyWhenEditingRooms = false;
|
||||
ShowRoomOrigin = false;
|
||||
bFlipDoorArrowSide = false;
|
||||
DoorArrowLength = 300.0f;
|
||||
DoorArrowHeadSize = 300.0f;
|
||||
OnScreenPrintDebug = false;
|
||||
PrintDebugDuration = 60.0f;
|
||||
|
||||
// Register console variables.
|
||||
|
||||
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Occlusion")
|
||||
, OcclusionCulling
|
||||
, TEXT("Enable/disable the plugin's occlusion culling system.")
|
||||
, EConsoleVariableFlags::ECVF_Cheat
|
||||
);
|
||||
|
||||
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Occlusion.Distance")
|
||||
, OcclusionDistance
|
||||
, TEXT("Change the number of room shown by the plugin's occlusion culling system.\n")
|
||||
TEXT("1 means only the player's room is visible. 0 or negative means no room visible at all. 2 or more will show the connected rooms to the player based on their number of connection.")
|
||||
, EConsoleVariableFlags::ECVF_Cheat
|
||||
);
|
||||
|
||||
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Occlusion.DynamicActors")
|
||||
, OccludeDynamicActors
|
||||
, TEXT("Enable/disable the occlusion of actors with a RoomVisibility component attached on them.")
|
||||
, EConsoleVariableFlags::ECVF_Cheat
|
||||
);
|
||||
|
||||
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Draw")
|
||||
, DrawDebug
|
||||
, TEXT("Enable/disable the debug drawings of the rooms and doors.")
|
||||
, EConsoleVariableFlags::ECVF_Cheat
|
||||
);
|
||||
|
||||
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Draw.EditingOnly")
|
||||
, bDrawOnlyWhenEditingRooms
|
||||
, TEXT("Enable/disable the debug drawings to be only shown when editing a room level.")
|
||||
, EConsoleVariableFlags::ECVF_Cheat
|
||||
);
|
||||
|
||||
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Log.OnScreen")
|
||||
, OnScreenPrintDebug
|
||||
, TEXT("Enable/disable the on-screen logging of the plugin.")
|
||||
, EConsoleVariableFlags::ECVF_Cheat
|
||||
);
|
||||
|
||||
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Log.Duration")
|
||||
, PrintDebugDuration
|
||||
, TEXT("Change the on-screen logging duration (in seconds) of the plugin.")
|
||||
, EConsoleVariableFlags::ECVF_Cheat
|
||||
);
|
||||
}
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "DoorType.h"
|
||||
|
||||
bool operator!(const EDoorDirection& Direction)
|
||||
{
|
||||
return Direction == EDoorDirection::NbDirection;
|
||||
}
|
||||
|
||||
EDoorDirection operator+(const EDoorDirection& A, const EDoorDirection& B)
|
||||
{
|
||||
check(!!A && !!B);
|
||||
return static_cast<EDoorDirection>(static_cast<uint8>((static_cast<uint8>(A) + static_cast<uint8>(B))) % static_cast<uint8>(EDoorDirection::NbDirection));
|
||||
}
|
||||
|
||||
EDoorDirection operator-(const EDoorDirection& A, const EDoorDirection& B)
|
||||
{
|
||||
check(!!A && !!B);
|
||||
return static_cast<EDoorDirection>(static_cast<uint8>((static_cast<uint8>(A) - static_cast<uint8>(B))) % static_cast<uint8>(EDoorDirection::NbDirection));
|
||||
}
|
||||
|
||||
EDoorDirection& operator+=(EDoorDirection& A, const EDoorDirection& B)
|
||||
{
|
||||
A = A + B;
|
||||
return A;
|
||||
}
|
||||
|
||||
EDoorDirection& operator-=(EDoorDirection& A, const EDoorDirection& B)
|
||||
{
|
||||
A = A - B;
|
||||
return A;
|
||||
}
|
||||
|
||||
EDoorDirection& operator++(EDoorDirection& Direction)
|
||||
{
|
||||
Direction = Direction + EDoorDirection::East;
|
||||
return Direction;
|
||||
}
|
||||
|
||||
EDoorDirection& operator--(EDoorDirection& Direction)
|
||||
{
|
||||
Direction = Direction - EDoorDirection::East;
|
||||
return Direction;
|
||||
}
|
||||
|
||||
EDoorDirection operator++(EDoorDirection& Direction, int)
|
||||
{
|
||||
EDoorDirection old {Direction};
|
||||
++Direction;
|
||||
return old;
|
||||
}
|
||||
|
||||
EDoorDirection operator--(EDoorDirection& Direction, int)
|
||||
{
|
||||
EDoorDirection old {Direction};
|
||||
--Direction;
|
||||
return old;
|
||||
}
|
||||
|
||||
EDoorDirection operator-(const EDoorDirection& Direction)
|
||||
{
|
||||
return EDoorDirection::North - Direction;
|
||||
}
|
||||
|
||||
EDoorDirection operator~(const EDoorDirection& Direction)
|
||||
{
|
||||
return Direction + EDoorDirection::South;
|
||||
}
|
||||
|
||||
FIntVector ToIntVector(const EDoorDirection& Direction)
|
||||
{
|
||||
FIntVector Dir = FIntVector::ZeroValue;
|
||||
switch (Direction)
|
||||
{
|
||||
case EDoorDirection::North:
|
||||
Dir.X = 1;
|
||||
break;
|
||||
case EDoorDirection::East:
|
||||
Dir.Y = 1;
|
||||
break;
|
||||
case EDoorDirection::West:
|
||||
Dir.Y = -1;
|
||||
break;
|
||||
case EDoorDirection::South:
|
||||
Dir.X = -1;
|
||||
break;
|
||||
default:
|
||||
checkNoEntry();
|
||||
}
|
||||
return Dir;
|
||||
}
|
||||
|
||||
FVector ToVector(const EDoorDirection& Direction)
|
||||
{
|
||||
FVector Dir = FVector::ZeroVector;
|
||||
switch (Direction)
|
||||
{
|
||||
case EDoorDirection::North:
|
||||
Dir.X = 1.0f;
|
||||
break;
|
||||
case EDoorDirection::East:
|
||||
Dir.Y = 1.0f;
|
||||
break;
|
||||
case EDoorDirection::West:
|
||||
Dir.Y = -1.0f;
|
||||
break;
|
||||
case EDoorDirection::South:
|
||||
Dir.X = -1.0f;
|
||||
break;
|
||||
default:
|
||||
checkNoEntry();
|
||||
}
|
||||
return Dir;
|
||||
}
|
||||
|
||||
FQuat ToQuaternion(const EDoorDirection& Direction)
|
||||
{
|
||||
check(!!Direction);
|
||||
return FRotator(0.0f, ToAngle(Direction), 0.0f).Quaternion();
|
||||
}
|
||||
|
||||
float ToAngle(const EDoorDirection& Direction)
|
||||
{
|
||||
check(!!Direction);
|
||||
return 90.0f * static_cast<uint8>(Direction);
|
||||
}
|
||||
|
||||
FIntVector Rotate(const FIntVector& Pos, const EDoorDirection& Rot)
|
||||
{
|
||||
FIntVector NewPos = Pos;
|
||||
switch (Rot)
|
||||
{
|
||||
case EDoorDirection::North:
|
||||
NewPos = Pos;
|
||||
break;
|
||||
case EDoorDirection::West:
|
||||
NewPos.Y = -Pos.X;
|
||||
NewPos.X = Pos.Y;
|
||||
break;
|
||||
case EDoorDirection::East:
|
||||
NewPos.Y = Pos.X;
|
||||
NewPos.X = -Pos.Y;
|
||||
break;
|
||||
case EDoorDirection::South:
|
||||
NewPos.Y = -Pos.Y;
|
||||
NewPos.X = -Pos.X;
|
||||
break;
|
||||
default:
|
||||
checkNoEntry();
|
||||
}
|
||||
return NewPos;
|
||||
}
|
||||
|
||||
FVector Rotate(const FVector& Pos, const EDoorDirection& Rot)
|
||||
{
|
||||
FVector NewPos = Pos;
|
||||
switch (Rot)
|
||||
{
|
||||
case EDoorDirection::North:
|
||||
NewPos = Pos;
|
||||
break;
|
||||
case EDoorDirection::West:
|
||||
NewPos.Y = -Pos.X;
|
||||
NewPos.X = Pos.Y;
|
||||
break;
|
||||
case EDoorDirection::East:
|
||||
NewPos.Y = Pos.X;
|
||||
NewPos.X = -Pos.Y;
|
||||
break;
|
||||
case EDoorDirection::South:
|
||||
NewPos.Y = -Pos.Y;
|
||||
NewPos.X = -Pos.X;
|
||||
break;
|
||||
default:
|
||||
checkNoEntry();
|
||||
}
|
||||
return NewPos;
|
||||
}
|
||||
|
||||
FIntVector Transform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation)
|
||||
{
|
||||
return Rotate(Pos, Rotation) + Translation;
|
||||
}
|
||||
|
||||
FIntVector InverseTransform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation)
|
||||
{
|
||||
return Rotate(Pos - Translation, -Rotation);
|
||||
}
|
||||
|
||||
EDoorDirection Transform(const EDoorDirection& Direction, const EDoorDirection& Rotation)
|
||||
{
|
||||
return Direction + Rotation;
|
||||
}
|
||||
|
||||
EDoorDirection InverseTransform(const EDoorDirection& Direction, const EDoorDirection& Rotation)
|
||||
{
|
||||
return Direction - Rotation;
|
||||
}
|
||||
|
||||
// ############ FDoorDef ##############
|
||||
|
||||
const FDoorDef FDoorDef::Invalid(FIntVector::ZeroValue, EDoorDirection::NbDirection);
|
||||
|
||||
FDoorDef::FDoorDef(const FIntVector& InPosition, EDoorDirection InDirection, UDoorType* InType)
|
||||
{
|
||||
Position = InPosition;
|
||||
Direction = InDirection;
|
||||
Type = InType;
|
||||
}
|
||||
|
||||
bool FDoorDef::IsValid() const
|
||||
{
|
||||
return Direction != EDoorDirection::NbDirection;
|
||||
}
|
||||
|
||||
bool FDoorDef::operator==(const FDoorDef& Other) const
|
||||
{
|
||||
return Position == Other.Position && Direction == Other.Direction;
|
||||
}
|
||||
|
||||
bool FDoorDef::AreCompatible(const FDoorDef& A, const FDoorDef& B)
|
||||
{
|
||||
return UDoorType::AreCompatible(A.Type, B.Type);
|
||||
}
|
||||
|
||||
FVector FDoorDef::GetDoorSize() const
|
||||
{
|
||||
return UDoorType::GetSize(Type);
|
||||
}
|
||||
|
||||
float FDoorDef::GetDoorOffset() const
|
||||
{
|
||||
return UDoorType::GetOffset(Type);
|
||||
}
|
||||
|
||||
FColor FDoorDef::GetDoorColor() const
|
||||
{
|
||||
return UDoorType::GetColor(Type);
|
||||
}
|
||||
|
||||
FString FDoorDef::GetTypeName() const
|
||||
{
|
||||
return ::IsValid(Type) ? Type->GetName() : TEXT("Default");
|
||||
}
|
||||
|
||||
FString FDoorDef::ToString() const
|
||||
{
|
||||
FText DirectionName;
|
||||
UEnum::GetDisplayValueAsText(Direction, DirectionName);
|
||||
return FString::Printf(TEXT("(%d,%d,%d) [%s]"), Position.X, Position.Y, Position.Z, *DirectionName.ToString());
|
||||
}
|
||||
|
||||
FDoorDef FDoorDef::GetOpposite() const
|
||||
{
|
||||
FDoorDef OppositeDoor(*this);
|
||||
OppositeDoor.Position = Position + ToIntVector(Direction);
|
||||
OppositeDoor.Direction = ~Direction;
|
||||
return OppositeDoor;
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent FDoorDef::GetBounds(const FVector RoomUnit, bool bIncludeOffset) const
|
||||
{
|
||||
const FVector RotatedDoorSize = Rotate(GetDoorSize(), (!Direction) ? EDoorDirection::North : Direction).GetAbs();
|
||||
const FVector WorldPosition = GetRealDoorPosition(*this, RoomUnit, bIncludeOffset) + FVector(0, 0, RotatedDoorSize.Z * 0.5f);
|
||||
return FBoxCenterAndExtent(WorldPosition, 0.5f * RotatedDoorSize);
|
||||
}
|
||||
|
||||
FVector FDoorDef::GetRealDoorPosition(const FDoorDef& DoorDef, const FVector RoomUnit, bool bIncludeOffset)
|
||||
{
|
||||
return GetRealDoorPosition(DoorDef.Position, DoorDef.Direction, RoomUnit, bIncludeOffset ? DoorDef.GetDoorOffset() : 0.0f);
|
||||
}
|
||||
|
||||
FVector FDoorDef::GetRealDoorPosition(FIntVector DoorCell, EDoorDirection DoorRot, const FVector RoomUnit, float DoorOffset)
|
||||
{
|
||||
const FVector CellPosition = FVector(DoorCell);
|
||||
const FVector DirectionOffset = !DoorRot ? FVector::ZeroVector : (0.5f * ToVector(DoorRot));
|
||||
const FVector HeightOffset = FVector(0, 0, DoorOffset);
|
||||
return RoomUnit * (CellPosition + DirectionOffset + HeightOffset);
|
||||
}
|
||||
|
||||
FQuat FDoorDef::GetRealDoorRotation(const FDoorDef& DoorDef, bool bFlipped)
|
||||
{
|
||||
return FRotator(0, 90 * static_cast<uint8>(DoorDef.Direction) + bFlipped * 180, 0).Quaternion();
|
||||
}
|
||||
|
||||
FDoorDef FDoorDef::Transform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation)
|
||||
{
|
||||
FDoorDef NewDoor = DoorDef;
|
||||
NewDoor.Position = ::Transform(DoorDef.Position, Translation, Rotation);
|
||||
NewDoor.Direction = ::Transform(DoorDef.Direction, Rotation);
|
||||
return NewDoor;
|
||||
}
|
||||
|
||||
FDoorDef FDoorDef::InverseTransform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation)
|
||||
{
|
||||
FDoorDef NewDoor = DoorDef;
|
||||
NewDoor.Position = ::InverseTransform(DoorDef.Position, Translation, Rotation);
|
||||
NewDoor.Direction = ::InverseTransform(DoorDef.Direction, Rotation);
|
||||
return NewDoor;
|
||||
}
|
||||
|
||||
#if !UE_BUILD_SHIPPING
|
||||
void FDoorDef::DrawDebug(const UWorld* World, const FDoorDef& DoorDef, const FVector RoomUnit, const FTransform& Transform, bool bIncludeOffset, bool bIsConnected)
|
||||
{
|
||||
DrawDebug(World, DoorDef.GetDoorColor(), DoorDef.GetDoorSize(), RoomUnit, DoorDef.Position, DoorDef.Direction, Transform, bIncludeOffset ? DoorDef.GetDoorOffset() : 0.0f, bIsConnected);
|
||||
|
||||
// Door debug draw using its bounds
|
||||
//FBoxCenterAndExtent DoorBounds = DoorDef.GetBounds(bIncludeOffset);
|
||||
//DrawDebugBox(World, Transform.TransformPosition(DoorBounds.Center), DoorBounds.Extent, Transform.GetRotation(), FColor::Cyan);
|
||||
}
|
||||
|
||||
void FDoorDef::DrawDebug(const UWorld* World, const FColor& Color, const FVector& DoorSize, const FVector RoomUnit, const FIntVector& DoorCell, const EDoorDirection& DoorRot, const FTransform& Transform, float DoorOffset, bool bIsConnected)
|
||||
{
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
// @TODO: Use FDoorDef::GetBounds here? (should mabye remove this overload and use exclusively the one with FDoorDef?)
|
||||
FQuat DoorRotation = Transform.GetRotation() * ToQuaternion(!DoorRot ? EDoorDirection::North : DoorRot);
|
||||
FVector DoorPosition = Transform.TransformPosition(GetRealDoorPosition(DoorCell, DoorRot, RoomUnit, DoorOffset) + FVector(0, 0, DoorSize.Z * 0.5f));
|
||||
|
||||
// Door frame
|
||||
DrawDebugBox(World, DoorPosition, DoorSize * 0.5f, DoorRotation, Color);
|
||||
|
||||
if (bIsConnected)
|
||||
{
|
||||
// Arrow (there is a room on the other side OR in the editor preview)
|
||||
FVector ArrowVector = (Dungeon::FlipDoorArrow() ? -1.0f : 1.0f) * FVector(Dungeon::DoorArrowLength(), 0.0f, 0.0f);
|
||||
DrawDebugDirectionalArrow(World, DoorPosition, DoorPosition + DoorRotation * ArrowVector, Dungeon::DoorArrowHeadSize(), Color);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cross (there is no room on the other side AND NOT in the editor preview)
|
||||
FVector HalfSize = DoorRotation * FVector(0, DoorSize.Y, DoorSize.Z) * 0.5f;
|
||||
FVector HalfSizeConjugate = DoorRotation * FVector(0, DoorSize.Y, -DoorSize.Z) * 0.5f;
|
||||
DrawDebugLine(World, DoorPosition - HalfSize, DoorPosition + HalfSize, Color);
|
||||
DrawDebugLine(World, DoorPosition - HalfSizeConjugate, DoorPosition + HalfSizeConjugate, Color);
|
||||
}
|
||||
#endif // ENABLE_DRAW_DEBUG
|
||||
}
|
||||
#endif // !UE_BUILD_SHIPPING
|
||||
|
||||
// ############ FBoxMinAndMax ##############
|
||||
|
||||
const FBoxMinAndMax FBoxMinAndMax::Invalid {FIntVector::ZeroValue, FIntVector::ZeroValue};
|
||||
|
||||
FBoxMinAndMax::FBoxMinAndMax(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
SetMinAndMax(A, B);
|
||||
}
|
||||
|
||||
void FBoxMinAndMax::SetMinAndMax(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
Min = IntVector::Min(A, B);
|
||||
Max = IntVector::Max(A, B);
|
||||
}
|
||||
|
||||
bool FBoxMinAndMax::IsValid() const
|
||||
{
|
||||
return Max.X > Min.X && Max.Y > Min.Y && Max.Z > Min.Z;
|
||||
}
|
||||
|
||||
FIntVector FBoxMinAndMax::GetSize() const
|
||||
{
|
||||
return Max - Min;
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent FBoxMinAndMax::ToCenterAndExtent() const
|
||||
{
|
||||
FVector Center = 0.5f * FVector(Min + Max - FIntVector(1, 1, 0));
|
||||
FVector Extent = 0.5f * FVector(Max - Min);
|
||||
return FBoxCenterAndExtent(Center, Extent.GetAbs());
|
||||
}
|
||||
|
||||
bool FBoxMinAndMax::IsInside(const FIntVector& Cell) const
|
||||
{
|
||||
return (Cell.X >= Min.X) && (Cell.X < Max.X)
|
||||
&& (Cell.Y >= Min.Y) && (Cell.Y < Max.Y)
|
||||
&& (Cell.Z >= Min.Z) && (Cell.Z < Max.Z);
|
||||
}
|
||||
|
||||
bool FBoxMinAndMax::IsInside(const FBoxMinAndMax& Other) const
|
||||
{
|
||||
return (Other.Min.X >= Min.X) && (Other.Max.X <= Max.X)
|
||||
&& (Other.Min.Y >= Min.Y) && (Other.Max.Y <= Max.Y)
|
||||
&& (Other.Min.Z >= Min.Z) && (Other.Max.Z <= Max.Z);
|
||||
}
|
||||
|
||||
void FBoxMinAndMax::Rotate(const EDoorDirection& Rot)
|
||||
{
|
||||
FIntVector Compensation = FIntVector::ZeroValue;
|
||||
switch (Rot)
|
||||
{
|
||||
case EDoorDirection::East:
|
||||
Compensation.X = 1;
|
||||
break;
|
||||
case EDoorDirection::West:
|
||||
Compensation.Y = 1;
|
||||
break;
|
||||
case EDoorDirection::South:
|
||||
Compensation.X = 1;
|
||||
Compensation.Y = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const FIntVector A = ::Rotate(Min, Rot) + Compensation;
|
||||
const FIntVector B = ::Rotate(Max, Rot) + Compensation;
|
||||
Min = IntVector::Min(A, B);
|
||||
Max = IntVector::Max(A, B);
|
||||
}
|
||||
|
||||
void FBoxMinAndMax::Extend(const FBoxMinAndMax& Other)
|
||||
{
|
||||
if (Min != Max)
|
||||
{
|
||||
Min = IntVector::Min(Min, Other.Min);
|
||||
Max = IntVector::Max(Max, Other.Max);
|
||||
}
|
||||
else
|
||||
{
|
||||
Min = Other.Min;
|
||||
Max = Other.Max;
|
||||
}
|
||||
}
|
||||
|
||||
FString FBoxMinAndMax::ToString() const
|
||||
{
|
||||
return FString::Printf(TEXT("[(%d, %d, %d), (%d, %d, %d)]"), Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z);
|
||||
}
|
||||
|
||||
FIntVector FBoxMinAndMax::GetClosestPoint(const FIntVector& Point) const
|
||||
{
|
||||
return IntVector::Min(Max, IntVector::Max(Min, Point));
|
||||
}
|
||||
|
||||
bool FBoxMinAndMax::Overlap(const FBoxMinAndMax& A, const FBoxMinAndMax& B)
|
||||
{
|
||||
return (A.Max.X > B.Min.X && A.Min.X < B.Max.X)
|
||||
&& (A.Max.Y > B.Min.Y && A.Min.Y < B.Max.Y)
|
||||
&& (A.Max.Z > B.Min.Z && A.Min.Z < B.Max.Z);
|
||||
}
|
||||
|
||||
FBoxMinAndMax& FBoxMinAndMax::operator+=(const FIntVector& X)
|
||||
{
|
||||
Min += X;
|
||||
Max += X;
|
||||
return *this;
|
||||
}
|
||||
|
||||
FBoxMinAndMax& FBoxMinAndMax::operator-=(const FIntVector& X)
|
||||
{
|
||||
Min -= X;
|
||||
Max -= X;
|
||||
return *this;
|
||||
}
|
||||
|
||||
FBoxMinAndMax FBoxMinAndMax::operator+(const FIntVector& X) const
|
||||
{
|
||||
FBoxMinAndMax NewBox(*this);
|
||||
NewBox += X;
|
||||
return NewBox;
|
||||
}
|
||||
|
||||
FBoxMinAndMax FBoxMinAndMax::operator-(const FIntVector& X) const
|
||||
{
|
||||
FBoxMinAndMax NewBox(*this);
|
||||
NewBox -= X;
|
||||
return NewBox;
|
||||
}
|
||||
|
||||
bool FBoxMinAndMax::operator==(const FBoxMinAndMax& Other) const
|
||||
{
|
||||
return (Min == Other.Min) && (Max == Other.Max);
|
||||
}
|
||||
|
||||
bool FBoxMinAndMax::operator!=(const FBoxMinAndMax& Other) const
|
||||
{
|
||||
return !FBoxMinAndMax::operator==(Other);
|
||||
}
|
||||
|
||||
FBoxMinAndMax Rotate(const FBoxMinAndMax& Box, const EDoorDirection& Rot)
|
||||
{
|
||||
FBoxMinAndMax NewBox(Box);
|
||||
NewBox.Rotate(Rot);
|
||||
return NewBox;
|
||||
}
|
||||
|
||||
FRoomCandidate FRoomCandidate::Invalid = FRoomCandidate();
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonSettings.h"
|
||||
#include "Room.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Math/GenericOctree.h" // FBoxCenterAndExtent
|
||||
#include "ProceduralDungeonTypes.h" // FBoxMinAndMax
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/PlayerState.h"
|
||||
#include "GameFramework/GameState.h"
|
||||
#include "Components/PrimitiveComponent.h"
|
||||
|
||||
FIntVector IntVector::Min(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
return FIntVector(FMath::Min(A.X, B.X), FMath::Min(A.Y, B.Y), FMath::Min(A.Z, B.Z));
|
||||
}
|
||||
|
||||
FIntVector IntVector::Max(const FIntVector& A, const FIntVector& B)
|
||||
{
|
||||
return FIntVector(FMath::Max(A.X, B.X), FMath::Max(A.Y, B.Y), FMath::Max(A.Z, B.Z));
|
||||
}
|
||||
|
||||
void IntVector::MinMax(const FIntVector& A, const FIntVector& B, FIntVector& OutMin, FIntVector& OutMax)
|
||||
{
|
||||
OutMin = Min(A, B);
|
||||
OutMax = Max(A, B);
|
||||
}
|
||||
|
||||
FVector Dungeon::ToWorldLocation(FIntVector RoomPoint, const FVector RoomUnit)
|
||||
{
|
||||
return RoomUnit * (FVector(RoomPoint) - FVector(0.5f, 0.5f, 0.0f));
|
||||
}
|
||||
|
||||
FVector Dungeon::ToWorldVector(FIntVector RoomPoint, const FVector RoomUnit)
|
||||
{
|
||||
return RoomUnit * FVector(RoomPoint);
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent Dungeon::ToWorld(const FBoxMinAndMax& Box, const FVector RoomUnit, const FTransform& Transform)
|
||||
{
|
||||
return ToWorld(Box.ToCenterAndExtent(), RoomUnit, Transform);
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent Dungeon::ToWorld(const FBoxCenterAndExtent& Box, const FVector RoomUnit, const FTransform& Transform)
|
||||
{
|
||||
const FVector Center = Transform.TransformPositionNoScale(RoomUnit * Box.Center);
|
||||
const FVector Extent = Transform.TransformVector(RoomUnit * Box.Extent).GetAbs();
|
||||
return FBoxCenterAndExtent(Center, Extent);
|
||||
}
|
||||
|
||||
FIntVector Dungeon::ToRoomLocation(FVector WorldPoint, const FVector RoomUnit)
|
||||
{
|
||||
const int X = FMath::RoundToInt(0.5f + (WorldPoint.X) / RoomUnit.X);
|
||||
const int Y = FMath::RoundToInt(0.5f + (WorldPoint.Y) / RoomUnit.Y);
|
||||
const int Z = FMath::RoundToInt((WorldPoint.Z) / RoomUnit.Z);
|
||||
return FIntVector(X, Y, Z);
|
||||
}
|
||||
|
||||
FIntVector Dungeon::ToRoomVector(FVector WorldVector, const FVector RoomUnit)
|
||||
{
|
||||
const int X = FMath::RoundToInt(WorldVector.X / RoomUnit.X);
|
||||
const int Y = FMath::RoundToInt(WorldVector.Y / RoomUnit.Y);
|
||||
const int Z = FMath::RoundToInt(WorldVector.Z / RoomUnit.Z);
|
||||
return FIntVector(X, Y, Z);
|
||||
}
|
||||
|
||||
FVector Dungeon::SnapPoint(FVector Point, const FVector RoomUnit)
|
||||
{
|
||||
return ToWorldLocation(ToRoomLocation(Point, RoomUnit), RoomUnit);
|
||||
}
|
||||
|
||||
// =================== Plugin's Settings ========================
|
||||
|
||||
FVector Dungeon::RoomUnit()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->RoomUnit;
|
||||
}
|
||||
|
||||
FVector Dungeon::DefaultDoorSize()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->DoorSize;
|
||||
}
|
||||
|
||||
FColor Dungeon::DefaultDoorColor()
|
||||
{
|
||||
return FColor::Blue;
|
||||
}
|
||||
|
||||
float Dungeon::DoorOffset()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->DoorOffset;
|
||||
}
|
||||
|
||||
bool Dungeon::OcclusionCulling()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->OcclusionCulling;
|
||||
}
|
||||
|
||||
bool Dungeon::UseLegacyOcclusion()
|
||||
{
|
||||
//const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
//return Settings->LegacyOcclusion;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32 Dungeon::OcclusionDistance()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->OcclusionDistance;
|
||||
}
|
||||
|
||||
bool Dungeon::OccludeDynamicActors()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->OccludeDynamicActors;
|
||||
}
|
||||
|
||||
bool Dungeon::DrawDebug()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->DrawDebug;
|
||||
}
|
||||
|
||||
bool Dungeon::DrawOnlyWhenEditingRoom()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->bDrawOnlyWhenEditingRooms;
|
||||
}
|
||||
|
||||
bool Dungeon::ShowRoomOrigin()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->ShowRoomOrigin;
|
||||
}
|
||||
|
||||
bool Dungeon::FlipDoorArrow()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->bFlipDoorArrowSide;
|
||||
}
|
||||
|
||||
float Dungeon::DoorArrowLength()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->DoorArrowLength;
|
||||
}
|
||||
|
||||
float Dungeon::DoorArrowHeadSize()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->DoorArrowHeadSize;
|
||||
}
|
||||
|
||||
bool Dungeon::CanLoop()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->CanLoop;
|
||||
}
|
||||
|
||||
ECollisionChannel Dungeon::RoomObjectType()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->RoomObjectType;
|
||||
}
|
||||
|
||||
uint32 Dungeon::MaxGenerationTryBeforeGivingUp()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->MaxGenerationTry;
|
||||
}
|
||||
|
||||
uint32 Dungeon::MaxRoomPlacementTryBeforeGivingUp()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->MaxRoomPlacementTry;
|
||||
}
|
||||
|
||||
int32 Dungeon::RoomLimit()
|
||||
{
|
||||
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
|
||||
return Settings->RoomLimit;
|
||||
}
|
||||
|
||||
void Dungeon::EnableOcclusionCulling(bool Enable)
|
||||
{
|
||||
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
|
||||
Settings->OcclusionCulling = Enable;
|
||||
}
|
||||
|
||||
void Dungeon::SetOcclusionDistance(int32 Distance)
|
||||
{
|
||||
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
|
||||
Settings->OcclusionDistance = Distance;
|
||||
}
|
||||
|
||||
uint64 Concat(uint32 A, uint32 B)
|
||||
{
|
||||
return (static_cast<uint64>(A) << 32) | static_cast<uint64>(B);
|
||||
}
|
||||
|
||||
uint32 Random::Guid2Seed(FGuid Guid, int64 Salt)
|
||||
{
|
||||
//// CAUTION: This function must not be modified if not necessary!
|
||||
//// Or else it will break behaviour compatibility with previous versions of the plugin!
|
||||
|
||||
// Using PCG-RXS-M-XS found in this paper: https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf (p. 45)
|
||||
|
||||
// Creating a 64bit states from the 128bit Guid and the salt.
|
||||
const uint64 Part1 = Concat(Guid.A, Guid.B);
|
||||
const uint64 Part2 = Concat(Guid.C, Guid.D);
|
||||
uint64 State = Part1 ^ Part2 ^ Salt;
|
||||
|
||||
// Applying PCG-RXS-M-XS to create much more variations from the salt.
|
||||
const uint8 Count = State >> 59; // Extracting the highest 5 bits for the random xorshift below (64-5=59)
|
||||
State ^= State >> (5 + Count); // [RXS] Random xorshift (at least 5 to leave the highest 5 bits untouched)
|
||||
State *= 12605985483714917081u; // [M] Multiplication with a really big odd number
|
||||
State ^= State >> 43; // [XS] Xorshifting 1/3 of the top bits to the 1/3 of the lower bits
|
||||
return static_cast<uint32>(State ^ (State >> 32)); // Folding the top half for the result on the bottom half to convert into a 32bit output.
|
||||
}
|
||||
|
||||
void ObjectUtils::DispatchToObjectAndSubobjects(UObject* Obj, TFunction<void(UObject*)> Func, int32 Depth)
|
||||
{
|
||||
// Calls the function on the object itself.
|
||||
DungeonLog_Debug("[%d] - Dispatch function to object '%s'.", Depth, *GetNameSafe(Obj));
|
||||
Func(Obj);
|
||||
|
||||
// Get all direct subobjects of this object.
|
||||
TArray<UObject*> Subobjects;
|
||||
GetObjectsWithOuter(Obj, Subobjects, /*bIncludeNestedObjects = */ false);
|
||||
|
||||
++Depth;
|
||||
// Recursively dispatch to all subobjects found.
|
||||
for (UObject* Sub : Subobjects)
|
||||
{
|
||||
DispatchToObjectAndSubobjects(Sub, Func, Depth);
|
||||
}
|
||||
}
|
||||
|
||||
FBox ActorUtils::GetActorBoundingBoxForRooms(AActor* Actor, const FTransform& DungeonTransform)
|
||||
{
|
||||
if (!IsValid(Actor))
|
||||
{
|
||||
DungeonLog_Error("Invalid Actor provided.");
|
||||
return FBox(ForceInit);
|
||||
}
|
||||
|
||||
// Copied from AActor::GetComponentsBoundingBox but check also collision response with the room object type
|
||||
FBox ActorBox(ForceInit);
|
||||
Actor->ForEachComponent<UPrimitiveComponent>(/*bIncludeFromChildActors = */ false, [&](const UPrimitiveComponent* Component) {
|
||||
if (Component->IsRegistered()
|
||||
&& Component->IsCollisionEnabled()
|
||||
&& Component->GetCollisionResponseToChannel(Dungeon::RoomObjectType()) != ECollisionResponse::ECR_Ignore)
|
||||
{
|
||||
ActorBox += Component->Bounds.GetBox();
|
||||
}
|
||||
});
|
||||
|
||||
ActorBox = ActorBox.InverseTransformBy(DungeonTransform);
|
||||
return ActorBox;
|
||||
}
|
||||
|
||||
APlayerController* ActorUtils::GetPlayerControllerFromPlayerId(const UObject* WorldContextObject, int32 PlayerId)
|
||||
{
|
||||
UWorld* World = WorldContextObject->GetWorld();
|
||||
if (!IsValid(World))
|
||||
return nullptr;
|
||||
|
||||
AGameStateBase* GameState = World->GetGameState();
|
||||
if (!IsValid(GameState))
|
||||
return nullptr;
|
||||
|
||||
const auto* StatePtr = GameState->PlayerArray.FindByPredicate([PlayerId](const APlayerState* State) { return State->GetPlayerId() == PlayerId; });
|
||||
if (StatePtr == nullptr)
|
||||
return nullptr;
|
||||
|
||||
const APlayerState* State = *StatePtr;
|
||||
if (!IsValid(State))
|
||||
return nullptr;
|
||||
|
||||
#if UE_VERSION_OLDER_THAN(5, 0, 0)
|
||||
const APawn* Pawn = State->GetPawn();
|
||||
if (!IsValid(Pawn))
|
||||
return nullptr;
|
||||
|
||||
return Cast<APlayerController>(Pawn->GetController());
|
||||
#else
|
||||
return State->GetPlayerController();
|
||||
#endif
|
||||
}
|
||||
|
||||
UObject* ActorUtils::GetInterfaceImplementer(AActor* Actor, TSubclassOf<UInterface> InterfaceClass)
|
||||
{
|
||||
if (!IsValid(Actor) || InterfaceClass == nullptr)
|
||||
return nullptr;
|
||||
|
||||
UClass* ActorClass = Actor->GetClass();
|
||||
if (ActorClass && ActorClass->ImplementsInterface(InterfaceClass))
|
||||
return Actor;
|
||||
|
||||
const auto Components = Actor->GetComponentsByInterface(InterfaceClass);
|
||||
if (Components.Num() <= 0)
|
||||
return nullptr;
|
||||
|
||||
if (Components.Num() > 1)
|
||||
{
|
||||
DungeonLog_WarningSilent("Multiple components have a %s interface. Only one used, remove the unnecessary ones to prevent any confusion!", *GetNameSafe(InterfaceClass));
|
||||
}
|
||||
|
||||
return Components[0];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2021 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "QueueOrStack.h"
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ReadOnlyRoom.h"
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ReplicableObject.h"
|
||||
#include "Engine/ActorChannel.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Utils/ReplicationUtils.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
const TCHAR* GetWithPredicate(const TCHAR* Str, bool bPredicate)
|
||||
{
|
||||
return (bPredicate) ? Str : TEXT("");
|
||||
}
|
||||
} //namespace
|
||||
|
||||
bool UReplicableObject::ReplicateSubobject(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
|
||||
{
|
||||
// Ensure that nested objects are replicated BEFORE!
|
||||
// thus any reference to them inside this object will be correct.
|
||||
bool bWroteSomething = ReplicateSubobjects(Channel, Bunch, RepFlags);
|
||||
bWroteSomething |= Channel->ReplicateSubobject(this, *Bunch, *RepFlags);
|
||||
return bWroteSomething;
|
||||
}
|
||||
|
||||
void UReplicableObject::RegisterAsReplicable(bool bRegister, FRegisterSubObjectParams Params)
|
||||
{
|
||||
#if UE_WITH_SUBOBJECT_LIST
|
||||
AActor* Owner = GetTypedOuter<AActor>();
|
||||
if (!IsValid(Owner))
|
||||
{
|
||||
ensureMsgf(false, TEXT("Trying to %sregister %s as replicable subobject but actor owner is invalid."), ::GetWithPredicate(TEXT("un"), !bRegister), *GetNameSafe(this));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Owner->HasAuthority())
|
||||
return;
|
||||
|
||||
// Ignores if owner does not use registered subobject list
|
||||
if (!Owner->IsUsingRegisteredSubObjectList())
|
||||
return;
|
||||
|
||||
// Ignore if the object is already registered/unregistered
|
||||
if (Owner->IsReplicatedSubObjectRegistered(this) == bRegister)
|
||||
return;
|
||||
|
||||
DungeonLog_InfoSilent("%s Replicable Subobject: %s", (bRegister) ? TEXT("Register") : TEXT("Unregister"), *GetNameSafe(this));
|
||||
|
||||
if (bRegister)
|
||||
Owner->AddReplicatedSubObject(this, Params.NetCondition);
|
||||
else
|
||||
{
|
||||
switch (Params.UnregisterType)
|
||||
{
|
||||
case EUnregisterSubObjectType::Unregister:
|
||||
Owner->RemoveReplicatedSubObject(this);
|
||||
break;
|
||||
#if UE_VERSION_NEWER_THAN(5, 2, 0)
|
||||
case EUnregisterSubObjectType::Destroy:
|
||||
Owner->DestroyReplicatedSubObjectOnRemotePeers(this);
|
||||
break;
|
||||
case EUnregisterSubObjectType::TearOff:
|
||||
Owner->TearOffReplicatedSubObjectOnRemotePeers(this);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
checkf(false, TEXT("Unimplemented case."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RegisterReplicableSubobjects(bRegister);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UE_WITH_IRIS
|
||||
void UReplicableObject::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags)
|
||||
{
|
||||
// Build descriptors and allocate PropertyReplicationFragments for this object
|
||||
UE::Net::FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags);
|
||||
}
|
||||
#endif // UE_WITH_IRIS
|
||||
|
||||
bool UReplicableObject::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* UReplicableObject::GetWorld() const
|
||||
{
|
||||
UObject* Outer = GetOuter();
|
||||
if (!Outer)
|
||||
return nullptr;
|
||||
return Outer->GetWorld();
|
||||
}
|
||||
|
||||
bool UReplicableObject::HasAuthority() const
|
||||
{
|
||||
AActor* Owner = GetTypedOuter<AActor>();
|
||||
if (!Owner)
|
||||
return false;
|
||||
return Owner->HasAuthority();
|
||||
}
|
||||
|
||||
FString UReplicableObject::GetAuthorityName() const
|
||||
{
|
||||
return HasAuthority() ? TEXT("Server") : TEXT("Client");
|
||||
}
|
||||
|
||||
void UReplicableObject::WakeUpOwnerActor()
|
||||
{
|
||||
AActor* Owner = GetTypedOuter<AActor>();
|
||||
if (!IsValid(Owner))
|
||||
return;
|
||||
|
||||
if (!Owner->HasAuthority())
|
||||
return;
|
||||
|
||||
Owner->FlushNetDormancy();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,451 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomConnection.h"
|
||||
#include "Room.h"
|
||||
#include "RoomData.h"
|
||||
#include "Door.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Utils/ReplicationUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Interfaces/RoomContainer.h"
|
||||
#include "Interfaces/DoorInterface.h"
|
||||
#include "Utils/DungeonSaveUtils.h"
|
||||
#include "DungeonGeneratorBase.h"
|
||||
#include "ProceduralDungeonCustomVersion.h"
|
||||
#include "Components/DoorComponent.h"
|
||||
|
||||
void URoomConnection::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
FDoRepLifetimeParams Params;
|
||||
Params.bIsPushBased = true;
|
||||
// InitialOnly is not called on newly created subobjects after the InitialCond of actor owner has already been called!!!
|
||||
//Params.Condition = COND_InitialOnly;
|
||||
DOREPLIFETIME_WITH_PARAMS(URoomConnection, ID, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomA, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomADoorId, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomB, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomBDoorId, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS(URoomConnection, DoorInstance, Params);
|
||||
DOREPLIFETIME_WITH_PARAMS(URoomConnection, DoorState, Params);
|
||||
}
|
||||
|
||||
bool URoomConnection::SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading)
|
||||
{
|
||||
check(!SaveData.IsValid());
|
||||
SaveData = MakeUnique<FSaveData>();
|
||||
SaveData->Version = Record.GetUnderlyingArchive().CustomVer(FProceduralDungeonCustomVersion::GUID);
|
||||
|
||||
if (!bIsLoading)
|
||||
{
|
||||
TSoftObjectPtr<URoom> RoomAWeak(RoomA.Get());
|
||||
TSoftObjectPtr<URoom> RoomBWeak(RoomB.Get());
|
||||
SaveData->RoomAID = RoomA.IsValid() ? RoomA->GetRoomID() : -1;
|
||||
SaveData->RoomBID = RoomB.IsValid() ? RoomB->GetRoomID() : -1;
|
||||
|
||||
// Serializing the door instance's properties only during the save here.
|
||||
// The properties will be loaded back when the door is spawned.
|
||||
if (DoorInstance.IsValid())
|
||||
SerializeUObject(SaveData->DoorSavedData, DoorInstance.Get(), false);
|
||||
}
|
||||
|
||||
Record.EnterField(AR_FIELD_NAME("RoomA")) << SaveData->RoomAID;
|
||||
Record.EnterField(AR_FIELD_NAME("RoomB")) << SaveData->RoomBID;
|
||||
|
||||
Record.EnterField(AR_FIELD_NAME("DoorClass")) << DoorClass;
|
||||
Record.EnterField(AR_FIELD_NAME("DoorProperties")) << SaveData->DoorSavedData;
|
||||
|
||||
if (bIsLoading)
|
||||
{
|
||||
DungeonLog_Debug("[%s] Loaded DoorClass: %s", *GetNameSafe(this), *GetNameSafe(DoorClass));
|
||||
}
|
||||
|
||||
if (!bIsLoading)
|
||||
{
|
||||
// No need to keep the saved data after saving.
|
||||
SaveData.Reset();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool URoomConnection::FixupReferences(UObject* Context)
|
||||
{
|
||||
check(SaveData.IsValid());
|
||||
|
||||
auto* RoomContainer = Cast<IRoomContainer>(Context);
|
||||
if (RoomContainer == nullptr)
|
||||
{
|
||||
DungeonLog_Error("[%s] Failed to fixup RoomConnection references: Context is not a RoomContainer.", *GetNameSafe(this));
|
||||
return false;
|
||||
}
|
||||
|
||||
RoomA = RoomContainer->GetRoomByIndex(SaveData->RoomAID);
|
||||
RoomB = RoomContainer->GetRoomByIndex(SaveData->RoomBID);
|
||||
|
||||
DungeonLog_Debug("[%s] Fixed up RoomConnection references: RoomA=%s, RoomB=%s", *GetNameSafe(this), *GetNameSafe(RoomA.Get()), *GetNameSafe(RoomB.Get()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void URoomConnection::PreSaveDungeon_Implementation()
|
||||
{
|
||||
if (DoorInstance.IsValid() && DoorInstance->Implements<UDungeonSaveInterface>())
|
||||
{
|
||||
IDungeonSaveInterface::Execute_PreSaveDungeon(DoorInstance.Get());
|
||||
}
|
||||
}
|
||||
|
||||
void URoomConnection::PostLoadDungeon_Implementation()
|
||||
{
|
||||
SaveData.Reset();
|
||||
|
||||
if (DoorInstance.IsValid() && DoorInstance->Implements<UDungeonSaveInterface>())
|
||||
{
|
||||
IDungeonSaveInterface::Execute_PostLoadDungeon(DoorInstance.Get());
|
||||
}
|
||||
}
|
||||
|
||||
int32 URoomConnection::GetID() const
|
||||
{
|
||||
return ID;
|
||||
}
|
||||
|
||||
const TWeakObjectPtr<URoom> URoomConnection::GetRoomA() const
|
||||
{
|
||||
return RoomA;
|
||||
}
|
||||
|
||||
const TWeakObjectPtr<URoom> URoomConnection::GetRoomB() const
|
||||
{
|
||||
return RoomB;
|
||||
}
|
||||
|
||||
const URoom* URoomConnection::GetRoomA_BP() const
|
||||
{
|
||||
return RoomA.Get();
|
||||
}
|
||||
|
||||
const URoom* URoomConnection::GetRoomB_BP() const
|
||||
{
|
||||
return RoomB.Get();
|
||||
}
|
||||
|
||||
int32 URoomConnection::GetRoomADoorId() const
|
||||
{
|
||||
return RoomADoorId;
|
||||
}
|
||||
|
||||
int32 URoomConnection::GetRoomBDoorId() const
|
||||
{
|
||||
return RoomBDoorId;
|
||||
}
|
||||
|
||||
TWeakObjectPtr<URoom> URoomConnection::GetOtherRoom(const URoom* FromRoom) const
|
||||
{
|
||||
check(FromRoom == RoomA || FromRoom == RoomB);
|
||||
return (FromRoom == RoomA) ? RoomB : RoomA;
|
||||
}
|
||||
|
||||
int32 URoomConnection::GetOtherDoorId(const URoom* FromRoom) const
|
||||
{
|
||||
check(FromRoom == RoomA || FromRoom == RoomB);
|
||||
return (FromRoom == RoomA) ? RoomBDoorId : RoomADoorId;
|
||||
}
|
||||
|
||||
bool URoomConnection::IsDoorInstanced() const
|
||||
{
|
||||
return DoorInstance.IsValid();
|
||||
}
|
||||
|
||||
AActor* URoomConnection::GetDoorInstance() const
|
||||
{
|
||||
return DoorInstance.Get();
|
||||
}
|
||||
|
||||
FVector URoomConnection::GetDoorLocation(bool bIgnoreGeneratorTransform) const
|
||||
{
|
||||
FDoorDef DoorDef;
|
||||
const AActor* Generator = nullptr;
|
||||
const URoomData* Data = nullptr;
|
||||
if (RoomA.IsValid())
|
||||
{
|
||||
DoorDef = RoomA->GetDoorDef(RoomADoorId);
|
||||
Generator = RoomA->Generator();
|
||||
Data = RoomA->GetRoomData();
|
||||
}
|
||||
else if (RoomB.IsValid())
|
||||
{
|
||||
DoorDef = RoomB->GetDoorDef(RoomBDoorId);
|
||||
Generator = RoomB->Generator();
|
||||
Data = RoomB->GetRoomData();
|
||||
}
|
||||
else
|
||||
{
|
||||
return FVector();
|
||||
}
|
||||
|
||||
check(IsValid(Data));
|
||||
FVector Location = FDoorDef::GetRealDoorPosition(DoorDef, Data->GetRoomUnit());
|
||||
if (!bIgnoreGeneratorTransform && IsValid(Generator))
|
||||
Location = Generator->GetTransform().TransformPositionNoScale(Location);
|
||||
|
||||
return Location;
|
||||
}
|
||||
|
||||
FRotator URoomConnection::GetDoorRotation(bool bIgnoreGeneratorTransform) const
|
||||
{
|
||||
FDoorDef DoorDef;
|
||||
const AActor* Generator = nullptr;
|
||||
bool bFinalFlipped = bFlipped;
|
||||
if (RoomA.IsValid())
|
||||
{
|
||||
DoorDef = RoomA->GetDoorDef(RoomADoorId);
|
||||
Generator = RoomA->Generator();
|
||||
}
|
||||
else if (RoomB.IsValid())
|
||||
{
|
||||
DoorDef = RoomB->GetDoorDef(RoomBDoorId);
|
||||
Generator = RoomB->Generator();
|
||||
bFinalFlipped = !bFlipped;
|
||||
}
|
||||
else
|
||||
{
|
||||
return FRotator::ZeroRotator;
|
||||
}
|
||||
|
||||
FQuat Rotation = FDoorDef::GetRealDoorRotation(DoorDef, bFinalFlipped);
|
||||
if (!bIgnoreGeneratorTransform && IsValid(Generator))
|
||||
Rotation = Generator->GetTransform().InverseTransformRotation(Rotation);
|
||||
|
||||
return Rotation.Rotator();
|
||||
}
|
||||
|
||||
bool URoomConnection::IsDoorOpen() const
|
||||
{
|
||||
return DoorState.bIsOpen;
|
||||
}
|
||||
|
||||
bool URoomConnection::IsDoorLocked() const
|
||||
{
|
||||
return DoorState.bIsLocked;
|
||||
}
|
||||
|
||||
void URoomConnection::SetDoorOpen(bool bOpen)
|
||||
{
|
||||
DoorState.bIsOpen = bOpen;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(URoomConnection, DoorState, this);
|
||||
}
|
||||
|
||||
void URoomConnection::SetDoorLocked(bool bLocked)
|
||||
{
|
||||
DoorState.bIsLocked = bLocked;
|
||||
MARK_PROPERTY_DIRTY_FROM_NAME(URoomConnection, DoorState, this);
|
||||
}
|
||||
|
||||
void URoomConnection::SetDoorClass(TSubclassOf<AActor> InDoorClass, bool bInFlipped)
|
||||
{
|
||||
DoorClass = InDoorClass;
|
||||
bFlipped = bInFlipped;
|
||||
}
|
||||
|
||||
AActor* URoomConnection::InstantiateDoor(UWorld* World, AActor* Owner, bool bUseOwnerTransform)
|
||||
{
|
||||
if (!IsValid(World))
|
||||
{
|
||||
DungeonLog_Error("Can't spanw door: Invalid World.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (DoorInstance.IsValid())
|
||||
{
|
||||
DungeonLog_WarningSilent("Door already instanced.");
|
||||
return DoorInstance.Get();
|
||||
}
|
||||
|
||||
if (nullptr == DoorClass)
|
||||
return nullptr;
|
||||
|
||||
// Get next room
|
||||
const URoom* Room = RoomA.Get();
|
||||
int32 DoorId = RoomADoorId;
|
||||
bool bFinalFlipped = bFlipped;
|
||||
if (!IsValid(Room))
|
||||
{
|
||||
Room = RoomB.Get();
|
||||
DoorId = RoomBDoorId;
|
||||
bFinalFlipped = !bFinalFlipped; // Flipped is inverted when using RoomB instead of RoomA
|
||||
}
|
||||
|
||||
FDoorDef DoorDef = Room->GetDoorDef(DoorId);
|
||||
FVector InstanceDoorPos = FDoorDef::GetRealDoorPosition(DoorDef, Room->GetRoomData()->GetRoomUnit());
|
||||
FQuat InstanceDoorRot = FDoorDef::GetRealDoorRotation(DoorDef, bFinalFlipped);
|
||||
|
||||
if (bUseOwnerTransform && IsValid(Owner))
|
||||
{
|
||||
InstanceDoorPos = Owner->GetActorTransform().TransformPositionNoScale(InstanceDoorPos);
|
||||
InstanceDoorRot = Owner->GetActorTransform().TransformRotation(InstanceDoorRot);
|
||||
}
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.Owner = Owner;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
AActor* Door = GetWorld()->SpawnActor<AActor>(DoorClass, InstanceDoorPos, InstanceDoorRot.Rotator(), SpawnParams);
|
||||
|
||||
if (!IsValid(Door))
|
||||
{
|
||||
DungeonLog_Error("Failed to spawn Door, make sure you set door actor to always spawning.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UObject* Implementer = ActorUtils::GetInterfaceImplementer<UDoorInterface>(Door);
|
||||
if (IsValid(Implementer))
|
||||
IDoorInterface::Execute_SetRoomConnection(Implementer, this);
|
||||
|
||||
DoorInstance = Door;
|
||||
|
||||
if (SaveData.IsValid())
|
||||
{
|
||||
// Load door data back if we have some saved data.
|
||||
SerializeUObject(SaveData->DoorSavedData, Door, true);
|
||||
DungeonLog_InfoSilent("Loaded saved data for door '%s'", *GetNameSafe(Door));
|
||||
|
||||
if (SaveData->Version < FProceduralDungeonCustomVersion::DoorLogicRefactored)
|
||||
{
|
||||
if (ADoor* LegacyDoorActor = Cast<ADoor>(Door))
|
||||
{
|
||||
DoorState.bIsOpen = LegacyDoorActor->GetLegacyShouldBeOpen();
|
||||
DoorState.bIsLocked = LegacyDoorActor->GetLegacyShouldBeLocked();
|
||||
|
||||
if (UDoorComponent* Component = LegacyDoorActor->FindComponentByClass<UDoorComponent>())
|
||||
{
|
||||
Component->SetAlwaysVisible(LegacyDoorActor->GetLegacyAlwaysVisible());
|
||||
Component->SetAlwaysUnlocked(LegacyDoorActor->GetLegacyAlwaysUnlocked());
|
||||
Component->SetDoorType(LegacyDoorActor->GetLegacyDoorType());
|
||||
}
|
||||
else
|
||||
{
|
||||
DungeonLog_WarningSilent("Legacy door actor '%s' does not have a DoorComponent, can't migrate its AlwaysVisible and AlwaysUnlocked properties.", *GetNameSafe(LegacyDoorActor));
|
||||
}
|
||||
|
||||
DungeonLog_InfoSilent("Migrated from old door actor '%s': Open:%d | Locked:%d", *GetNameSafe(LegacyDoorActor), DoorState.bIsOpen, DoorState.bIsLocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Door;
|
||||
}
|
||||
|
||||
void URoomConnection::DestroyDoor()
|
||||
{
|
||||
if (!DoorInstance.IsValid())
|
||||
return;
|
||||
|
||||
DoorInstance->Destroy();
|
||||
DoorInstance.Reset();
|
||||
}
|
||||
|
||||
void URoomConnection::OnRep_ID()
|
||||
{
|
||||
DungeonLog_Debug("[%s] RoomConnection '%s' ID replicated: %d", *GetAuthorityName(), *GetNameSafe(this), ID);
|
||||
}
|
||||
|
||||
void URoomConnection::OnRep_RoomA()
|
||||
{
|
||||
DungeonLog_Debug("[%s] RoomConnection '%s' RoomA replicated: %s", *GetAuthorityName(), *GetNameSafe(this), *GetNameSafe(RoomA.Get()));
|
||||
}
|
||||
|
||||
void URoomConnection::OnRep_RoomB()
|
||||
{
|
||||
DungeonLog_Debug("[%s] RoomConnection '%s' RoomB replicated: %s", *GetAuthorityName(), *GetNameSafe(this), *GetNameSafe(RoomB.Get()));
|
||||
}
|
||||
|
||||
void URoomConnection::OnRep_DoorState()
|
||||
{
|
||||
DungeonLog_Debug("[%s] RoomConnection '%s' DoorState replicated: Open:%d | Locked:%d", *GetAuthorityName(), *GetNameSafe(this), DoorState.bIsOpen, DoorState.bIsLocked);
|
||||
}
|
||||
|
||||
URoom* URoomConnection::GetOtherRoom(const URoomConnection* Conn, const URoom* FromRoom)
|
||||
{
|
||||
return (Conn != nullptr) ? Conn->GetOtherRoom(FromRoom).Get() : nullptr;
|
||||
}
|
||||
|
||||
int32 URoomConnection::GetOtherDoorId(const URoomConnection* Conn, const URoom* FromRoom)
|
||||
{
|
||||
return (Conn != nullptr) ? Conn->GetOtherDoorId(FromRoom) : -1;
|
||||
}
|
||||
|
||||
AActor* URoomConnection::GetDoorInstance(const URoomConnection* Conn)
|
||||
{
|
||||
return (Conn != nullptr) ? Conn->DoorInstance.Get() : nullptr;
|
||||
}
|
||||
|
||||
UDoorType* URoomConnection::GetDoorType(const URoomConnection* Conn)
|
||||
{
|
||||
if (!IsValid(Conn))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
URoom* Room = Conn->RoomA.Get();
|
||||
int32 DoorId = Conn->RoomADoorId;
|
||||
if (!IsValid(Room))
|
||||
{
|
||||
Room = Conn->RoomB.Get();
|
||||
DoorId = Conn->RoomBDoorId;
|
||||
}
|
||||
|
||||
if (!IsValid(Room))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Room->GetRoomData()->Doors[DoorId].Type;
|
||||
}
|
||||
|
||||
void URoomConnection::GetBothDoorTypes(const URoomConnection* Conn, UDoorType*& DoorTypeA, UDoorType*& DoorTypeB)
|
||||
{
|
||||
if (!IsValid(Conn))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DoorTypeA = Conn->RoomA.IsExplicitlyNull() ? nullptr : Conn->RoomA->GetRoomData()->Doors[Conn->RoomADoorId].Type;
|
||||
DoorTypeB = Conn->RoomB.IsExplicitlyNull() ? nullptr : Conn->RoomB->GetRoomData()->Doors[Conn->RoomBDoorId].Type;
|
||||
}
|
||||
|
||||
URoomConnection* URoomConnection::CreateConnection(URoom* RoomA, int32 DoorA, URoom* RoomB, int32 DoorB, UObject* Outer, int32 IdInOuter)
|
||||
{
|
||||
// At least one room and its door index must be valid.
|
||||
const bool bIsAValid = IsValid(RoomA) && RoomA->IsDoorIndexValid(DoorA);
|
||||
const bool bIsBValid = IsValid(RoomB) && RoomB->IsDoorIndexValid(DoorB);
|
||||
check(bIsAValid || bIsBValid);
|
||||
|
||||
URoomConnection* NewConnection = NewObject<URoomConnection>(Outer);
|
||||
check(NewConnection != nullptr);
|
||||
|
||||
NewConnection->ID = IdInOuter;
|
||||
NewConnection->RoomA = RoomA;
|
||||
NewConnection->RoomADoorId = DoorA;
|
||||
NewConnection->RoomB = RoomB;
|
||||
NewConnection->RoomBDoorId = DoorB;
|
||||
|
||||
if (bIsAValid)
|
||||
{
|
||||
RoomA->SetConnection(DoorA, NewConnection);
|
||||
}
|
||||
|
||||
if (bIsBValid)
|
||||
{
|
||||
RoomB->SetConnection(DoorB, NewConnection);
|
||||
}
|
||||
|
||||
return NewConnection;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomConstraints/RoomConstraint.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
|
||||
bool URoomConstraint::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
|
||||
{
|
||||
DungeonLog_WarningSilent("Constraint %s does not implements the URoomConstraint::Check function!", *GetNameSafe(this));
|
||||
return true;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright Benoit Pelletier 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomConstraints/RoomConstraint_CountLimit.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "RoomData.h"
|
||||
#include "DungeonGraph.h"
|
||||
|
||||
bool URoomConstraint_CountLimit::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
|
||||
{
|
||||
if (!IsValid(RoomData))
|
||||
{
|
||||
DungeonLog_Error("Invalid RoomData passed to %s", *GetNameSafe(this));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsValid(Dungeon))
|
||||
{
|
||||
DungeonLog_Error("Invalid Dungeon passed to %s", *GetNameSafe(this));
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 ActualCount = Dungeon->CountRoomData(RoomData);
|
||||
return ActualCount < MaxCount;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomConstraints/RoomConstraint_Direction.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Room.h"
|
||||
|
||||
bool URoomConstraint_Direction::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
|
||||
{
|
||||
return AllowedDirections.Contains(Direction);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomConstraints/RoomConstraint_Location.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "RoomData.h"
|
||||
|
||||
bool URoomConstraint_Location::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
|
||||
{
|
||||
if (!IsValid(RoomData))
|
||||
{
|
||||
DungeonLog_Error("Invalid RoomData passed to %s", *GetNameSafe(this));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FBoxMinAndMax BoundingLimits = Limits.GetBox();
|
||||
FBoxMinAndMax RoomBounds = RoomData->GetIntBounds();
|
||||
RoomBounds.Rotate(Direction);
|
||||
return BoundingLimits.IsInside(RoomBounds + Location);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomCustomData.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "Utils/DungeonSaveUtils.h"
|
||||
|
||||
UActorComponent* CreateComponentOnInstance(AActor* ActorInstance, TSubclassOf<UActorComponent> ComponentClass, USceneComponent* OptionalParentForSceneComponent = nullptr)
|
||||
{
|
||||
if (!ActorInstance || !ComponentClass)
|
||||
return nullptr;
|
||||
|
||||
// Don't create component if on a template actor (CDO or Archetype)
|
||||
if (ActorInstance->IsTemplate())
|
||||
return nullptr;
|
||||
|
||||
// For multiplayer games, create component only on server if component is replicating
|
||||
const UActorComponent* ComponentCDO = ComponentClass->GetDefaultObject<UActorComponent>();
|
||||
if (ComponentCDO->GetIsReplicated() && !ActorInstance->HasAuthority())
|
||||
return nullptr;
|
||||
|
||||
UActorComponent* NewComp = NewObject<UActorComponent>(ActorInstance, ComponentClass);
|
||||
|
||||
// Handles USceneComponent attachment
|
||||
if (USceneComponent* NewSceneComp = Cast<USceneComponent>(NewComp))
|
||||
{
|
||||
USceneComponent* ParentComponent = OptionalParentForSceneComponent;
|
||||
if (!ParentComponent)
|
||||
ParentComponent = ActorInstance->GetRootComponent();
|
||||
|
||||
if (ParentComponent)
|
||||
{
|
||||
// Parent component should always be owned by the passed in actor instance!
|
||||
check(ParentComponent->GetOwner() != ActorInstance);
|
||||
NewSceneComp->SetupAttachment(ParentComponent);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set component directly as root if no root component on the actor
|
||||
ActorInstance->SetRootComponent(NewSceneComp);
|
||||
}
|
||||
}
|
||||
|
||||
NewComp->RegisterComponent();
|
||||
return NewComp;
|
||||
}
|
||||
|
||||
void URoomCustomData::CreateLevelComponent(ARoomLevel* LevelActor)
|
||||
{
|
||||
if (!LevelComponent)
|
||||
return;
|
||||
|
||||
LevelComponentInstance = CreateComponentOnInstance(LevelActor, LevelComponent);
|
||||
if (!LevelComponentInstance.IsValid())
|
||||
{
|
||||
DungeonLog_Error("Failed to create component '%s' on room level '%s'.", *GetNameSafe(LevelComponent), *GetNameSafe(LevelActor));
|
||||
}
|
||||
}
|
||||
|
||||
bool URoomCustomData::SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading)
|
||||
{
|
||||
// Nothing more to serialize if no component
|
||||
if (nullptr == LevelComponent)
|
||||
return true;
|
||||
|
||||
SavedData = MakeUnique<FSaveData>();
|
||||
|
||||
if (!bIsLoading)
|
||||
{
|
||||
// Serialize component data
|
||||
if (LevelComponentInstance.IsValid())
|
||||
{
|
||||
SerializeUObject(SavedData->ComponentData, LevelComponentInstance.Get(), false);
|
||||
}
|
||||
}
|
||||
|
||||
Record.EnterField(AR_FIELD_NAME("ComponentData")) << SavedData->ComponentData;
|
||||
|
||||
if (!bIsLoading)
|
||||
{
|
||||
// No need to keep the data after saving
|
||||
SavedData.Reset();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void URoomCustomData::PreSaveDungeon_Implementation()
|
||||
{
|
||||
if (!LevelComponentInstance.IsValid())
|
||||
return;
|
||||
|
||||
if (LevelComponentInstance->Implements<UDungeonSaveInterface>())
|
||||
{
|
||||
IDungeonSaveInterface::Execute_PreSaveDungeon(LevelComponentInstance.Get());
|
||||
}
|
||||
}
|
||||
|
||||
void URoomCustomData::PostLoadDungeon_Implementation()
|
||||
{
|
||||
if (!SavedData.IsValid())
|
||||
return;
|
||||
|
||||
// Deserialize component data
|
||||
if (LevelComponentInstance.IsValid())
|
||||
{
|
||||
SerializeUObject(SavedData->ComponentData, LevelComponentInstance.Get(), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
DungeonLog_Error("Failed to deserialize component data for '%s' in room custom data '%s'", *GetNameSafe(LevelComponent), *GetNameSafe(this));
|
||||
}
|
||||
|
||||
SavedData.Reset();
|
||||
|
||||
if (LevelComponentInstance.IsValid() && LevelComponentInstance->Implements<UDungeonSaveInterface>())
|
||||
{
|
||||
IDungeonSaveInterface::Execute_PostLoadDungeon(LevelComponentInstance.Get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomData.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "RoomCustomData.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "ProceduralDungeonCustomVersion.h"
|
||||
#include "DoorType.h"
|
||||
#include "DungeonSettings.h"
|
||||
#include "RoomConstraints/RoomConstraint.h"
|
||||
#include "Serialization/CustomVersion.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
#if !USE_LEGACY_DATA_VALIDATION
|
||||
#include "Misc/DataValidation.h"
|
||||
#endif
|
||||
|
||||
URoomData::URoomData()
|
||||
: Super()
|
||||
{
|
||||
BoundingBoxes.Add({FIntVector(0), FIntVector(1)});
|
||||
}
|
||||
|
||||
void URoomData::Serialize(FArchive& Ar)
|
||||
{
|
||||
Super::Serialize(Ar);
|
||||
|
||||
Ar.UsingCustomVersion(FProceduralDungeonCustomVersion::GUID);
|
||||
|
||||
// If loading an old version, we need to handle the migration
|
||||
if (Ar.IsLoading())
|
||||
{
|
||||
const int32 DungeonVersion = Ar.CustomVer(FProceduralDungeonCustomVersion::GUID);
|
||||
|
||||
if (DungeonVersion < FProceduralDungeonCustomVersion::RoomDataBoundingBoxesMigration)
|
||||
{
|
||||
DungeonLog_Warning("Migrating RoomData '%s' from legacy FirstPoint/SecondPoint to BoundingBoxes.", *GetName());
|
||||
|
||||
if (BoundingBoxes.Num() == 0)
|
||||
BoundingBoxes.AddDefaulted();
|
||||
|
||||
BoundingBoxes[0].SetMinAndMax(FirstPoint, SecondPoint);
|
||||
|
||||
// Clear the legacy data after migration
|
||||
FirstPoint = FIntVector(0);
|
||||
SecondPoint = FIntVector(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FDoorDef& URoomData::GetDoorDef(int32 DoorIndex) const
|
||||
{
|
||||
if (DoorIndex >= 0 && DoorIndex < Doors.Num())
|
||||
return Doors[DoorIndex];
|
||||
return FDoorDef::Invalid;
|
||||
}
|
||||
|
||||
bool URoomData::HasCompatibleDoor(const FDoorDef& DoorData) const
|
||||
{
|
||||
for (int i = 0; i < Doors.Num(); ++i)
|
||||
{
|
||||
if (FDoorDef::AreCompatible(Doors[i], DoorData))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void URoomData::GetCompatibleDoors(const FDoorDef& DoorData, TArray<int>& CompatibleDoors) const
|
||||
{
|
||||
CompatibleDoors.Empty();
|
||||
for (int i = 0; i < Doors.Num(); ++i)
|
||||
{
|
||||
if (FDoorDef::AreCompatible(Doors[i], DoorData))
|
||||
CompatibleDoors.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
bool URoomData::HasDoorOfType(UDoorType* DoorType) const
|
||||
{
|
||||
for (const auto& Door : Doors)
|
||||
{
|
||||
if (Door.Type == DoorType)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool URoomData::HasAnyDoorOfType(const TArray<UDoorType*>& DoorTypes) const
|
||||
{
|
||||
for (const auto& Door : Doors)
|
||||
{
|
||||
if (DoorTypes.Contains(Door.Type))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool URoomData::HasAllDoorOfType(const TArray<UDoorType*>& DoorTypes) const
|
||||
{
|
||||
TSet<UDoorType*> AllDoorTypes(DoorTypes);
|
||||
for (const auto& Door : Doors)
|
||||
{
|
||||
AllDoorTypes.Remove(Door.Type);
|
||||
}
|
||||
return AllDoorTypes.Num() <= 0;
|
||||
}
|
||||
|
||||
bool URoomData::HasCustomData(TSubclassOf<URoomCustomData> CustomDataClass) const
|
||||
{
|
||||
return CustomData.Contains(CustomDataClass);
|
||||
}
|
||||
|
||||
bool URoomData::HasAnyCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomDataList) const
|
||||
{
|
||||
for (const auto& CustomDataClass : CustomDataList)
|
||||
{
|
||||
if (HasCustomData(CustomDataClass))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool URoomData::HasAllCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomDataList) const
|
||||
{
|
||||
for (const auto& CustomDataClass : CustomDataList)
|
||||
{
|
||||
if (!HasCustomData(CustomDataClass))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void URoomData::InitializeRoom_Implementation(URoom* Room, UDungeonGraph* Dungeon) const
|
||||
{
|
||||
}
|
||||
|
||||
void URoomData::CleanupRoom_Implementation(URoom* Room, UDungeonGraph* Dungeon) const
|
||||
{
|
||||
}
|
||||
|
||||
FVector URoomData::GetRoomUnit() const
|
||||
{
|
||||
return UDungeonSettings::GetRoomUnit(GetSettings());
|
||||
}
|
||||
|
||||
bool URoomData::DoesPassAllConstraints(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction)
|
||||
{
|
||||
if (!IsValid(RoomData))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const URoomConstraint* Constraint : RoomData->Constraints)
|
||||
{
|
||||
if (!IsValid(Constraint))
|
||||
{
|
||||
DungeonLog_WarningSilent("Invalid constraint detected in RoomData %s", *GetNameSafe(RoomData));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Constraint->Check(Dungeon, RoomData, Location, Direction))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent URoomData::GetBounds(FTransform Transform) const
|
||||
{
|
||||
return Dungeon::ToWorld(GetIntBounds(), GetRoomUnit(), Transform);
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent URoomData::GetSubBounds(int32 Index, FTransform Transform) const
|
||||
{
|
||||
check(Index >= 0 && Index < BoundingBoxes.Num());
|
||||
const FBoxMinAndMax& Box = BoundingBoxes[Index];
|
||||
return Dungeon::ToWorld(Box, GetRoomUnit(), Transform);
|
||||
}
|
||||
|
||||
FIntVector URoomData::GetSize() const
|
||||
{
|
||||
return GetIntBounds().GetSize();
|
||||
}
|
||||
|
||||
int URoomData::GetVolume() const
|
||||
{
|
||||
const FVoxelBounds Bounds = GetVoxelBounds();
|
||||
return Bounds.GetCellCount();
|
||||
}
|
||||
|
||||
FBoxMinAndMax URoomData::GetIntBounds() const
|
||||
{
|
||||
return GetVoxelBounds().GetBounds();
|
||||
}
|
||||
|
||||
FVoxelBounds URoomData::GetVoxelBounds() const
|
||||
{
|
||||
if (CachedVoxelBounds.IsValid())
|
||||
return CachedVoxelBounds;
|
||||
|
||||
// For now, just convert the IntBounds into a VoxelBounds.
|
||||
// When the VoxelBounds editor will be implemented, we will just have to return the serialized VoxelBounds.
|
||||
for (const FBoxMinAndMax& Box : BoundingBoxes)
|
||||
{
|
||||
CachedVoxelBounds.AddBox(Box);
|
||||
}
|
||||
CachedVoxelBounds.ResetToWalls();
|
||||
|
||||
// Add the doors
|
||||
for (int i = 0; i < Doors.Num(); ++i)
|
||||
{
|
||||
const FDoorDef& Door = Doors[i];
|
||||
const FIntVector DoorPos = Door.Position;
|
||||
const EDoorDirection DoorDir = Door.Direction;
|
||||
CachedVoxelBounds.SetCellConnection(DoorPos, FVoxelBounds::EDirection(DoorDir), FVoxelBoundsConnection(Door.Type));
|
||||
}
|
||||
|
||||
return CachedVoxelBounds;
|
||||
}
|
||||
|
||||
bool URoomData::IsRoomInBounds(const FBoxMinAndMax& Bounds, int DoorIndex, const FDoorDef& DoorDungeonPos) const
|
||||
{
|
||||
const FIntVector BoundSize = Bounds.GetSize();
|
||||
if (BoundSize.X == 0 || BoundSize.Y == 0 || BoundSize.Z == 0)
|
||||
return false;
|
||||
|
||||
if (DoorIndex < 0 || DoorIndex >= Doors.Num())
|
||||
return false;
|
||||
|
||||
const FDoorDef& Door = Doors[DoorIndex];
|
||||
FBoxMinAndMax RoomBounds = GetIntBounds();
|
||||
RoomBounds -= Door.Position;
|
||||
RoomBounds.Rotate(DoorDungeonPos.Direction - Door.Direction);
|
||||
RoomBounds += DoorDungeonPos.Position;
|
||||
return Bounds.IsInside(RoomBounds);
|
||||
}
|
||||
|
||||
#if !(UE_BUILD_SHIPPING) || WITH_EDITOR
|
||||
|
||||
bool URoomData::IsDoorValid(int DoorIndex) const
|
||||
{
|
||||
check(DoorIndex >= 0 && DoorIndex < Doors.Num());
|
||||
|
||||
bool bFacingNoBox = true;
|
||||
bool bAtLeastInABox = false;
|
||||
const FDoorDef& DoorDef = Doors[DoorIndex];
|
||||
for (const auto& Box : BoundingBoxes)
|
||||
{
|
||||
bAtLeastInABox |= Box.IsInside(DoorDef.Position);
|
||||
|
||||
const FIntVector FacingCell = DoorDef.Position + ToIntVector(DoorDef.Direction);
|
||||
bFacingNoBox &= !Box.IsInside(FacingCell);
|
||||
}
|
||||
|
||||
return bAtLeastInABox && bFacingNoBox;
|
||||
}
|
||||
|
||||
bool URoomData::IsDoorDuplicate(int DoorIndex) const
|
||||
{
|
||||
check(DoorIndex >= 0 && DoorIndex < Doors.Num());
|
||||
for (int i = 0; i < Doors.Num(); ++i)
|
||||
{
|
||||
if (DoorIndex != i && Doors[i] == Doors[DoorIndex])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void URoomData::DrawDebug(const UWorld* World, const FTransform& Transform, const FColor& Color)
|
||||
{
|
||||
if (!IsValid(World))
|
||||
return;
|
||||
|
||||
for (const FBoxMinAndMax& BoundingBox : BoundingBoxes)
|
||||
{
|
||||
const FBoxCenterAndExtent Box = Dungeon::ToWorld(BoundingBox, GetRoomUnit(), Transform);
|
||||
DrawDebugBox(World, Box.Center, Box.Extent, FQuat::Identity, Color, false, -1.0f, SDPG_World, 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !(UE_BUILD_SHIPPING) || WITH_EDITOR
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#if USE_LEGACY_DATA_VALIDATION
|
||||
#define VALIDATION_LOG_ERROR(Msg) ValidationErrors.Add(Msg)
|
||||
EDataValidationResult URoomData::IsDataValid(TArray<FText>& ValidationErrors)
|
||||
#else
|
||||
#define VALIDATION_LOG_ERROR(Msg) Context.AddError(Msg)
|
||||
EDataValidationResult URoomData::IsDataValid(FDataValidationContext& Context) const
|
||||
#endif // USE_LEGACY_DATA_VALIDATION
|
||||
{
|
||||
#if USE_LEGACY_DATA_VALIDATION
|
||||
EDataValidationResult Result = Super::IsDataValid(ValidationErrors);
|
||||
#else
|
||||
EDataValidationResult Result = Super::IsDataValid(Context);
|
||||
#endif // USE_LEGACY_DATA_VALIDATION
|
||||
if (!IsAsset() || Result == EDataValidationResult::Invalid)
|
||||
return Result;
|
||||
|
||||
// Check the Level validity
|
||||
if (Level.IsNull())
|
||||
{
|
||||
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has no level set. You have to set up a room level."), *GetName())));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
|
||||
if (BoundingBoxes.Num() <= 0)
|
||||
{
|
||||
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" should have at least one bounding box."), *GetName())));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if all bounding boxes are valid
|
||||
for (const FBoxMinAndMax& Box : BoundingBoxes)
|
||||
{
|
||||
if (!Box.IsValid())
|
||||
{
|
||||
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has an invalid bounding box: %s."), *GetName(), *Box.ToString())));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Doors.Num() <= 0)
|
||||
{
|
||||
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" should have at least one door."), *GetName())));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < Doors.Num(); ++i)
|
||||
{
|
||||
// Check if all doors are valid
|
||||
if (!IsDoorValid(i))
|
||||
{
|
||||
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has invalid door: %s."), *GetName(), *Doors[i].ToString())));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
|
||||
// Check if there are no duplicated doors
|
||||
if (IsDoorDuplicate(i))
|
||||
{
|
||||
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has duplicated doors: %s."), *GetName(), *Doors[i].ToString())));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if CustomData Set does not have null value
|
||||
if (CustomData.Contains(nullptr))
|
||||
{
|
||||
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has value None in CustomData."), *GetName())));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
#undef VALIDATION_LOG_ERROR
|
||||
|
||||
void URoomData::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
OnPropertiesChanged.Broadcast(this);
|
||||
|
||||
// Reset the cached VoxelBounds to trigger a new computation.
|
||||
CachedVoxelBounds = FVoxelBounds();
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
@@ -0,0 +1,324 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomLevel.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "GameFramework/GameState.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "Room.h"
|
||||
#include "RoomData.h"
|
||||
#include "Door.h"
|
||||
#include "DungeonGenerator.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "RoomVisibilityComponent.h"
|
||||
#include "RoomVisitor.h"
|
||||
#include "Utils/ReplicationUtils.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
bool ARoomLevel::bIsDungeonEditorMode = false;
|
||||
#endif
|
||||
|
||||
ARoomLevel::ARoomLevel(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
bReplicates = false;
|
||||
SetNetUpdateFrequency(10);
|
||||
bIsInit = false;
|
||||
Room = nullptr;
|
||||
DungeonTransform = FTransform::Identity;
|
||||
|
||||
// Create a root component to have a world position
|
||||
SetRootComponent(CreateDefaultSubobject<USceneComponent>(FName("Root")));
|
||||
}
|
||||
|
||||
// Use this for initialization
|
||||
void ARoomLevel::Init(URoom* _Room)
|
||||
{
|
||||
check(IsValid(_Room));
|
||||
Room = _Room;
|
||||
bIsInit = false;
|
||||
|
||||
DungeonTransform = Room->Generator()->GetDungeonTransform();
|
||||
|
||||
// Update the room's bounding box for occlusion culling (also the red box drawn in debug)
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
void ARoomLevel::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (!IsValid(Room))
|
||||
{
|
||||
DungeonLog_Warning("RoomLevel was not spawned by a DungeonGenerator. It's fine for testing a room but occlusion will not work properly. Consider unchecking \"Occlude Dynamic Actors\" in the plugin's settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the data that spawned this level correspond to the data provided in blueprint
|
||||
if (Data != Room->GetRoomData())
|
||||
{
|
||||
DungeonLog_Error("RoomLevel's Data does not match RoomData's Level [Data \"%s\" | Level \"%s\"]. Debug Draw will be incorrect.", *GetNameSafe(Room->GetRoomData()), *GetName());
|
||||
}
|
||||
|
||||
// Create trigger box to track dynamic actors inside the room with IRoomVisitor
|
||||
RoomTrigger = NewObject<UBoxComponent>(this, UBoxComponent::StaticClass(), FName("Room Trigger"));
|
||||
RoomTrigger->RegisterComponent();
|
||||
RoomTrigger->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
|
||||
RoomTrigger->SetCanEverAffectNavigation(false);
|
||||
RoomTrigger->SetGenerateOverlapEvents(true);
|
||||
RoomTrigger->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
RoomTrigger->SetCollisionObjectType(Dungeon::RoomObjectType());
|
||||
RoomTrigger->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
|
||||
RoomTrigger->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic, ECollisionResponse::ECR_Ignore);
|
||||
RoomTrigger->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Ignore);
|
||||
RoomTrigger->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
|
||||
|
||||
static const uint8 FirstChannel = static_cast<uint8>(ECollisionChannel::ECC_GameTraceChannel1);
|
||||
static const uint8 LastChannel = static_cast<uint8>(ECollisionChannel::ECC_GameTraceChannel18);
|
||||
for (uint8 Channel = FirstChannel; Channel <= LastChannel; ++Channel)
|
||||
{
|
||||
ETraceTypeQuery TraceChannel = UEngineTypes::ConvertToTraceType(static_cast<ECollisionChannel>(Channel));
|
||||
if (TraceChannel != ETraceTypeQuery::TraceTypeQuery_MAX)
|
||||
{
|
||||
RoomTrigger->SetCollisionResponseToChannel(static_cast<ECollisionChannel>(Channel), ECollisionResponse::ECR_Ignore);
|
||||
}
|
||||
}
|
||||
|
||||
RoomTrigger->OnComponentBeginOverlap.AddDynamic(this, &ARoomLevel::OnTriggerBeginOverlap);
|
||||
RoomTrigger->OnComponentEndOverlap.AddDynamic(this, &ARoomLevel::OnTriggerEndOverlap);
|
||||
|
||||
// Update trigger box to have the room's bounds
|
||||
FBoxCenterAndExtent LocalBounds = Room->GetLocalBounds();
|
||||
RoomTrigger->SetRelativeLocationAndRotation(LocalBounds.Center, FQuat::Identity);
|
||||
RoomTrigger->SetBoxExtent(LocalBounds.Extent, true);
|
||||
|
||||
SetActorsVisible(Room->IsVisible());
|
||||
|
||||
// Create dynamic components from the RoomCustomData
|
||||
Room->CreateLevelComponents(this);
|
||||
|
||||
bIsInit = true;
|
||||
}
|
||||
|
||||
void ARoomLevel::EndPlay(EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
Room = nullptr;
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void ARoomLevel::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
// @TODO: Place the debug draw in an editor module of the plugin?
|
||||
|
||||
const bool bIsEditingRoom = GetLevel() == GetWorld()->PersistentLevel;
|
||||
bool bShouldDrawDebug = Dungeon::DrawDebug() && (!Dungeon::DrawOnlyWhenEditingRoom() || bIsEditingRoom);
|
||||
#if WITH_EDITOR
|
||||
// Force debug drawing when the editor is in DungeonEditor mode
|
||||
bShouldDrawDebug |= bIsDungeonEditorMode;
|
||||
#endif
|
||||
|
||||
if (IsValid(Data) && bShouldDrawDebug)
|
||||
{
|
||||
const bool bIsRoomValid = (Room != nullptr);
|
||||
const bool bIsRoomDataValid = bIsRoomValid && (Data == Room->GetRoomData());
|
||||
|
||||
const FTransform& RoomTransform = (bIsRoomValid) ? Room->GetTransform() : FTransform::Identity;
|
||||
const bool bIsRoomLocked = bIsRoomValid && Room->IsLocked();
|
||||
UpdateBounds();
|
||||
|
||||
// Cache world
|
||||
const UWorld* World = GetWorld();
|
||||
|
||||
// @TODO: is it still needed now?
|
||||
// Pivot
|
||||
if (Dungeon::ShowRoomOrigin())
|
||||
DrawDebugSphere(World, DungeonTransform.TransformPositionNoScale(RoomTransform.GetLocation()), 100.0f, 4, FColor::Magenta);
|
||||
|
||||
// Room bounds
|
||||
Data->DrawDebug(World, RoomTransform * DungeonTransform, IsPlayerInside() ? FColor::Green : FColor::Red);
|
||||
|
||||
if (bIsRoomLocked)
|
||||
{
|
||||
FBox Box = Bounds.GetBox();
|
||||
const FVector& Min = Box.Min;
|
||||
const FVector& Max = Box.Max;
|
||||
#ifdef T
|
||||
static_assert(false, "T macro is already defined! Please change its name to avoid potential conflicts");
|
||||
#endif
|
||||
#define T(POINT) DungeonTransform.TransformPositionNoScale(POINT)
|
||||
DrawDebugLine(World, T(Min), T(Max), FColor::Red);
|
||||
DrawDebugLine(World, T(FVector(Min.X, Min.Y, Max.Z)), T(FVector(Max.X, Max.Y, Min.Z)), FColor::Red);
|
||||
DrawDebugLine(World, T(FVector(Min.X, Max.Y, Max.Z)), T(FVector(Max.X, Min.Y, Min.Z)), FColor::Red);
|
||||
DrawDebugLine(World, T(FVector(Min.X, Max.Y, Min.Z)), T(FVector(Max.X, Min.Y, Max.Z)), FColor::Red);
|
||||
#undef T
|
||||
}
|
||||
|
||||
// Doors
|
||||
for (int i = 0; i < Data->GetNbDoor(); i++)
|
||||
{
|
||||
const bool bIsConnected = !bIsRoomValid || (bIsRoomDataValid && Room->IsConnected(i));
|
||||
const bool bIsDoorValid = Data->IsDoorValid(i) && !Data->IsDoorDuplicate(i);
|
||||
FDoorDef::DrawDebug(World, Data->Doors[i], Data->GetRoomUnit(), RoomTransform * DungeonTransform, /*bIncludeOffset = */ true, bIsDoorValid && bIsConnected);
|
||||
}
|
||||
}
|
||||
#endif // ENABLE_DRAW_DEBUG
|
||||
}
|
||||
|
||||
bool ARoomLevel::IsPlayerInside(const APlayerController* PlayerController)
|
||||
{
|
||||
return IsValid(Room) ? Room->IsPlayerInside(PlayerController) : false;
|
||||
}
|
||||
|
||||
bool ARoomLevel::IsVisible()
|
||||
{
|
||||
return IsValid(Room) ? Room->IsVisible() : true;
|
||||
}
|
||||
|
||||
bool ARoomLevel::IsLocked()
|
||||
{
|
||||
return IsValid(Room) ? Room->IsLocked() : false;
|
||||
}
|
||||
|
||||
void ARoomLevel::Lock(bool lock)
|
||||
{
|
||||
if (IsValid(Room))
|
||||
Room->Lock(lock);
|
||||
}
|
||||
|
||||
void ARoomLevel::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
TriggerActor(OtherActor, true);
|
||||
}
|
||||
|
||||
void ARoomLevel::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
|
||||
{
|
||||
TriggerActor(OtherActor, false);
|
||||
}
|
||||
|
||||
FVector ARoomLevel::GetBoundsCenter() const
|
||||
{
|
||||
return DungeonTransform.TransformPositionNoScale(Bounds.Center);
|
||||
}
|
||||
|
||||
FVector ARoomLevel::GetBoundsExtent() const
|
||||
{
|
||||
return Bounds.Extent;
|
||||
}
|
||||
|
||||
void ARoomLevel::UpdateBounds()
|
||||
{
|
||||
if (IsValid(Room))
|
||||
{
|
||||
Bounds = Room->GetBounds();
|
||||
}
|
||||
else if (IsValid(Data))
|
||||
{
|
||||
Bounds = Data->GetBounds();
|
||||
}
|
||||
}
|
||||
|
||||
void ARoomLevel::SetActorsVisible(bool Visible)
|
||||
{
|
||||
if (!Dungeon::OcclusionCulling())
|
||||
{
|
||||
// Force visibility when occlusion culling is disabled
|
||||
Visible = true;
|
||||
}
|
||||
|
||||
ULevel* Level = GetLevel();
|
||||
if (IsValid(Level))
|
||||
{
|
||||
for (AActor* Actor : Level->Actors)
|
||||
{
|
||||
if (!IsValid(Actor))
|
||||
continue;
|
||||
|
||||
// HACK: Don't manage replicated actors as their ActorHiddenInGame is replicated
|
||||
// and will mess up the actor visibility on clients!
|
||||
if (Actor->GetIsReplicated())
|
||||
continue;
|
||||
|
||||
// Discard explicitly ignored actors.
|
||||
// They can have a (Static) Room Visibility Component attached to have a custom occlusion management.
|
||||
if (Actor->ActorHasTag(FName("Ignore Room Culling")))
|
||||
continue;
|
||||
|
||||
Actor->SetActorHiddenInGame(!Visible);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the change (useful for RoomVisibilityComponent)
|
||||
VisibilityChangedEvent.Broadcast(this, Visible);
|
||||
}
|
||||
|
||||
void ARoomLevel::UpdateVisitor(UObject* Visitor, bool IsInside)
|
||||
{
|
||||
check(Visitor->Implements<URoomVisitor>());
|
||||
|
||||
if (IsInside && !Visitors.Contains(Visitor))
|
||||
{
|
||||
Visitors.Add(Visitor);
|
||||
IRoomVisitor::Execute_OnRoomEnter(Visitor, this);
|
||||
}
|
||||
else if (!IsInside && Visitors.Contains(Visitor))
|
||||
{
|
||||
Visitors.Remove(Visitor);
|
||||
IRoomVisitor::Execute_OnRoomExit(Visitor, this);
|
||||
}
|
||||
}
|
||||
|
||||
void ARoomLevel::TriggerActor(AActor* Actor, bool IsInTrigger)
|
||||
{
|
||||
if (!IsValid(Actor))
|
||||
return;
|
||||
|
||||
// Call the interface on the actor itself
|
||||
if (Actor->Implements<URoomVisitor>())
|
||||
{
|
||||
UpdateVisitor(Actor, IsInTrigger);
|
||||
}
|
||||
|
||||
// Call the interface on its components too
|
||||
TArray<UActorComponent*, FDefaultAllocator> VisitorComps = Actor->GetComponentsByInterface(URoomVisitor::StaticClass());
|
||||
for (UActorComponent* VisitorComp : VisitorComps)
|
||||
{
|
||||
check(VisitorComp);
|
||||
UpdateVisitor(VisitorComp, IsInTrigger);
|
||||
}
|
||||
|
||||
if (IsInTrigger)
|
||||
ActorEnterRoomEvent.Broadcast(this, Actor);
|
||||
else
|
||||
ActorExitRoomEvent.Broadcast(this, Actor);
|
||||
}
|
||||
|
||||
void ARoomLevel::PostInitProperties()
|
||||
{
|
||||
Super::PostInitProperties();
|
||||
UpdateBounds();
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void ARoomLevel::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||
{
|
||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||
UpdateBounds();
|
||||
OnPropertiesChanged.Broadcast(this);
|
||||
}
|
||||
#endif
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomVisibilityComponent.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "ProceduralDungeonLog.h"
|
||||
#include "RoomLevel.h"
|
||||
|
||||
URoomVisibilityComponent::URoomVisibilityComponent()
|
||||
: Super()
|
||||
{
|
||||
}
|
||||
|
||||
void URoomVisibilityComponent::OnRoomEnter_Implementation(ARoomLevel* RoomLevel)
|
||||
{
|
||||
DungeonLog_Debug("[Visibility] '%s' Enters Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
|
||||
RegisterVisibilityDelegate(RoomLevel, true);
|
||||
}
|
||||
|
||||
void URoomVisibilityComponent::OnRoomExit_Implementation(ARoomLevel* RoomLevel)
|
||||
{
|
||||
DungeonLog_Debug("[Visibility] '%s' Exits Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
|
||||
RegisterVisibilityDelegate(RoomLevel, false);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "RoomVisitor.h"
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBoxMinAndMaxTest, "ProceduralDungeon.Types.BoxMinAndMax", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
|
||||
|
||||
bool FBoxMinAndMaxTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Constructor Test
|
||||
{
|
||||
FBoxMinAndMax Box0;
|
||||
FBoxMinAndMax Box1(FIntVector(-1), FIntVector(1));
|
||||
FBoxMinAndMax Box2(FIntVector(3, 2, 1), FIntVector(-1, -2, -3));
|
||||
FBoxMinAndMax Box3(Box2);
|
||||
FBoxMinAndMax Box4(FIntVector(-3, 2, -1), FIntVector(1, -2, 3));
|
||||
FBoxMinAndMax Box5 = FBoxMinAndMax::Invalid;
|
||||
|
||||
TestEqual(TEXT("Default Constructor Min == 0"), Box0.GetMin(), FIntVector(0));
|
||||
TestEqual(TEXT("Default Constructor Max == 1"), Box0.GetMax(), FIntVector(1));
|
||||
TestEqual(TEXT("Constructor (-1,1) Min == -1"), Box1.GetMin(), FIntVector(-1));
|
||||
TestEqual(TEXT("Constructor (-1,1) Max == 1"), Box1.GetMax(), FIntVector(1));
|
||||
TestEqual(TEXT("Constructor ((3,2,1), (-1,-2,-3)) Min == (-1,-2,-3)"), Box2.GetMin(), FIntVector(-1, -2, -3));
|
||||
TestEqual(TEXT("Constructor ((3,2,1), (-1,-2,-3)) Max == (3,2,1)"), Box2.GetMax(), FIntVector(3, 2, 1));
|
||||
TestEqual(TEXT("Copy Constructor of ((3,2,1), (-1,-2,-3)) Min == (-1,-2,-3)"), Box3.GetMin(), FIntVector(-1, -2, -3));
|
||||
TestEqual(TEXT("Copy Constructor of ((3,2,1), (-1,-2,-3)) Max == (3,2,1)"), Box3.GetMax(), FIntVector(3, 2, 1));
|
||||
TestEqual(TEXT("Constructor ((-3,2,-1), (1,-2,3)) Min == (-3,-2,-1)"), Box4.GetMin(), FIntVector(-3, -2, -1));
|
||||
TestEqual(TEXT("Constructor ((-3,2,-1), (1,-2,3)) Max == (1,2,3)"), Box4.GetMax(), FIntVector(1, 2, 3));
|
||||
|
||||
TestEqual(TEXT("Invalid Box Max == 0"), Box5.GetMin(), FIntVector(0));
|
||||
TestEqual(TEXT("Invalid Box Max == 0"), Box5.GetMax(), FIntVector(0));
|
||||
TestFalse(TEXT("Invalid Box is not Valid"), Box5.IsValid());
|
||||
}
|
||||
|
||||
// Size Test
|
||||
{
|
||||
FBoxMinAndMax Box0(FIntVector(0), FIntVector(0));
|
||||
FBoxMinAndMax Box1(FIntVector(0), FIntVector(1));
|
||||
FBoxMinAndMax Box2(FIntVector(0), FIntVector(-1));
|
||||
FBoxMinAndMax Box3(FIntVector(-1), FIntVector(0));
|
||||
FBoxMinAndMax Box4(FIntVector(1), FIntVector(0));
|
||||
FBoxMinAndMax Box5(FIntVector(0, 0, 0), FIntVector(3, 4, 5));
|
||||
FBoxMinAndMax Box6(FIntVector(3, 4, 5), FIntVector(-5, -4, -3));
|
||||
|
||||
TestEqual(TEXT("Box(0, 0).GetSize() == (0,0,0)"), Box0.GetSize(), FIntVector(0));
|
||||
TestEqual(TEXT("Box(0, 1).GetSize() == (1,1,1)"), Box1.GetSize(), FIntVector(1));
|
||||
TestEqual(TEXT("Box(0, -1).GetSize() == (1,1,1)"), Box2.GetSize(), FIntVector(1));
|
||||
TestEqual(TEXT("Box(-1, 0).GetSize() == (1,1,1)"), Box3.GetSize(), FIntVector(1));
|
||||
TestEqual(TEXT("Box(1, 0).GetSize() == (1,1,1)"), Box4.GetSize(), FIntVector(1));
|
||||
TestEqual(TEXT("Box(0, (3, 4, 5)).GetSize() == (3,4,5)"), Box5.GetSize(), FIntVector(3, 4, 5));
|
||||
TestEqual(TEXT("Box((3, 4, 5), (-5, -4, -3)).GetSize() == (8,8,8)"), Box6.GetSize(), FIntVector(8));
|
||||
}
|
||||
|
||||
// Overlap Test
|
||||
{
|
||||
FBoxMinAndMax Box1(FIntVector(-2, -3, -4), FIntVector(2, 3, 4));
|
||||
FBoxMinAndMax Box2(FIntVector(3, 0, 0), FIntVector(5, 5, 5));
|
||||
FBoxMinAndMax Box3(FIntVector(2, 3, 0), FIntVector(5, 5, 5));
|
||||
FBoxMinAndMax Box4(FIntVector(2, 2, 0), FIntVector(5, 5, 5));
|
||||
FBoxMinAndMax Box5(FIntVector(1, 2, 0), FIntVector(5, 5, 5));
|
||||
FBoxMinAndMax Box6(FIntVector(-3, 0, 0), FIntVector(-5, -5, -5));
|
||||
FBoxMinAndMax Box7(FIntVector(-2, -2, 0), FIntVector(-5, -5, -5));
|
||||
FBoxMinAndMax Box8(FIntVector(-1, -2, 0), FIntVector(-5, -5, -5));
|
||||
|
||||
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-2,-3,-4), (2,3,4)) overlap"), FBoxMinAndMax::Overlap(Box1, Box1));
|
||||
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((3,0,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box2));
|
||||
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((2,3,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box3));
|
||||
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((2,2,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box4));
|
||||
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((1,2,0), (5,5,5)) overlap"), FBoxMinAndMax::Overlap(Box1, Box5));
|
||||
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-3,0,0), (-5,-5,-5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box6));
|
||||
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-2,-2,0), (-5,-5,-5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box7));
|
||||
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-1,-2,0), (-5,-5,-5)) overlap"), FBoxMinAndMax::Overlap(Box1, Box8));
|
||||
|
||||
FBoxMinAndMax BoxA(FIntVector(0), FIntVector(1));
|
||||
FBoxMinAndMax BoxB(FIntVector(-2), FIntVector(5));
|
||||
TestTrue(TEXT("Box(0, 1) and Box(-2, 5) overlap"), FBoxMinAndMax::Overlap(BoxA, BoxB));
|
||||
}
|
||||
|
||||
// Rotation Test
|
||||
{
|
||||
FBoxMinAndMax Box0(FIntVector(0), FIntVector(1));
|
||||
FBoxMinAndMax RotBox0N = Rotate(Box0, EDoorDirection::North);
|
||||
FBoxMinAndMax RotBox0E = Rotate(Box0, EDoorDirection::East);
|
||||
FBoxMinAndMax RotBox0S = Rotate(Box0, EDoorDirection::South);
|
||||
FBoxMinAndMax RotBox0W = Rotate(Box0, EDoorDirection::West);
|
||||
|
||||
TestEqual(TEXT("Rotate(Box(0,1), N).Min == 0"), RotBox0N.GetMin(), FIntVector(0));
|
||||
TestEqual(TEXT("Rotate(Box(0,1), N).Max == 1"), RotBox0N.GetMax(), FIntVector(1));
|
||||
TestEqual(TEXT("Rotate(Box(0,1), E).Min == 0"), RotBox0E.GetMin(), FIntVector(0));
|
||||
TestEqual(TEXT("Rotate(Box(0,1), E).Max == 1"), RotBox0E.GetMax(), FIntVector(1));
|
||||
TestEqual(TEXT("Rotate(Box(0,1), S).Min == 0"), RotBox0S.GetMin(), FIntVector(0));
|
||||
TestEqual(TEXT("Rotate(Box(0,1), S).Max == 1"), RotBox0S.GetMax(), FIntVector(1));
|
||||
TestEqual(TEXT("Rotate(Box(0,1), W).Min == 0"), RotBox0W.GetMin(), FIntVector(0));
|
||||
TestEqual(TEXT("Rotate(Box(0,1), W).Max == 1"), RotBox0W.GetMax(), FIntVector(1));
|
||||
|
||||
FBoxMinAndMax Box1(FIntVector(-1, 0, -1), FIntVector(3, 1, 1));
|
||||
FBoxMinAndMax RotBox1N = Rotate(Box1, EDoorDirection::North);
|
||||
FBoxMinAndMax RotBox1E = Rotate(Box1, EDoorDirection::East);
|
||||
FBoxMinAndMax RotBox1S = Rotate(Box1, EDoorDirection::South);
|
||||
FBoxMinAndMax RotBox1W = Rotate(Box1, EDoorDirection::West);
|
||||
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), N).Min == (-1,0,-1)"), RotBox1N.GetMin(), FIntVector(-1, 0, -1));
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), N).Max == (3,1,1)"), RotBox1N.GetMax(), FIntVector(3, 1, 1));
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), E).Min == (0,-1,-1)"), RotBox1E.GetMin(), FIntVector(0, -1, -1));
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), E).Max == (1,3,1)"), RotBox1E.GetMax(), FIntVector(1, 3, 1));
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), S).Min == (-2,0,-1)"), RotBox1S.GetMin(), FIntVector(-2, 0, -1));
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), S).Max == (2,1,1)"), RotBox1S.GetMax(), FIntVector(2, 1, 1));
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), W).Min == (0,-2,-1)"), RotBox1W.GetMin(), FIntVector(0, -2, -1));
|
||||
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), W).Max == (1,2,1)"), RotBox1W.GetMax(), FIntVector(1, 2, 1));
|
||||
}
|
||||
|
||||
// Extend Test
|
||||
{
|
||||
FBoxMinAndMax BoxToExtend;
|
||||
FBoxMinAndMax Box0({0, 0, 0}, {1, 1, 1});
|
||||
FBoxMinAndMax Box1 = Box0 + FIntVector(1, 2, 3); // offseted box
|
||||
FBoxMinAndMax Box2({-1, -2, -3}, {4, 5, 6});
|
||||
|
||||
BoxToExtend.Extend(Box0);
|
||||
TestEqual(TEXT("Extend Box Step 1"), BoxToExtend, Box0);
|
||||
|
||||
// Extend the box to contain the provided box
|
||||
BoxToExtend.Extend(Box1);
|
||||
TestEqual(TEXT("Extend Box Step 2"), BoxToExtend, FBoxMinAndMax({0, 0, 0}, {2, 3, 4}));
|
||||
|
||||
BoxToExtend.Extend(Box2);
|
||||
TestEqual(TEXT("Extend Box Step 3"), BoxToExtend, Box2);
|
||||
|
||||
// The extended box should not change when using a box entirely contained in it.
|
||||
BoxToExtend.Extend(Box0);
|
||||
TestEqual(TEXT("Extend Box Step 4"), BoxToExtend, Box2);
|
||||
}
|
||||
|
||||
// IsInside(FBoxMinAndMax) Test
|
||||
{
|
||||
FBoxMinAndMax Bounds(FIntVector(-4, -5, -6), FIntVector(7, 8, 9));
|
||||
FBoxMinAndMax Box(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
|
||||
|
||||
// Completely inside, no coincident face
|
||||
TestTrue(*FString::Printf(TEXT("Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
|
||||
// Positive X
|
||||
Box += FIntVector(6, 0, 0); // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[X+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(1, 0, 0); // Intersecting
|
||||
TestFalse(*FString::Printf(TEXT("[X+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(1, 0, 0); // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[X+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
|
||||
// Reset
|
||||
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
|
||||
|
||||
// Positive Y
|
||||
Box += FIntVector(0, 6, 0); // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Y+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, 1, 0); // Intersecting
|
||||
TestFalse(*FString::Printf(TEXT("[Y+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, 1, 0); // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Y+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
|
||||
// Reset
|
||||
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
|
||||
|
||||
// Positive Z
|
||||
Box += FIntVector(0, 0, 6); // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Z+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, 0, 1); // Intersecting
|
||||
TestFalse(*FString::Printf(TEXT("[Z+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, 0, 1); // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Z+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
|
||||
// Reset
|
||||
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
|
||||
|
||||
// Negative X
|
||||
Box += FIntVector(-3, 0, 0); // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[X-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(-1, 0, 0); // Intersecting
|
||||
TestFalse(*FString::Printf(TEXT("[X-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(-1, 0, 0); // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[X-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
|
||||
// Reset
|
||||
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
|
||||
|
||||
// Negative Y
|
||||
Box += FIntVector(0, -4, 0); // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Y-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, -2, 0); // Intersecting
|
||||
TestFalse(*FString::Printf(TEXT("[Y-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, -1, 0); // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Y-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
|
||||
// Reset
|
||||
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
|
||||
|
||||
// Negative Z
|
||||
Box += FIntVector(0, 0, -5); // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Z-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, 0, -3); // Intersecting
|
||||
TestFalse(*FString::Printf(TEXT("[Z-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
Box += FIntVector(0, 0, -1); // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Z-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
|
||||
}
|
||||
|
||||
// IsInside(FIntVector) Test
|
||||
{
|
||||
FBoxMinAndMax Bounds(FIntVector(-4, -5, -6), FIntVector(7, 8, 9));
|
||||
FIntVector Cell {0};
|
||||
|
||||
// Completely inside, no coincident face
|
||||
TestTrue(*FString::Printf(TEXT("Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
|
||||
// Positive X
|
||||
Cell = {6, 0, 0}; // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[X+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
Cell = {7, 0, 0}; // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[X+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
|
||||
// Negative X
|
||||
Cell = {-4, 0, 0}; // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[X-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
Cell = {-5, 0, 0}; // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[X-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
|
||||
// Positive Y
|
||||
Cell = {0, 7, 0}; // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Y+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
Cell = {0, 8, 0}; // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Y+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
|
||||
// Negative Y
|
||||
Cell = {0, -5, 0}; // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Y-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
Cell = {0, -6, 0}; // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Y-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
|
||||
// Positive Z
|
||||
Cell = {0, 0, 8}; // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Z+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
Cell = {0, 0, 9}; // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Z+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
|
||||
// Negative Z
|
||||
Cell = {0, 0, -6}; // Inside but with coincident face
|
||||
TestTrue(*FString::Printf(TEXT("[Z-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
Cell = {0, 0, -7}; // Outside but with a coincident face
|
||||
TestFalse(*FString::Printf(TEXT("[Z-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "VoxelBounds/VoxelBounds.h"
|
||||
#include "CustomScoreCallbacks.generated.h"
|
||||
|
||||
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
|
||||
class UCustomScoreCallback : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION()
|
||||
bool ZeroScore(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B, int32& Score)
|
||||
{
|
||||
Score = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
UFUNCTION()
|
||||
bool NeverPass(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B, int32& Score)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "Utils/CompatUtils.h"
|
||||
#include "DungeonSaveClasses.generated.h"
|
||||
|
||||
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
|
||||
class USaveTestObject : public UObject, public IDungeonCustomSerialization, public IDungeonSaveInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ Begin IDungeonCustomSerialization Interface
|
||||
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override
|
||||
{
|
||||
OrderOfExecution += (bIsLoading) ? TEXT("X") : TEXT("C");
|
||||
Record.EnterField(AR_FIELD_NAME("NativeTest")) << TestSerializeObjectFunction;
|
||||
return true;
|
||||
}
|
||||
//~ End IDungeonCustomSerialization Interface
|
||||
|
||||
//~ Begin IDungeonSaveInterface Interface
|
||||
virtual void PreSaveDungeon_Implementation() override
|
||||
{
|
||||
OrderOfExecution += TEXT("A");
|
||||
}
|
||||
|
||||
virtual void DungeonPreSerialize_Implementation(bool bIsLoading) override
|
||||
{
|
||||
OrderOfExecution += (bIsLoading) ? TEXT("W") : TEXT("B");
|
||||
}
|
||||
|
||||
virtual void DungeonPostSerialize_Implementation(bool bIsLoading) override
|
||||
{
|
||||
OrderOfExecution += (bIsLoading) ? TEXT("Y") : TEXT("D");
|
||||
}
|
||||
|
||||
virtual void PostLoadDungeon_Implementation() override
|
||||
{
|
||||
OrderOfExecution += TEXT("Z");
|
||||
}
|
||||
//~ End IDungeonSaveInterface Interface
|
||||
|
||||
public:
|
||||
UPROPERTY(SaveGame)
|
||||
int32 TestSaveGameFlag {0};
|
||||
|
||||
int32 TestSerializeObjectFunction {0};
|
||||
|
||||
UPROPERTY();
|
||||
FString OrderOfExecution {};
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RoomConstraints/RoomConstraint.h"
|
||||
#include "RoomConstraintChildClasses.generated.h"
|
||||
|
||||
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
|
||||
class UConstraintPass : public URoomConstraint
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
|
||||
class UConstraintFail : public URoomConstraint
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RoomCustomData.h"
|
||||
#include "RoomCustomDataChildClasses.generated.h"
|
||||
|
||||
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
|
||||
class UCustomDataA : public URoomCustomData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
|
||||
class UCustomDataB : public URoomCustomData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
|
||||
class UCustomDataC : public URoomCustomData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "DoorType.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorDefTest, "ProceduralDungeon.Types.DoorDef", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
|
||||
|
||||
bool FDoorDefTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
CREATE_DATA_ASSET(UDoorType, DoorTypeA);
|
||||
CREATE_DATA_ASSET(UDoorType, DoorTypeB);
|
||||
|
||||
// Constructor Test
|
||||
{
|
||||
FDoorDef Door0;
|
||||
FDoorDef Door1(FDoorDef::Invalid);
|
||||
FDoorDef Door2({1, 2, 3}, EDoorDirection::South, DoorTypeA.Get());
|
||||
FDoorDef Door3(Door2);
|
||||
|
||||
TestTrue(TEXT("Default Constructor makes valid door"), Door0.IsValid());
|
||||
TestFalse(TEXT("Copy constructor of invalid door mus makes an invalid door"), Door1.IsValid());
|
||||
|
||||
TestTrue(TEXT("Constructor (1,2,3) South DoorTypeA is valid"), Door2.IsValid());
|
||||
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Position == (1,2,3)"), Door2.Position, {1, 2, 3});
|
||||
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Direction == South"), Door2.Direction, EDoorDirection::South);
|
||||
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Type == DoorTypeA"), Door2.Type, DoorTypeA.Get());
|
||||
|
||||
TestTrue(TEXT("Copy Constructor of valid door must be valid too"), Door3.IsValid());
|
||||
TestEqual(TEXT("Copy Constructor must carry over position"), Door3.Position, Door2.Position);
|
||||
TestEqual(TEXT("Copy Constructor must carry over direction"), Door3.Direction, Door2.Direction);
|
||||
TestEqual(TEXT("Copy Constructor must carry over type"), Door3.Type, Door2.Type);
|
||||
}
|
||||
|
||||
// Compatibility Test
|
||||
{
|
||||
FDoorDef Door0({0, 0, 0}, EDoorDirection::North, DoorTypeA.Get());
|
||||
FDoorDef Door1({1, 2, 3}, EDoorDirection::South, DoorTypeA.Get());
|
||||
FDoorDef Door2({1, 2, 3}, EDoorDirection::South, DoorTypeB.Get());
|
||||
FDoorDef Door3;
|
||||
|
||||
TestTrue(TEXT("Door0 is compatible with Door1"), FDoorDef::AreCompatible(Door0, Door1));
|
||||
TestFalse(TEXT("Door0 is not compatible with Door2"), FDoorDef::AreCompatible(Door0, Door2));
|
||||
TestFalse(TEXT("Door0 is not compatible with Door3"), FDoorDef::AreCompatible(Door0, Door3));
|
||||
TestFalse(TEXT("Door1 is not compatible with Door2"), FDoorDef::AreCompatible(Door1, Door2));
|
||||
TestFalse(TEXT("Door1 is not compatible with Door3"), FDoorDef::AreCompatible(Door1, Door3));
|
||||
TestFalse(TEXT("Door2 is not compatible with Door3"), FDoorDef::AreCompatible(Door2, Door3));
|
||||
}
|
||||
|
||||
// Opposite Test
|
||||
{
|
||||
FDoorDef Door0({1, 2, 3}, EDoorDirection::North, DoorTypeA.Get());
|
||||
FDoorDef Door1 = Door0.GetOpposite();
|
||||
|
||||
TestTrue(TEXT("Opposite door is valid"), Door1.IsValid());
|
||||
TestEqual(TEXT("Opposite of North is South"), Door1.Direction, EDoorDirection::South);
|
||||
TestEqual(TEXT("Opposite cell of (1,2,3)[North] is (2,2,3)"), Door1.Position, {2, 2, 3});
|
||||
TestEqual(TEXT("Opposite type is the same"), Door1.Type, DoorTypeA.Get());
|
||||
TestTrue(TEXT("Opposite door is compatible with original"), FDoorDef::AreCompatible(Door0, Door1));
|
||||
}
|
||||
|
||||
// Transform Test
|
||||
{
|
||||
FDoorDef Door0({1, 2, 3}, EDoorDirection::North, DoorTypeA.Get());
|
||||
FDoorDef TransformedDoor0({-1, 3, 6}, EDoorDirection::East, DoorTypeA.Get());
|
||||
FDoorDef TransformedDoor1({0, 0, 6}, EDoorDirection::South, DoorTypeA.Get());
|
||||
FDoorDef TransformedDoor2({3, 1, 6}, EDoorDirection::West, DoorTypeA.Get());
|
||||
|
||||
TestEqual(TEXT("Transformation {(1, 2, 3), East} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::East), TransformedDoor0);
|
||||
TestEqual(TEXT("Transformation {(1, 2, 3), South} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::South), TransformedDoor1);
|
||||
TestEqual(TEXT("Transformation {(1, 2, 3), West} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::West), TransformedDoor2);
|
||||
|
||||
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), East} is correct"), FDoorDef::InverseTransform(TransformedDoor0, {1, 2, 3}, EDoorDirection::East), Door0);
|
||||
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), South} is correct"), FDoorDef::InverseTransform(TransformedDoor1, {1, 2, 3}, EDoorDirection::South), Door0);
|
||||
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), West} is correct"), FDoorDef::InverseTransform(TransformedDoor2, {1, 2, 3}, EDoorDirection::West), Door0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorDirectionTest, "ProceduralDungeon.Types.DoorDirection", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
|
||||
|
||||
bool FDoorDirectionTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Adding directions is correct
|
||||
{
|
||||
TestEqual(TEXT("North + North = North"), EDoorDirection::North + EDoorDirection::North, EDoorDirection::North);
|
||||
TestEqual(TEXT("North + East = East"), EDoorDirection::North + EDoorDirection::East, EDoorDirection::East);
|
||||
TestEqual(TEXT("North + South = South"), EDoorDirection::North + EDoorDirection::South, EDoorDirection::South);
|
||||
TestEqual(TEXT("North + West = West"), EDoorDirection::North + EDoorDirection::West, EDoorDirection::West);
|
||||
|
||||
TestEqual(TEXT("East + North = East"), EDoorDirection::East + EDoorDirection::North, EDoorDirection::East);
|
||||
TestEqual(TEXT("East + East = South"), EDoorDirection::East + EDoorDirection::East, EDoorDirection::South);
|
||||
TestEqual(TEXT("East + South = West"), EDoorDirection::East + EDoorDirection::South, EDoorDirection::West);
|
||||
TestEqual(TEXT("East + West = North"), EDoorDirection::East + EDoorDirection::West, EDoorDirection::North);
|
||||
|
||||
TestEqual(TEXT("South + North = South"), EDoorDirection::South + EDoorDirection::North, EDoorDirection::South);
|
||||
TestEqual(TEXT("South + East = West"), EDoorDirection::South + EDoorDirection::East, EDoorDirection::West);
|
||||
TestEqual(TEXT("South + South = North"), EDoorDirection::South + EDoorDirection::South, EDoorDirection::North);
|
||||
TestEqual(TEXT("South + West = East"), EDoorDirection::South + EDoorDirection::West, EDoorDirection::East);
|
||||
|
||||
TestEqual(TEXT("West + North = West"), EDoorDirection::West + EDoorDirection::North, EDoorDirection::West);
|
||||
TestEqual(TEXT("West + East = North"), EDoorDirection::West + EDoorDirection::East, EDoorDirection::North);
|
||||
TestEqual(TEXT("West + South = East"), EDoorDirection::West + EDoorDirection::South, EDoorDirection::East);
|
||||
TestEqual(TEXT("West + West = South"), EDoorDirection::West + EDoorDirection::West, EDoorDirection::South);
|
||||
}
|
||||
|
||||
// Subtracting directions is correct
|
||||
{
|
||||
TestEqual(TEXT("North - North = North"), EDoorDirection::North - EDoorDirection::North, EDoorDirection::North);
|
||||
TestEqual(TEXT("North - East = West"), EDoorDirection::North - EDoorDirection::East, EDoorDirection::West);
|
||||
TestEqual(TEXT("North - South = South"), EDoorDirection::North - EDoorDirection::South, EDoorDirection::South);
|
||||
TestEqual(TEXT("North - West = East"), EDoorDirection::North - EDoorDirection::West, EDoorDirection::East);
|
||||
|
||||
TestEqual(TEXT("East - North = East"), EDoorDirection::East - EDoorDirection::North, EDoorDirection::East);
|
||||
TestEqual(TEXT("East - East = North"), EDoorDirection::East - EDoorDirection::East, EDoorDirection::North);
|
||||
TestEqual(TEXT("East - South = West"), EDoorDirection::East - EDoorDirection::South, EDoorDirection::West);
|
||||
TestEqual(TEXT("East - West = South"), EDoorDirection::East - EDoorDirection::West, EDoorDirection::South);
|
||||
|
||||
TestEqual(TEXT("South - North = South"), EDoorDirection::South - EDoorDirection::North, EDoorDirection::South);
|
||||
TestEqual(TEXT("South - East = East"), EDoorDirection::South - EDoorDirection::East, EDoorDirection::East);
|
||||
TestEqual(TEXT("South - South = North"), EDoorDirection::South - EDoorDirection::South, EDoorDirection::North);
|
||||
TestEqual(TEXT("South - West = West"), EDoorDirection::South - EDoorDirection::West, EDoorDirection::West);
|
||||
|
||||
TestEqual(TEXT("West - North = West"), EDoorDirection::West - EDoorDirection::North, EDoorDirection::West);
|
||||
TestEqual(TEXT("West - East = South"), EDoorDirection::West - EDoorDirection::East, EDoorDirection::South);
|
||||
TestEqual(TEXT("West - South = East"), EDoorDirection::West - EDoorDirection::South, EDoorDirection::East);
|
||||
TestEqual(TEXT("West - West = North"), EDoorDirection::West - EDoorDirection::West, EDoorDirection::North);
|
||||
}
|
||||
|
||||
// Negating directions are correct
|
||||
{
|
||||
TestEqual(TEXT("-North = North"), -EDoorDirection::North, EDoorDirection::North);
|
||||
TestEqual(TEXT("-East = West"), -EDoorDirection::East, EDoorDirection::West);
|
||||
TestEqual(TEXT("-South = South"), -EDoorDirection::South, EDoorDirection::South);
|
||||
TestEqual(TEXT("-West = East"), -EDoorDirection::West, EDoorDirection::East);
|
||||
}
|
||||
|
||||
// Opposite directions are correct
|
||||
{
|
||||
TestEqual(TEXT("~North = South"), ~EDoorDirection::North, EDoorDirection::South);
|
||||
TestEqual(TEXT("~East = West"), ~EDoorDirection::East, EDoorDirection::West);
|
||||
TestEqual(TEXT("~South = North"), ~EDoorDirection::South, EDoorDirection::North);
|
||||
TestEqual(TEXT("~West = East"), ~EDoorDirection::West, EDoorDirection::East);
|
||||
}
|
||||
|
||||
// Incrementing/decrementing directions are correct
|
||||
{
|
||||
EDoorDirection direction {EDoorDirection::North};
|
||||
TestEqual(TEXT("++North = East"), ++direction, EDoorDirection::East);
|
||||
TestEqual(TEXT("++East = South"), ++direction, EDoorDirection::South);
|
||||
TestEqual(TEXT("++South = West"), ++direction, EDoorDirection::West);
|
||||
TestEqual(TEXT("++West = North"), ++direction, EDoorDirection::North);
|
||||
|
||||
direction = EDoorDirection::North;
|
||||
TestEqual(TEXT("--North = West"), --direction, EDoorDirection::West);
|
||||
TestEqual(TEXT("--West = South"), --direction, EDoorDirection::South);
|
||||
TestEqual(TEXT("--South = East"), --direction, EDoorDirection::East);
|
||||
TestEqual(TEXT("--East = North"), --direction, EDoorDirection::North);
|
||||
}
|
||||
|
||||
// Boolean testing directions
|
||||
{
|
||||
TestFalse(TEXT("!North = false"), !EDoorDirection::North);
|
||||
TestFalse(TEXT("!East = false"), !EDoorDirection::East);
|
||||
TestFalse(TEXT("!South = false"), !EDoorDirection::South);
|
||||
TestFalse(TEXT("!West = false"), !EDoorDirection::West);
|
||||
TestTrue(TEXT("!NbDirection = true"), !EDoorDirection::NbDirection);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "DoorType.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorTypeTests, "ProceduralDungeon.Types.DoorType", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDoorTypeTests::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Test DoorType Compatibility
|
||||
{
|
||||
|
||||
CREATE_DATA_ASSET(UDoorType, TypeA);
|
||||
CREATE_DATA_ASSET(UDoorType, TypeB);
|
||||
CREATE_DATA_ASSET(UDoorType, TypeC);
|
||||
/* Default Compatibility Table:
|
||||
* A B C N
|
||||
* A O X X X
|
||||
* B X O X X
|
||||
* C X X O X
|
||||
* N X X X O
|
||||
*/
|
||||
TestTrue("TypeA should be compatible with itself", UDoorType::AreCompatible(TypeA.Get(), TypeA.Get()));
|
||||
TestFalse("TypeA should not be compatible with TypeB", UDoorType::AreCompatible(TypeA.Get(), TypeB.Get()));
|
||||
TestFalse("TypeA should not be compatible with TypeC", UDoorType::AreCompatible(TypeA.Get(), TypeC.Get()));
|
||||
TestFalse("TypeA should not be compatible with Null", UDoorType::AreCompatible(TypeA.Get(), nullptr));
|
||||
|
||||
TestFalse("TypeB should not be compatible with TypeA", UDoorType::AreCompatible(TypeB.Get(), TypeA.Get()));
|
||||
TestTrue("TypeB should be compatible with itself", UDoorType::AreCompatible(TypeB.Get(), TypeB.Get()));
|
||||
TestFalse("TypeB should not be compatible with TypeC", UDoorType::AreCompatible(TypeB.Get(), TypeC.Get()));
|
||||
TestFalse("TypeB should not be compatible with Null", UDoorType::AreCompatible(TypeB.Get(), nullptr));
|
||||
|
||||
TestFalse("TypeC should not be compatible with TypeA", UDoorType::AreCompatible(TypeC.Get(), TypeA.Get()));
|
||||
TestFalse("TypeC should not be compatible with TypeB", UDoorType::AreCompatible(TypeC.Get(), TypeB.Get()));
|
||||
TestTrue("TypeC should not be compatible with TypeC", UDoorType::AreCompatible(TypeC.Get(), TypeC.Get()));
|
||||
TestFalse("TypeC should not be compatible with Null", UDoorType::AreCompatible(TypeC.Get(), nullptr));
|
||||
|
||||
TestFalse("Null should not be compatible with TypeA", UDoorType::AreCompatible(nullptr, TypeA.Get()));
|
||||
TestFalse("Null should not be compatible with TypeB", UDoorType::AreCompatible(nullptr, TypeB.Get()));
|
||||
TestFalse("Null should not be compatible with TypeC", UDoorType::AreCompatible(nullptr, TypeC.Get()));
|
||||
TestTrue("Null should be compatible with Null", UDoorType::AreCompatible(nullptr, nullptr));
|
||||
|
||||
/* Test Compatibility Table:
|
||||
* A B C N
|
||||
* A O O X X
|
||||
* B O X X X
|
||||
* C X X O X
|
||||
* N X X X O
|
||||
*/
|
||||
TypeA->SetCompatibility({});
|
||||
TypeB->SetCompatibility({ TypeA.Get() });
|
||||
TypeB->SetCompatibleWithItself(false);
|
||||
TypeC->SetCompatibility({});
|
||||
|
||||
TestTrue("TypeA should be compatible with itself", UDoorType::AreCompatible(TypeA.Get(), TypeA.Get()));
|
||||
TestTrue("TypeA should be compatible with TypeB", UDoorType::AreCompatible(TypeA.Get(), TypeB.Get()));
|
||||
TestFalse("TypeA should not be compatible with TypeC", UDoorType::AreCompatible(TypeA.Get(), TypeC.Get()));
|
||||
TestFalse("TypeA should not be compatible with Null", UDoorType::AreCompatible(TypeA.Get(), nullptr));
|
||||
|
||||
TestTrue("TypeB should be compatible with TypeA", UDoorType::AreCompatible(TypeB.Get(), TypeA.Get()));
|
||||
TestFalse("TypeB should not be compatible with itself", UDoorType::AreCompatible(TypeB.Get(), TypeB.Get()));
|
||||
TestFalse("TypeB should not be compatible with TypeC", UDoorType::AreCompatible(TypeB.Get(), TypeC.Get()));
|
||||
TestFalse("TypeB should not be compatible with Null", UDoorType::AreCompatible(TypeB.Get(), nullptr));
|
||||
|
||||
TestFalse("TypeC should not be compatible with TypeA", UDoorType::AreCompatible(TypeC.Get(), TypeA.Get()));
|
||||
TestFalse("TypeC should not be compatible with TypeB", UDoorType::AreCompatible(TypeC.Get(), TypeB.Get()));
|
||||
TestTrue("TypeC should be compatible with TypeC", UDoorType::AreCompatible(TypeC.Get(), TypeC.Get()));
|
||||
TestFalse("TypeC should not be compatible with Null", UDoorType::AreCompatible(TypeC.Get(), nullptr));
|
||||
|
||||
TestFalse("Null should not be compatible with TypeA", UDoorType::AreCompatible(nullptr, TypeA.Get()));
|
||||
TestFalse("Null should not be compatible with TypeB", UDoorType::AreCompatible(nullptr, TypeB.Get()));
|
||||
TestFalse("Null should not be compatible with TypeC", UDoorType::AreCompatible(nullptr, TypeC.Get()));
|
||||
TestTrue("Null should be compatible with Null", UDoorType::AreCompatible(nullptr, nullptr));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "DungeonGraph.h"
|
||||
#include "Room.h"
|
||||
#include "RoomData.h"
|
||||
#include "TestUtils.h"
|
||||
#include "./Classes/RoomConstraintChildClasses.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonGraphTest, "ProceduralDungeon.Types.DungeonGraph", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
#define INIT_TEST(Graph) \
|
||||
TStrongObjectPtr<UDungeonGraph> Graph(NewObject<UDungeonGraph>(GetTransientPackage(), #Graph));
|
||||
|
||||
#define CLEAN_TEST() \
|
||||
Graph->Clear();
|
||||
|
||||
// Utility to create and initialize a room
|
||||
#define CREATE_ROOM(Name, RoomDataPtr) \
|
||||
URoom* Name = NewObject<URoom>(); \
|
||||
Name->Init(RoomDataPtr.Get(), nullptr, Graph->Count()); \
|
||||
Graph->AddRoom(Name);
|
||||
|
||||
// Utility to create room data
|
||||
#define CREATE_ROOM_DATA(Data) \
|
||||
CREATE_DATA_ASSET(URoomData, Data); \
|
||||
Data->Doors.Empty();
|
||||
|
||||
// Utility to create a non-empty path
|
||||
#define DUMMY_PATH(Path) \
|
||||
Path.Empty(); \
|
||||
Path.Add(nullptr);
|
||||
|
||||
#pragma optimize("", off)
|
||||
bool FDungeonGraphTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
{
|
||||
// Creating data assets
|
||||
CREATE_ROOM_DATA(DA_A);
|
||||
CREATE_ROOM_DATA(DA_B);
|
||||
CREATE_ROOM_DATA(DA_C);
|
||||
CREATE_ROOM_DATA(DA_D);
|
||||
|
||||
DA_A->Doors.Add({{0, 0, 0}, EDoorDirection::South});
|
||||
|
||||
DA_B->Doors.Add({{0, 0, 0}, EDoorDirection::East});
|
||||
DA_B->Doors.Add({{0, 0, 0}, EDoorDirection::West});
|
||||
|
||||
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::North});
|
||||
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::South});
|
||||
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::East});
|
||||
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::West});
|
||||
|
||||
DA_D->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 1, 2});
|
||||
DA_D->Doors.Add({{0, 0, 0}, EDoorDirection::North});
|
||||
DA_D->Doors.Add({{0, 0, 1}, EDoorDirection::North});
|
||||
|
||||
// Test pathfind
|
||||
{
|
||||
INIT_TEST(Graph);
|
||||
|
||||
// (Rooms are numbered from left to right and top to bottom starting at 0)
|
||||
// A--C-C-!C-C-B
|
||||
// | | |
|
||||
// A-!B B--B-C
|
||||
|
||||
// first line
|
||||
CREATE_ROOM(Room0, DA_A);
|
||||
CREATE_ROOM(Room1, DA_C);
|
||||
CREATE_ROOM(Room2, DA_C);
|
||||
CREATE_ROOM(Room3, DA_C);
|
||||
CREATE_ROOM(Room4, DA_C);
|
||||
CREATE_ROOM(Room5, DA_B);
|
||||
|
||||
// second line
|
||||
CREATE_ROOM(Room6, DA_A);
|
||||
CREATE_ROOM(Room7, DA_B);
|
||||
CREATE_ROOM(Room8, DA_B);
|
||||
CREATE_ROOM(Room9, DA_B);
|
||||
CREATE_ROOM(Room10, DA_C);
|
||||
|
||||
Room3->Lock(true);
|
||||
Room7->Lock(true);
|
||||
|
||||
// first line
|
||||
Graph->Connect(Room0, 0, Room1, 1);
|
||||
Graph->Connect(Room1, 0, Room2, 1);
|
||||
Graph->Connect(Room2, 0, Room3, 1);
|
||||
Graph->Connect(Room3, 0, Room4, 1);
|
||||
Graph->Connect(Room4, 0, Room5, 1);
|
||||
|
||||
// second line
|
||||
Graph->Connect(Room6, 0, Room7, 0);
|
||||
Graph->Connect(Room8, 0, Room9, 1);
|
||||
Graph->Connect(Room9, 0, Room10, 0);
|
||||
|
||||
// transversal
|
||||
Graph->Connect(Room1, 2, Room7, 1);
|
||||
Graph->Connect(Room2, 2, Room8, 1);
|
||||
Graph->Connect(Room4, 2, Room10, 1);
|
||||
|
||||
// Used to test path output for some scenarios
|
||||
TArray<const URoom*> Path;
|
||||
|
||||
// Check graph state
|
||||
TestEqual(TEXT("Graph should have 11 rooms"), 11, Graph->Count());
|
||||
TestEqual(TEXT("Graph should have 2 rooms from data DA_A"), 2, Graph->CountRoomData(DA_A.Get()));
|
||||
TestEqual(TEXT("Graph should have 4 rooms from data DA_B"), 4, Graph->CountRoomData(DA_B.Get()));
|
||||
TestEqual(TEXT("Graph should have 5 rooms from data DA_C"), 5, Graph->CountRoomData(DA_C.Get()));
|
||||
|
||||
// Check path find with null rooms
|
||||
DUMMY_PATH(Path);
|
||||
TestFalse(TEXT("Path should not be found when both rooms are null"), UDungeonGraph::FindPath(nullptr, nullptr, &Path));
|
||||
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
|
||||
DUMMY_PATH(Path);
|
||||
TestFalse(TEXT("Path should not be found when first room is null"), UDungeonGraph::FindPath(nullptr, Room1, &Path));
|
||||
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
|
||||
DUMMY_PATH(Path);
|
||||
TestFalse(TEXT("Path should not be found when second room is null"), UDungeonGraph::FindPath(Room0, nullptr, &Path));
|
||||
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
|
||||
|
||||
// Check path found with same room
|
||||
TestTrue(TEXT("Path should be found when both rooms are same"), UDungeonGraph::FindPath(Room0, Room0, &Path));
|
||||
TestEqual(TEXT("Path should have 1 room"), Path.Num(), 1);
|
||||
TestTrue(TEXT("Path should have Room0"), Path[0] == Room0);
|
||||
TestTrue(TEXT("Path should be found when both rooms are same (even if locked)"), UDungeonGraph::FindPath(Room3, Room3, &Path));
|
||||
TestEqual(TEXT("Path should have 1 room"), Path.Num(), 1);
|
||||
TestTrue(TEXT("Path should have Room3"), Path[0] == Room3);
|
||||
|
||||
// Check no path when one or both rooms are locked
|
||||
DUMMY_PATH(Path);
|
||||
TestFalse(TEXT("No path should be found between Room0 and Room3"), UDungeonGraph::FindPath(Room0, Room3, &Path));
|
||||
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
|
||||
DUMMY_PATH(Path);
|
||||
TestFalse(TEXT("No path should be found between Room3 and Room0"), UDungeonGraph::FindPath(Room3, Room0, &Path));
|
||||
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
|
||||
DUMMY_PATH(Path);
|
||||
TestFalse(TEXT("No path should be found between Room3 and Room7"), UDungeonGraph::FindPath(Room3, Room7, &Path));
|
||||
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
|
||||
|
||||
// output path has rooms in the correct order
|
||||
TestTrue(TEXT("Path should be found between Room0 and Room5"), UDungeonGraph::FindPath(Room0, Room5, &Path));
|
||||
TestEqual(TEXT("Path should have 8 rooms"), Path.Num(), 8);
|
||||
TestTrue(TEXT("Path should not go through Room3"), !Path.Contains(Room3));
|
||||
TestEqual(TEXT("Path's 1st room should be Room0"), Path[0], (const URoom*)Room0);
|
||||
TestTrue(TEXT("Path's 2nd room should be Room1"), Path[1] == Room1);
|
||||
TestTrue(TEXT("Path's 3rd room should be Room2"), Path[2] == Room2);
|
||||
TestTrue(TEXT("Path's 4th room should be Room8"), Path[3] == Room8);
|
||||
TestTrue(TEXT("Path's 5th room should be Room9"), Path[4] == Room9);
|
||||
TestTrue(TEXT("Path's 6th room should be Room10"), Path[5] == Room10);
|
||||
TestTrue(TEXT("Path's 7th room should be Room4"), Path[6] == Room4);
|
||||
TestTrue(TEXT("Path's 8th room should be Room5"), Path[7] == Room5);
|
||||
|
||||
// Check pathfind through locked door (not first nor last)
|
||||
DUMMY_PATH(Path);
|
||||
TestFalse(TEXT("No path should be found between Room0 and Room6"), UDungeonGraph::FindPath(Room0, Room6, &Path));
|
||||
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
|
||||
TestTrue(TEXT("Path should be found between Room0 and Room6 (when locked rooms allowed)"), UDungeonGraph::FindPath(Room0, Room6, nullptr, /*IgnoreLocked = */ true));
|
||||
|
||||
CLEAN_TEST();
|
||||
}
|
||||
|
||||
// Test Voxel Bounds Conversions
|
||||
{
|
||||
INIT_TEST(Graph);
|
||||
|
||||
// first floor second floor
|
||||
// C - B - A C - C
|
||||
// | |
|
||||
// D D
|
||||
|
||||
CREATE_ROOM(Room0, DA_C);
|
||||
CREATE_ROOM(Room1, DA_B);
|
||||
CREATE_ROOM(Room2, DA_A);
|
||||
CREATE_ROOM(Room3, DA_D);
|
||||
CREATE_ROOM(Room4, DA_C);
|
||||
CREATE_ROOM(Room5, DA_C);
|
||||
|
||||
Room1->Position = {0, 1, 0};
|
||||
Room2->Position = {0, 2, 0};
|
||||
Room2->Direction = EDoorDirection::East;
|
||||
Room3->Position = {-1, 0, 0};
|
||||
Room4->Position = {0, 0, 1};
|
||||
Room5->Position = {0, 1, 1};
|
||||
|
||||
// Room positions are modified manually, so we need to explicitely rebuild bounds
|
||||
Graph->RebuildBounds();
|
||||
|
||||
// Check Room0 voxel bounds conversion
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({0, 0, 0});
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = Room0->GetVoxelBounds();
|
||||
TestEqual(TEXT("Room0 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
|
||||
// Check Room1 voxel bounds conversion
|
||||
{
|
||||
FVoxelBounds ExceptedBounds;
|
||||
ExceptedBounds.AddCell({0, 1, 0});
|
||||
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = Room1->GetVoxelBounds();
|
||||
TestEqual(TEXT("Room1 bounds should be as expected"), ConvertedBounds, ExceptedBounds);
|
||||
}
|
||||
|
||||
// Check Room2 voxel bounds conversion
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({0, 2, 0});
|
||||
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = Room2->GetVoxelBounds();
|
||||
TestEqual(TEXT("Room2 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
|
||||
// Check Room3 voxel bounds conversion
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({-1, 0, 0});
|
||||
ExpectedBounds.AddCell({-1, 0, 1});
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = Room3->GetVoxelBounds();
|
||||
TestEqual(TEXT("Room3 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
|
||||
// Check Room4 voxel bounds conversion
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({0, 0, 1});
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = Room4->GetVoxelBounds();
|
||||
TestEqual(TEXT("Room4 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
|
||||
// Check Room5 voxel bounds conversion
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({0, 1, 1});
|
||||
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = Room5->GetVoxelBounds();
|
||||
TestEqual(TEXT("Room5 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
}
|
||||
|
||||
// FilterAndSort Test
|
||||
{
|
||||
INIT_TEST(Graph);
|
||||
|
||||
// first floor second floor
|
||||
// C - B - A C - C
|
||||
// | |
|
||||
// D D
|
||||
|
||||
CREATE_DATA_ASSET(UConstraintPass, Pass);
|
||||
CREATE_DATA_ASSET(UConstraintFail, Fail);
|
||||
|
||||
CREATE_ROOM_DATA(DA_E);
|
||||
DA_E->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 2, 1});
|
||||
DA_E->Doors.Add({{0, 1, 0}, EDoorDirection::North});
|
||||
DA_E->Constraints.Add(Pass.Get());
|
||||
|
||||
// Same as DA_E but constraint fail
|
||||
CREATE_ROOM_DATA(DA_F);
|
||||
DA_F->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 2, 1});
|
||||
DA_F->Doors.Add({{0, 1, 0}, EDoorDirection::North});
|
||||
DA_F->Constraints.Add(Fail.Get());
|
||||
|
||||
CREATE_ROOM(Room0, DA_C);
|
||||
CREATE_ROOM(Room1, DA_B);
|
||||
CREATE_ROOM(Room2, DA_A);
|
||||
CREATE_ROOM(Room3, DA_D);
|
||||
CREATE_ROOM(Room4, DA_C);
|
||||
CREATE_ROOM(Room5, DA_C);
|
||||
|
||||
Room1->Position = {0, 1, 0};
|
||||
Room2->Position = {0, 2, 0};
|
||||
Room2->Direction = EDoorDirection::East;
|
||||
Room3->Position = {-1, 0, 0};
|
||||
Room4->Position = {0, 0, 1};
|
||||
Room5->Position = {0, 1, 1};
|
||||
|
||||
// Room positions are modified manually, so we need to explicitely rebuild bounds
|
||||
Graph->RebuildBounds();
|
||||
|
||||
const TArray<URoomData*> RoomList = {DA_A.Get(), DA_D.Get(), DA_E.Get(), DA_F.Get()};
|
||||
TArray<FRoomCandidate> SortedRooms;
|
||||
|
||||
{
|
||||
FDoorDef FromDoor = {{0, 0, 0}, EDoorDirection::North};
|
||||
bool bHasCandidates = Graph->FilterAndSortRooms(RoomList, FromDoor, SortedRooms);
|
||||
TestTrue(TEXT("There should be candidates"), bHasCandidates);
|
||||
TestEqual(TEXT("There should be 4 candidates"), SortedRooms.Num(), 4);
|
||||
TestEqual(TEXT("RoomData D should be the best candidate"), SortedRooms[0].Data, DA_D.Get());
|
||||
TestEqual(TEXT("RoomData D Door index 0 should be the best candidate"), SortedRooms[0].DoorIndex, 0);
|
||||
TestEqual(TEXT("RoomData A should be the worst candidate"), SortedRooms[3].Data, DA_A.Get());
|
||||
|
||||
bool bContainsE = false;
|
||||
bool bContainsF = false;
|
||||
for (const FRoomCandidate& Candidate : SortedRooms)
|
||||
{
|
||||
bContainsE |= (Candidate.Data == DA_E.Get());
|
||||
bContainsF |= (Candidate.Data == DA_F.Get());
|
||||
}
|
||||
|
||||
// DA_E and DA_F are the same, but DA_F has a failing constraint
|
||||
TestTrue(TEXT("RoomData E should be a valid candidate"), bContainsE);
|
||||
TestFalse(TEXT("RoomData F should not be a valid candidate"), bContainsF);
|
||||
}
|
||||
|
||||
{
|
||||
FDoorDef FromDoor = {{0, 1, 1}, EDoorDirection::South};
|
||||
bool bHasCandidates = Graph->FilterAndSortRooms(RoomList, FromDoor, SortedRooms);
|
||||
TestTrue(TEXT("There should be candidates"), bHasCandidates);
|
||||
TestEqual(TEXT("There should be 3 candidates"), SortedRooms.Num(), 3);
|
||||
TestEqual(TEXT("RoomData D should be the best candidate"), SortedRooms[0].Data, DA_D.Get());
|
||||
TestEqual(TEXT("RoomData D DoorIndex 0 should be the best candidate"), SortedRooms[0].DoorIndex, 0);
|
||||
TestEqual(TEXT("RoomData D should be the worst candidate"), SortedRooms[2].Data, DA_D.Get());
|
||||
TestEqual(TEXT("RoomData D DoorIndex 1 should be the worst candidate"), SortedRooms[2].DoorIndex, 1);
|
||||
}
|
||||
|
||||
CLEAN_TEST();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#pragma optimize("", on)
|
||||
|
||||
#undef INIT_TEST
|
||||
#undef CLEAN_TEST
|
||||
#undef CREATE_ROOM
|
||||
#undef CREATE_ROOM_DATA
|
||||
#undef DUMMY_PATH
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "Utils/DungeonSaveUtils.h"
|
||||
#include "Classes/DungeonSaveClasses.h"
|
||||
#include "TestUtils.h"
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
DEFINE_SPEC(FDungeonSaveSpecs, "ProceduralDungeon.SaveLoad", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
void FDungeonSaveSpecs::Define()
|
||||
{
|
||||
Describe(TEXT("Interfaces"), [this]()
|
||||
{
|
||||
It(TEXT("DungeonSaveInterface"), [this]()
|
||||
{
|
||||
TStrongObjectPtr<USaveTestObject> TestSave(NewObject<USaveTestObject>(GetTransientPackage()));
|
||||
TStrongObjectPtr<USaveTestObject> TestLoad(NewObject<USaveTestObject>(GetTransientPackage()));
|
||||
|
||||
TestSave->TestSaveGameFlag = 5;
|
||||
TestSave->TestSerializeObjectFunction = 8;
|
||||
|
||||
TArray<uint8> SavedData {};
|
||||
SerializeUObject(SavedData, TestSave.Get(), false);
|
||||
|
||||
TestEqual(TEXT("should have correct order of execution"), TestSave->OrderOfExecution, FString(TEXT("BCD")));
|
||||
TestTrue(TEXT("should have written data"), SavedData.Num() > 0);
|
||||
|
||||
SerializeUObject(SavedData, TestLoad.Get(), true);
|
||||
|
||||
TestEqual(TEXT("should have correct order of execution"), TestLoad->OrderOfExecution, FString(TEXT("WXY")));
|
||||
TestEqual(TEXT("should have retrieved data from SaveGame flag"), TestLoad->TestSaveGameFlag, TestSave->TestSaveGameFlag);
|
||||
TestEqual(TEXT("should have retrieved data from SerializedObject function"), TestLoad->TestSerializeObjectFunction, TestSave->TestSerializeObjectFunction);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonUtilsTest_WeightedMap, "ProceduralDungeon.Utils.WeightedMaps", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
|
||||
|
||||
bool FDungeonUtilsTest_WeightedMap::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Built-in types test
|
||||
{
|
||||
TMap<int, int> WeightedMap = {
|
||||
{1, 0}, // Weight with 0 should never be returned
|
||||
{2, 1}, // The first non-zero weight should be return for index 0
|
||||
{3, 2}, // Weights greater than 1 should be returned for as much indices
|
||||
{4, 1} // The last one should be return when index == total weights minus one
|
||||
}; // Out of bounds index should return default value
|
||||
|
||||
TestEqual(TEXT("Total Weights"), Dungeon::GetTotalWeight(WeightedMap), 4);
|
||||
TestEqual(TEXT("Weighted value at -1"), Dungeon::GetWeightedAt(WeightedMap, -1), 0); // negative index should return default value
|
||||
TestEqual(TEXT("Weighted value at 0"), Dungeon::GetWeightedAt(WeightedMap, 0), 2);
|
||||
TestEqual(TEXT("Weighted value at 1"), Dungeon::GetWeightedAt(WeightedMap, 1), 3);
|
||||
TestEqual(TEXT("Weighted value at 2"), Dungeon::GetWeightedAt(WeightedMap, 2), 3);
|
||||
TestEqual(TEXT("Weighted value at 3"), Dungeon::GetWeightedAt(WeightedMap, 3), 4);
|
||||
TestEqual(TEXT("Weighted value at 4"), Dungeon::GetWeightedAt(WeightedMap, 4), 0); // default int is 0
|
||||
}
|
||||
|
||||
// Pointer test
|
||||
{
|
||||
int a = 1;
|
||||
int b = 2;
|
||||
int c = 3;
|
||||
int d = 4;
|
||||
|
||||
TMap<int*, int> WeightedMap = {
|
||||
{&a, 2},
|
||||
{&b, 0}, // Weight with 0 in middle should be skipped
|
||||
{&c, 1},
|
||||
{&d, 0} // The last one should not be returned if weight is 0
|
||||
};
|
||||
|
||||
TestEqual(TEXT("Total Weights"), Dungeon::GetTotalWeight(WeightedMap), 3);
|
||||
TestEqual(TEXT("Weighted pointer at -1"), Dungeon::GetWeightedAt(WeightedMap, -1), (int*)nullptr);
|
||||
TestEqual(TEXT("Weighted pointer at 0"), Dungeon::GetWeightedAt(WeightedMap, 0), &a);
|
||||
TestEqual(TEXT("Weighted pointer at 1"), Dungeon::GetWeightedAt(WeightedMap, 1), &a);
|
||||
TestEqual(TEXT("Weighted pointer at 2"), Dungeon::GetWeightedAt(WeightedMap, 2), &c);
|
||||
TestEqual(TEXT("Weighted pointer at 3"), Dungeon::GetWeightedAt(WeightedMap, 3), (int*)nullptr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonUtilsTest_Guid2Seed, "ProceduralDungeon.Utils.Guid2Seed.Simple", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
|
||||
|
||||
bool FDungeonUtilsTest_Guid2Seed::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Guid to Random Seed Tests
|
||||
{
|
||||
FGuid Guid1(0x12345678U, 0x9ABCDEF0U, 0x0FEDCBA9U, 0x87654321U);
|
||||
FGuid Guid2(0x08F7E6D5U, 0xC4B3A291U, 0x192A3B4CU, 0x5D6E7F80U);
|
||||
int64 Salt1 = 1;
|
||||
int64 Salt2 = 2;
|
||||
|
||||
TestTrue(TEXT("Same Guid and Salt should return same Seed (1)"), Random::Guid2Seed(Guid1, Salt1) == Random::Guid2Seed(Guid1, Salt1));
|
||||
TestTrue(TEXT("Same Guid and Salt should return same Seed (2)"), Random::Guid2Seed(Guid2, Salt2) == Random::Guid2Seed(Guid2, Salt2));
|
||||
TestTrue(TEXT("Same Guid but different Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid1, Salt2));
|
||||
TestTrue(TEXT("Same Guid but different Salt should return different Seeds (2)"), Random::Guid2Seed(Guid2, Salt1) != Random::Guid2Seed(Guid2, Salt2));
|
||||
TestTrue(TEXT("Different Guid but same Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid2, Salt1));
|
||||
TestTrue(TEXT("Different Guid but same Salt should return different Seeds (2)"), Random::Guid2Seed(Guid1, Salt2) != Random::Guid2Seed(Guid2, Salt2));
|
||||
TestTrue(TEXT("Different Guid and Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid2, Salt2));
|
||||
TestTrue(TEXT("Different Guid and Salt should return different Seeds (2)"), Random::Guid2Seed(Guid1, Salt2) != Random::Guid2Seed(Guid2, Salt1));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
BEGIN_DEFINE_SPEC(FGuid2SeedStatisticalTests, "ProceduralDungeon.Utils.Guid2Seed.Stats", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
|
||||
struct FTestParams
|
||||
{
|
||||
FGuid Guid;
|
||||
int32 NbElements;
|
||||
int32 NbSamples;
|
||||
double CriticalValue;
|
||||
};
|
||||
|
||||
using SampleArrayType = int32[];
|
||||
|
||||
static TUniquePtr<SampleArrayType> GenerateSamples(const FTestParams& Params)
|
||||
{
|
||||
TUniquePtr<SampleArrayType> Occurences(new int32[Params.NbElements] {0});
|
||||
for (int32 i = 0; i < Params.NbSamples; ++i)
|
||||
{
|
||||
// Create a random stream with only the salt modified.
|
||||
// This will simulate an actor in a room level generating its first random number
|
||||
// in each of the room instances (where only the room ID changes)
|
||||
FRandomStream RNG(Random::Guid2Seed(Params.Guid, i));
|
||||
Occurences[RNG.RandRange(0, Params.NbElements - 1)]++;
|
||||
}
|
||||
return Occurences;
|
||||
}
|
||||
|
||||
static bool PassChiSquaredTest(const FTestParams& Params, SampleArrayType Samples)
|
||||
{
|
||||
const double Expected = static_cast<double>(Params.NbSamples) / Params.NbElements;
|
||||
double X2 = 0;
|
||||
for (int i = 0; i < Params.NbElements; ++i)
|
||||
{
|
||||
const double Delta = Samples[i] - Expected;
|
||||
X2 += (Delta * Delta) / Expected;
|
||||
}
|
||||
//UE_LOG(LogTemp, Warning, TEXT("Chi Squared result %d: %f / %f"), Params.NbElements, X2, Params.CriticalValue);
|
||||
return X2 < Params.CriticalValue;
|
||||
}
|
||||
END_DEFINE_SPEC(FGuid2SeedStatisticalTests)
|
||||
|
||||
void FGuid2SeedStatisticalTests::Define()
|
||||
{
|
||||
Describe(TEXT("Chi Squared Test"), [this]()
|
||||
{
|
||||
FTestParams Params;
|
||||
Params.NbSamples = 1'000'000'000;
|
||||
|
||||
TArray<FGuid> GuidsToTest = {
|
||||
FGuid(),
|
||||
FGuid(0x12345678U, 0x9ABCDEF0U, 0x0FEDCBA9U, 0x87654321U),
|
||||
FGuid(0x08F7E6D5U, 0xC4B3A291U, 0x192A3B4CU, 0x5D6E7F80U),
|
||||
};
|
||||
|
||||
for (const auto& Guid : GuidsToTest)
|
||||
{
|
||||
Params.Guid = Guid;
|
||||
Describe(FString::Printf(TEXT("with Guid %s"), *Guid.ToString()), [this, Params]() mutable
|
||||
{
|
||||
// The test cases we want to check for each Guid.
|
||||
// First value is the number of elements in the generated samples.
|
||||
// Second value is the critical value for the Chi Squared test (with p-value of 5%)
|
||||
TArray<TTuple<int32, double>> TestCases;
|
||||
TestCases.Add(TTuple<int32, double>(10, 16.91898));
|
||||
TestCases.Add(TTuple<int32, double>(100, 123.22522));
|
||||
TestCases.Add(TTuple<int32, double>(1000, 1073.64265));
|
||||
|
||||
for (const auto& TestCase : TestCases)
|
||||
{
|
||||
Params.NbElements = TestCase.Key;
|
||||
Params.CriticalValue = TestCase.Value;
|
||||
It(FString::Printf(TEXT("with %d elements"), Params.NbElements), [this, Params]()
|
||||
{
|
||||
TUniquePtr<SampleArrayType> Samples = GenerateSamples(Params);
|
||||
TestTrue(TEXT("Pass"), PassChiSquaredTest(Params, Samples.Get()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,448 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "RoomData.h"
|
||||
#include "DoorType.h"
|
||||
#include "./Classes/RoomCustomDataChildClasses.h"
|
||||
#include "./Classes/RoomConstraintChildClasses.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "TestUtils.h"
|
||||
#include "VoxelBounds/VoxelBounds.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FRoomDataTests, "ProceduralDungeon.Types.RoomData", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
// Utility to create room data
|
||||
#define CREATE_ROOM_DATA(Data) \
|
||||
CREATE_DATA_ASSET(URoomData, Data); \
|
||||
Data->Doors.Empty();
|
||||
|
||||
#define ADD_DOOR(ROOM, DOOR_POS, DOOR_DIR, DOOR_TYPE) \
|
||||
{ \
|
||||
FDoorDef Door; \
|
||||
Door.Position = DOOR_POS; \
|
||||
Door.Direction = DOOR_DIR; \
|
||||
Door.Type = DOOR_TYPE; \
|
||||
ROOM->Doors.Add(Door); \
|
||||
}
|
||||
|
||||
bool FRoomDataTests::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Test IsRoomInBounds
|
||||
{
|
||||
// Creating this room data (X is the room origin, v is the door), thus we could test rotated bounds:
|
||||
// 1 +---+---+---+
|
||||
// | | X | |
|
||||
// 0 +---+---+---+
|
||||
// | | | |
|
||||
// -1 +---+---+---+
|
||||
// | | | |
|
||||
// -2 +---+-v-+---+
|
||||
// -1 0 1 2
|
||||
CREATE_ROOM_DATA(RoomData);
|
||||
RoomData->BoundingBoxes[0].SetMinAndMax(FIntVector(-2, -1, -1), FIntVector(1, 2, 2));
|
||||
|
||||
FDoorDef Door;
|
||||
Door.Position = FIntVector(-2, 0, 0);
|
||||
Door.Direction = EDoorDirection::South;
|
||||
RoomData->Doors.Add(Door);
|
||||
|
||||
// If we want to limit the dungeon cells from -2 to 2,
|
||||
// we need to create a box from -2 to 3 (see below).
|
||||
// +---+---+---+---+---+
|
||||
// |-2 |-1 | 0 | 1 | 2 |
|
||||
// +---+---+---+---+---+
|
||||
// -2 -1 0 1 2 3
|
||||
FBoxMinAndMax DungeonBounds({-1000, -2, -1000}, {1000, 3, 1000});
|
||||
|
||||
FBoxMinAndMax RoomBoundsAtDoorLocation = RoomData->GetIntBounds() - Door.Position;
|
||||
|
||||
// Rotated to South
|
||||
{
|
||||
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::North);
|
||||
TestEqual(TEXT("[S] Rotated Room Bounds: ((0, -1, -1), (3, 2, 2))"), RotatedRoomBounds, FBoxMinAndMax({0, -1, -1}, {3, 2, 2}));
|
||||
|
||||
Door.Direction = EDoorDirection::South;
|
||||
Door.Position = {0, 0, 0};
|
||||
TestTrue(TEXT("[S] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Positive Y
|
||||
|
||||
Door.Position = {0, 1, 0};
|
||||
TestTrue(TEXT("[S] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 2, 0};
|
||||
TestFalse(TEXT("[S] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 4, 0};
|
||||
TestFalse(TEXT("[S] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Negative Y
|
||||
|
||||
Door.Position = {0, -1, 0};
|
||||
TestTrue(TEXT("[S] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -2, 0};
|
||||
TestFalse(TEXT("[S] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -4, 0};
|
||||
TestFalse(TEXT("[S] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
}
|
||||
|
||||
// Rotated to East
|
||||
{
|
||||
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::West);
|
||||
TestEqual(TEXT("[E] Rotated Room Bounds: ((-1, -2, -1), (2, 1, 2))"), RotatedRoomBounds, FBoxMinAndMax({-1, -2, -1}, {2, 1, 2}));
|
||||
|
||||
Door.Direction = EDoorDirection::East;
|
||||
Door.Position = {0, 1, 0};
|
||||
TestTrue(TEXT("[E] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Positive Y
|
||||
|
||||
Door.Position = {0, 2, 0};
|
||||
TestTrue(TEXT("[E] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 3, 0};
|
||||
TestFalse(TEXT("[E] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 5, 0};
|
||||
TestFalse(TEXT("[E] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Negative Y
|
||||
|
||||
Door.Position = {0, 0, 0};
|
||||
TestTrue(TEXT("[E] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -1, 0};
|
||||
TestFalse(TEXT("[E] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -3, 0};
|
||||
TestFalse(TEXT("[E] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
}
|
||||
|
||||
// Rotated to West
|
||||
{
|
||||
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::East);
|
||||
TestEqual(TEXT("[W] Rotated Room Bounds: ((-1, 0, -1), (2, 3, 2))"), RotatedRoomBounds, FBoxMinAndMax({-1, 0, -1}, {2, 3, 2}));
|
||||
|
||||
Door.Direction = EDoorDirection::West;
|
||||
Door.Position = {0, -1, 0};
|
||||
TestTrue(TEXT("[W] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Positive Y
|
||||
|
||||
Door.Position = {0, 0, 0};
|
||||
TestTrue(TEXT("[W] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 1, 0};
|
||||
TestFalse(TEXT("[W] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 3, 0};
|
||||
TestFalse(TEXT("[W] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Negative Y
|
||||
|
||||
Door.Position = {0, -2, 0};
|
||||
TestTrue(TEXT("[W] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -3, 0};
|
||||
TestFalse(TEXT("[W] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -5, 0};
|
||||
TestFalse(TEXT("[W] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
}
|
||||
|
||||
// Rotated to North
|
||||
{
|
||||
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::South);
|
||||
TestEqual(TEXT("[N] Rotated Room Bounds: ((-2, -1, -1), (1, 2, 2))"), RotatedRoomBounds, FBoxMinAndMax({-2, -1, -1}, {1, 2, 2}));
|
||||
|
||||
Door.Direction = EDoorDirection::North;
|
||||
Door.Position = {0, 0, 0};
|
||||
TestTrue(TEXT("[N] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Positive Y
|
||||
|
||||
Door.Position = {0, 1, 0};
|
||||
TestTrue(TEXT("[N] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 2, 0};
|
||||
TestFalse(TEXT("[N] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, 4, 0};
|
||||
TestFalse(TEXT("[N] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
// Negative Y
|
||||
|
||||
Door.Position = {0, -1, 0};
|
||||
TestTrue(TEXT("[N] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -2, 0};
|
||||
TestFalse(TEXT("[N] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
|
||||
Door.Position = {0, -4, 0};
|
||||
TestFalse(TEXT("[N] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
|
||||
}
|
||||
}
|
||||
|
||||
// Test HasDoorOfType and variants
|
||||
{
|
||||
CREATE_ROOM_DATA(RoomData);
|
||||
|
||||
CREATE_DATA_ASSET(UDoorType, DoorA);
|
||||
CREATE_DATA_ASSET(UDoorType, DoorB);
|
||||
CREATE_DATA_ASSET(UDoorType, DoorC);
|
||||
CREATE_DATA_ASSET(UDoorType, DoorD);
|
||||
CREATE_DATA_ASSET(UDoorType, DoorE);
|
||||
|
||||
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::North, DoorA.Get());
|
||||
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::South, DoorB.Get());
|
||||
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::East, DoorC.Get());
|
||||
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::West, DoorD.Get());
|
||||
|
||||
// HasDoorOfType
|
||||
{
|
||||
TestTrue("HasDoorOfType(DoorA)", RoomData->HasDoorOfType(DoorA.Get()));
|
||||
TestTrue("HasDoorOfType(DoorB)", RoomData->HasDoorOfType(DoorB.Get()));
|
||||
TestTrue("HasDoorOfType(DoorC)", RoomData->HasDoorOfType(DoorC.Get()));
|
||||
TestTrue("HasDoorOfType(DoorD)", RoomData->HasDoorOfType(DoorD.Get()));
|
||||
TestFalse("HasDoorOfType(DoorE)", RoomData->HasDoorOfType(DoorE.Get()));
|
||||
TestFalse("HasDoorOfType(nullptr)", RoomData->HasDoorOfType(nullptr));
|
||||
}
|
||||
|
||||
// HasAnyDoorOfType
|
||||
{
|
||||
TestTrue("HasAnyDoorOfType({DoorA, DoorB, DoorE, nullptr})", RoomData->HasAnyDoorOfType({DoorA.Get(), DoorB.Get(), DoorE.Get(), nullptr}));
|
||||
TestTrue("HasAnyDoorOfType({DoorC, DoorD})", RoomData->HasAnyDoorOfType({DoorC.Get(), DoorD.Get()}));
|
||||
TestFalse("HasAnyDoorOfType({DoorE, nullptr})", RoomData->HasAnyDoorOfType({DoorE.Get(), nullptr}));
|
||||
TestFalse("HasAnyDoorOfType({})", RoomData->HasAnyDoorOfType({}));
|
||||
}
|
||||
|
||||
// HasAllDoorOfType
|
||||
{
|
||||
TestFalse("HasAllDoorOfType({DoorA, DoorB, DoorE, nullptr})", RoomData->HasAllDoorOfType({DoorA.Get(), DoorB.Get(), DoorE.Get(), nullptr}));
|
||||
TestTrue("HasAllDoorOfType({DoorC, DoorD})", RoomData->HasAllDoorOfType({DoorC.Get(), DoorD.Get()}));
|
||||
TestFalse("HasAllDoorOfType({DoorE, nullptr})", RoomData->HasAllDoorOfType({DoorE.Get(), nullptr}));
|
||||
TestTrue("HasAllDoorOfType({})", RoomData->HasAllDoorOfType({}));
|
||||
}
|
||||
}
|
||||
|
||||
// Test HasCustomData and variants
|
||||
{
|
||||
CREATE_ROOM_DATA(RoomData);
|
||||
|
||||
RoomData->CustomData.Add(UCustomDataA::StaticClass());
|
||||
RoomData->CustomData.Add(UCustomDataB::StaticClass());
|
||||
|
||||
// HasCustomData
|
||||
{
|
||||
TestTrue("HasCustomData(CustomDataA)", RoomData->HasCustomData(UCustomDataA::StaticClass()));
|
||||
TestTrue("HasCustomData(CustomDataB)", RoomData->HasCustomData(UCustomDataB::StaticClass()));
|
||||
TestFalse("HasCustomData(CustomDataC)", RoomData->HasCustomData(UCustomDataC::StaticClass()));
|
||||
TestFalse("HasCustomData(nullptr)", RoomData->HasCustomData(nullptr));
|
||||
}
|
||||
|
||||
// HasAnyCustomData
|
||||
{
|
||||
TestTrue("HasAnyCustomData({CustomDataA, CustomDataB})", RoomData->HasAnyCustomData({UCustomDataA::StaticClass(), UCustomDataB::StaticClass()}));
|
||||
TestTrue("HasAnyCustomData({CustomDataA, CustomDataC})", RoomData->HasAnyCustomData({UCustomDataA::StaticClass(), UCustomDataC::StaticClass()}));
|
||||
TestFalse("HasAnyCustomData({nullptr, CustomDataC})", RoomData->HasAnyCustomData({nullptr, UCustomDataC::StaticClass()}));
|
||||
TestFalse("HasAnyCustomData({})", RoomData->HasAnyCustomData({}));
|
||||
}
|
||||
|
||||
// HasAllCustomData
|
||||
{
|
||||
TestTrue("HasAllCustomData({CustomDataA, CustomDataB})", RoomData->HasAllCustomData({UCustomDataA::StaticClass(), UCustomDataB::StaticClass()}));
|
||||
TestFalse("HasAllCustomData({CustomDataA, CustomDataC})", RoomData->HasAllCustomData({UCustomDataA::StaticClass(), UCustomDataC::StaticClass()}));
|
||||
TestFalse("HasAllCustomData({nullptr, CustomDataC})", RoomData->HasAllCustomData({nullptr, UCustomDataC::StaticClass()}));
|
||||
TestTrue("HasAllCustomData({})", RoomData->HasAllCustomData({}));
|
||||
}
|
||||
}
|
||||
|
||||
// Test Size and Volume
|
||||
{
|
||||
// Should have Size=(1,1,1) and Volume=1
|
||||
CREATE_ROOM_DATA(RoomDataA);
|
||||
RoomDataA->BoundingBoxes[0].SetMinAndMax(FIntVector(0, 1, 0), FIntVector(1, 0, 1));
|
||||
|
||||
// Should have Size=(2,1,1) and Volume=2
|
||||
CREATE_ROOM_DATA(RoomDataB);
|
||||
RoomDataB->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, 0), FIntVector(1, 0, 1));
|
||||
|
||||
// Should have Size=(2,2,1) and Volume=4
|
||||
CREATE_ROOM_DATA(RoomDataC);
|
||||
RoomDataC->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, 0), FIntVector(1, -1, 1));
|
||||
|
||||
// Should have Size=(2,2,2) and Volume=8
|
||||
CREATE_ROOM_DATA(RoomDataD);
|
||||
RoomDataD->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, -1), FIntVector(1, -1, 1));
|
||||
|
||||
// GetSize
|
||||
{
|
||||
TestEqual("RoomDataA->GetSize() == {1,1,1}", RoomDataA->GetSize(), FIntVector {1, 1, 1});
|
||||
TestEqual("RoomDataB->GetSize() == {2,1,1}", RoomDataB->GetSize(), FIntVector {2, 1, 1});
|
||||
TestEqual("RoomDataC->GetSize() == {2,2,1}", RoomDataC->GetSize(), FIntVector {2, 2, 1});
|
||||
TestEqual("RoomDataD->GetSize() == {2,2,2}", RoomDataD->GetSize(), FIntVector {2, 2, 2});
|
||||
}
|
||||
|
||||
// GetVolume
|
||||
{
|
||||
TestEqual("RoomDataA->GetVolume() == 1", RoomDataA->GetVolume(), 1);
|
||||
TestEqual("RoomDataB->GetVolume() == 2", RoomDataB->GetVolume(), 2);
|
||||
TestEqual("RoomDataC->GetVolume() == 4", RoomDataC->GetVolume(), 4);
|
||||
TestEqual("RoomDataD->GetVolume() == 8", RoomDataD->GetVolume(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetVoxelBounds
|
||||
{
|
||||
CREATE_ROOM_DATA(RoomDataA);
|
||||
RoomDataA->Doors.Add({{0, 0, 0}, EDoorDirection::North});
|
||||
|
||||
// Should have one cell at (0,0,0) with a door at (0,0,0)[North]
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({0, 0, 0});
|
||||
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = RoomDataA->GetVoxelBounds();
|
||||
TestEqual("RoomDataA->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataB);
|
||||
RoomDataB->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 0, 0), FIntVector(2, 1, 1));
|
||||
RoomDataB->Doors.Add({{0, 0, 0}, EDoorDirection::West});
|
||||
RoomDataB->Doors.Add({{1, 0, 0}, EDoorDirection::North});
|
||||
|
||||
// Should have 3 cells at (-1,0,0), (0,0,0), (1,0,0) with doors at (0,0,0)[West] and (1,0,0)[North]
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({-1, 0, 0});
|
||||
ExpectedBounds.AddCell({0, 0, 0});
|
||||
ExpectedBounds.AddCell({1, 0, 0});
|
||||
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = RoomDataB->GetVoxelBounds();
|
||||
TestEqual("RoomDataB->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataC);
|
||||
RoomDataC->BoundingBoxes[0].SetMinAndMax(FIntVector(0, 0, -1), FIntVector(1, 1, 2));
|
||||
RoomDataC->Doors.Add({{0, 0, 0}, EDoorDirection::North});
|
||||
RoomDataC->Doors.Add({{0, 0, 1}, EDoorDirection::South});
|
||||
|
||||
// Should have 3 cells at (0,0,-1), (0,0,0), (0,0,1) with doors at (0,0,0)[North] and (0,0,1)[South]
|
||||
{
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell({0, 0, -1});
|
||||
ExpectedBounds.AddCell({0, 0, 0});
|
||||
ExpectedBounds.AddCell({0, 0, 1});
|
||||
|
||||
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
|
||||
|
||||
FVoxelBounds ConvertedBounds = RoomDataC->GetVoxelBounds();
|
||||
TestEqual("RoomDataC->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
|
||||
}
|
||||
}
|
||||
|
||||
// Test Room Constraints
|
||||
{
|
||||
#define CHECK_CONSTRAINTS(DATA) URoomData::DoesPassAllConstraints(nullptr, DATA, FIntVector::ZeroValue, EDoorDirection::North)
|
||||
|
||||
TestFalse("null data should fail.", CHECK_CONSTRAINTS(nullptr));
|
||||
|
||||
CREATE_DATA_ASSET(UConstraintPass, Pass);
|
||||
CREATE_DATA_ASSET(UConstraintFail, Fail);
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataA);
|
||||
TestTrue("No constraint should pass.", CHECK_CONSTRAINTS(RoomDataA.Get()));
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataB);
|
||||
RoomDataB->Constraints.Add(Pass.Get());
|
||||
TestTrue("One passing constraint should pass.", CHECK_CONSTRAINTS(RoomDataB.Get()));
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataC);
|
||||
RoomDataC->Constraints.Add(Fail.Get());
|
||||
TestFalse("One failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataC.Get()));
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataD);
|
||||
RoomDataD->Constraints.Add(Pass.Get());
|
||||
RoomDataD->Constraints.Add(Pass.Get());
|
||||
RoomDataD->Constraints.Add(Pass.Get());
|
||||
TestTrue("All passing constraint should pass.", CHECK_CONSTRAINTS(RoomDataD.Get()));
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataE);
|
||||
RoomDataE->Constraints.Add(Fail.Get());
|
||||
RoomDataE->Constraints.Add(Pass.Get());
|
||||
RoomDataE->Constraints.Add(Pass.Get());
|
||||
TestFalse("First failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataE.Get()));
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataF);
|
||||
RoomDataF->Constraints.Add(Pass.Get());
|
||||
RoomDataF->Constraints.Add(Fail.Get());
|
||||
RoomDataF->Constraints.Add(Pass.Get());
|
||||
TestFalse("Second failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataF.Get()));
|
||||
|
||||
CREATE_ROOM_DATA(RoomDataG);
|
||||
RoomDataG->Constraints.Add(Pass.Get());
|
||||
RoomDataG->Constraints.Add(Pass.Get());
|
||||
RoomDataG->Constraints.Add(Fail.Get());
|
||||
TestFalse("Third failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataG.Get()));
|
||||
|
||||
#undef CHECK_CONSTRAINTS
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef CREATE_DATA_ASSET
|
||||
#undef CREATE_ROOM_DATA
|
||||
#undef ADD_DOOR
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "Misc/EngineVersionComparison.h"
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
|
||||
#if UE_VERSION_OLDER_THAN(5, 5, 0)
|
||||
#define FLAG_APPLICATION_CONTEXT EAutomationTestFlags::ApplicationContextMask
|
||||
#else
|
||||
#define FLAG_APPLICATION_CONTEXT EAutomationTestFlags_ApplicationContextMask
|
||||
#endif
|
||||
|
||||
// Utility to create a data asset
|
||||
#define CREATE_DATA_ASSET(VAR_TYPE, VAR_NAME) \
|
||||
TStrongObjectPtr<VAR_TYPE> VAR_NAME(NewObject<VAR_TYPE>(GetTransientPackage(), #VAR_NAME))
|
||||
+522
@@ -0,0 +1,522 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "VoxelBounds/VoxelBounds.h"
|
||||
#include "Tests/Classes/CustomScoreCallbacks.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FVoxelBoundsTest, "ProceduralDungeon.Types.VoxelBounds", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
|
||||
|
||||
#define SET_CONNECTION(BOUNDS, CELL, DIR, TYPE) \
|
||||
BOUNDS.SetCellConnection(FIntVector CELL, FVoxelBounds::EDirection::DIR, FVoxelBoundsConnection(EVoxelBoundsConnectionType::TYPE));
|
||||
|
||||
bool FVoxelBoundsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Extend Test
|
||||
{
|
||||
FVoxelBounds BoundsA;
|
||||
FVoxelBounds BoundsB;
|
||||
FVoxelBounds BoundsC;
|
||||
|
||||
BoundsA.AddCell(FIntVector(0, -1, 0));
|
||||
BoundsA.AddCell(FIntVector(1, 1, 2));
|
||||
|
||||
BoundsB.AddCell(FIntVector(1, 1, 1));
|
||||
|
||||
TestEqual(TEXT("BoundsA == ({0, -1, 0}, {2, 2, 3})"), BoundsA.GetBounds(), FBoxMinAndMax({0, -1, 0}, {2, 2, 3}));
|
||||
TestEqual(TEXT("BoundsB == ({1, 1, 1}, {2, 2, 2})"), BoundsB.GetBounds(), FBoxMinAndMax({1, 1, 1}, {2, 2, 2}));
|
||||
TestEqual(TEXT("BoundsC == ({0, 0, 0}, {0, 0, 0})"), BoundsC.GetBounds(), FBoxMinAndMax::Invalid);
|
||||
}
|
||||
|
||||
// Comparison Test
|
||||
{
|
||||
FVoxelBounds BoundsA;
|
||||
FVoxelBounds BoundsB;
|
||||
FVoxelBounds BoundsC;
|
||||
FVoxelBounds BoundsD;
|
||||
FVoxelBounds BoundsE;
|
||||
|
||||
BoundsA.AddCell(FIntVector(0, 0, 0));
|
||||
BoundsA.AddCell(FIntVector(1, 1, 1));
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsA, (1, 1, 1), South, Door);
|
||||
|
||||
// Setting the cell connections in a different way is intentional here
|
||||
auto& CellA = BoundsB.AddCell(FIntVector(0, 0, 0));
|
||||
auto& CellB = BoundsB.AddCell(FIntVector(1, 1, 1));
|
||||
CellA[static_cast<uint8>(FVoxelBounds::EDirection::North)] = FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall);
|
||||
CellB[static_cast<uint8>(FVoxelBounds::EDirection::South)] = FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door);
|
||||
|
||||
BoundsC.AddCell(FIntVector(0, 0, 0));
|
||||
BoundsC.AddCell(FIntVector(1, 1, 1));
|
||||
SET_CONNECTION(BoundsC, (0, 0, 0), North, Wall);
|
||||
|
||||
TestEqual(TEXT("BoundsA == BoundsB"), BoundsA, BoundsB);
|
||||
TestNotEqual(TEXT("BoundsA != BoundsC"), BoundsA, BoundsC);
|
||||
TestTrue(TEXT("BoundsD is not valid"), !BoundsD.IsValid());
|
||||
TestNotEqual(TEXT("BoundsA != BoundsD"), BoundsA, BoundsD);
|
||||
}
|
||||
|
||||
// Transform Test
|
||||
{
|
||||
FVoxelBounds Bounds;
|
||||
Bounds.AddCell(FIntVector(0, 0, 0));
|
||||
Bounds.AddCell(FIntVector(1, 1, 1));
|
||||
SET_CONNECTION(Bounds, (0, 0, 0), North, Wall);
|
||||
SET_CONNECTION(Bounds, (1, 1, 1), South, Door);
|
||||
|
||||
// Offset by 0
|
||||
{
|
||||
TestEqual(TEXT("Bounds + (0,0,0) == OffsetBounds"), Bounds + FIntVector {0, 0, 0}, Bounds);
|
||||
}
|
||||
|
||||
// Simple addition
|
||||
{
|
||||
FVoxelBounds OffsetBounds;
|
||||
OffsetBounds.AddCell(FIntVector(1, -1, 1));
|
||||
OffsetBounds.AddCell(FIntVector(2, 0, 2));
|
||||
SET_CONNECTION(OffsetBounds, (1, -1, 1), North, Wall);
|
||||
SET_CONNECTION(OffsetBounds, (2, 0, 2), South, Door);
|
||||
|
||||
TestEqual(TEXT("Bounds + (1,-1,1) == OffsetBounds"), Bounds + FIntVector {1, -1, 1}, OffsetBounds);
|
||||
}
|
||||
|
||||
// Simple subtraction
|
||||
{
|
||||
FVoxelBounds OffsetBounds;
|
||||
OffsetBounds.AddCell(FIntVector(-1, 1, -1));
|
||||
OffsetBounds.AddCell(FIntVector(0, 2, 0));
|
||||
SET_CONNECTION(OffsetBounds, (-1, 1, -1), North, Wall);
|
||||
SET_CONNECTION(OffsetBounds, (0, 2, 0), South, Door);
|
||||
TestEqual(TEXT("Bounds - (1,-1,1) == OffsetBounds"), Bounds - FIntVector {1, -1, 1}, OffsetBounds);
|
||||
}
|
||||
|
||||
// North rotation
|
||||
{
|
||||
TestEqual(TEXT("Bounds.Rotate(North) == Bounds"), Rotate(Bounds, EDoorDirection::North), Bounds);
|
||||
}
|
||||
|
||||
// East rotation
|
||||
{
|
||||
FVoxelBounds RotatedBounds;
|
||||
RotatedBounds.AddCell(FIntVector(0, 0, 0));
|
||||
RotatedBounds.AddCell(FIntVector(-1, 1, 1));
|
||||
SET_CONNECTION(RotatedBounds, (0, 0, 0), East, Wall);
|
||||
SET_CONNECTION(RotatedBounds, (-1, 1, 1), West, Door);
|
||||
FVoxelBounds NewBounds = Rotate(Bounds, EDoorDirection::East);
|
||||
TestEqual(TEXT("Bounds.Rotate(East) == RotatedBounds"), NewBounds, RotatedBounds);
|
||||
}
|
||||
|
||||
// South rotation
|
||||
{
|
||||
FVoxelBounds RotatedBounds;
|
||||
RotatedBounds.AddCell(FIntVector(0, 0, 0));
|
||||
RotatedBounds.AddCell(FIntVector(-1, -1, 1));
|
||||
SET_CONNECTION(RotatedBounds, (0, 0, 0), South, Wall);
|
||||
SET_CONNECTION(RotatedBounds, (-1, -1, 1), North, Door);
|
||||
TestEqual(TEXT("Bounds.Rotate(South) == RotatedBounds"), Rotate(Bounds, EDoorDirection::South), RotatedBounds);
|
||||
}
|
||||
|
||||
// West rotation
|
||||
{
|
||||
FVoxelBounds RotatedBounds;
|
||||
RotatedBounds.AddCell(FIntVector(0, 0, 0));
|
||||
RotatedBounds.AddCell(FIntVector(1, -1, 1));
|
||||
SET_CONNECTION(RotatedBounds, (0, 0, 0), West, Wall);
|
||||
SET_CONNECTION(RotatedBounds, (1, -1, 1), East, Door);
|
||||
FVoxelBounds NewBounds = Rotate(Bounds, EDoorDirection::West);
|
||||
TestEqual(TEXT("Bounds.Rotate(West) == RotatedBounds"), NewBounds, RotatedBounds);
|
||||
}
|
||||
}
|
||||
|
||||
// Concatenation Test
|
||||
{
|
||||
FVoxelBounds BoundsA;
|
||||
BoundsA.AddCell(FIntVector(0, 0, 0));
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), East, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), South, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), West, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
|
||||
|
||||
FVoxelBounds BoundsB;
|
||||
BoundsB.AddCell(FIntVector(1, 0, 0));
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), South, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
|
||||
|
||||
FVoxelBounds BoundsC = BoundsA + BoundsB;
|
||||
FVoxelBounds BoundsD = BoundsB + BoundsA;
|
||||
|
||||
TestEqual(TEXT("BoundsC == BoundsD"), BoundsC, BoundsD);
|
||||
TestEqual(TEXT("BoundsC.GetBounds() == BoundsD.GetBounds()"), BoundsC.GetBounds(), BoundsD.GetBounds());
|
||||
|
||||
FVoxelBounds ExpectedBounds;
|
||||
ExpectedBounds.AddCell(FIntVector(0, 0, 0));
|
||||
ExpectedBounds.AddCell(FIntVector(1, 0, 0));
|
||||
SET_CONNECTION(ExpectedBounds, (0, 0, 0), East, Door);
|
||||
SET_CONNECTION(ExpectedBounds, (0, 0, 0), South, Door);
|
||||
SET_CONNECTION(ExpectedBounds, (0, 0, 0), West, Door);
|
||||
SET_CONNECTION(ExpectedBounds, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(ExpectedBounds, (0, 0, 0), Down, Wall);
|
||||
|
||||
SET_CONNECTION(ExpectedBounds, (1, 0, 0), North, Wall);
|
||||
SET_CONNECTION(ExpectedBounds, (1, 0, 0), East, Wall);
|
||||
SET_CONNECTION(ExpectedBounds, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(ExpectedBounds, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(ExpectedBounds, (1, 0, 0), Down, Wall);
|
||||
|
||||
TestEqual(TEXT("BoundsC == ExpectedBounds"), BoundsC, ExpectedBounds);
|
||||
}
|
||||
|
||||
// Subtraction test
|
||||
{
|
||||
FVoxelBounds BoundsA;
|
||||
BoundsA.AddCell(FIntVector(0, 0, 0));
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), East, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), South, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), West, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
|
||||
|
||||
FVoxelBounds BoundsB;
|
||||
BoundsB.AddCell(FIntVector(1, 0, 0));
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), South, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
|
||||
|
||||
FVoxelBounds BoundsC;
|
||||
BoundsC.AddCell(FIntVector(0, 0, 0));
|
||||
BoundsC.AddCell(FIntVector(1, 0, 0));
|
||||
SET_CONNECTION(BoundsC, (0, 0, 0), East, Door);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 0), South, Door);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 0), West, Door);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 0), Down, Wall);
|
||||
|
||||
SET_CONNECTION(BoundsC, (1, 0, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 0), Down, Wall);
|
||||
|
||||
FVoxelBounds ExpectedBoundsCMinusA;
|
||||
ExpectedBoundsCMinusA.AddCell(FIntVector(1, 0, 0));
|
||||
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), North, Wall);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), East, Wall);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), South, Door);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), Down, Wall);
|
||||
|
||||
FVoxelBounds ExpectedBoundsCMinusB;
|
||||
ExpectedBoundsCMinusB.AddCell(FIntVector(0, 0, 0));
|
||||
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), North, Wall);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), East, Door);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), South, Door);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), West, Door);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), Down, Wall);
|
||||
|
||||
FVoxelBounds BoundsCMinusA = BoundsC - BoundsA;
|
||||
FVoxelBounds BoundsCMinusB = BoundsC - BoundsB;
|
||||
|
||||
TestEqual(TEXT("BoundsC - BoundsA == ExpectedBoundsCMinusA"), BoundsCMinusA, ExpectedBoundsCMinusA);
|
||||
TestEqual(TEXT("BoundsC - BoundsB == ExpectedBoundsCMinusB"), BoundsCMinusB, ExpectedBoundsCMinusB);
|
||||
|
||||
// Non commutative
|
||||
FVoxelBounds BoundsAMinusC = BoundsA - BoundsC;
|
||||
FVoxelBounds BoundsBMinusC = BoundsB - BoundsC;
|
||||
|
||||
TestFalse(TEXT("BoundsA - BoundsC is invalid"), BoundsAMinusC.IsValid());
|
||||
TestFalse(TEXT("BoundsB - BoundsC is invalid"), BoundsBMinusC.IsValid());
|
||||
}
|
||||
|
||||
// DoesFitOutside Test
|
||||
{
|
||||
// Bounds of 2x2x2
|
||||
// +---+-o-+ + +---+
|
||||
// |100 110| |111|
|
||||
// +-o-+ + + +-o-+
|
||||
// |010|
|
||||
// + +---+ + + +
|
||||
FVoxelBounds DungeonBounds;
|
||||
DungeonBounds.AddCell(FIntVector(1, 0, 0));
|
||||
DungeonBounds.AddCell(FIntVector(0, 1, 0));
|
||||
DungeonBounds.AddCell(FIntVector(1, 1, 0));
|
||||
DungeonBounds.AddCell(FIntVector(1, 1, 1));
|
||||
|
||||
SET_CONNECTION(DungeonBounds, (1, 0, 0), North, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (1, 0, 0), South, Door);
|
||||
SET_CONNECTION(DungeonBounds, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (1, 0, 0), Down, Wall);
|
||||
|
||||
SET_CONNECTION(DungeonBounds, (0, 1, 0), West, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (0, 1, 0), East, Door);
|
||||
SET_CONNECTION(DungeonBounds, (0, 1, 0), South, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (0, 1, 0), Up, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (0, 1, 0), Down, Wall);
|
||||
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 0), North, Door);
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 0), East, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 0), Down, Wall);
|
||||
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 1), North, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 1), East, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 1), South, Door);
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 1), West, Wall);
|
||||
SET_CONNECTION(DungeonBounds, (1, 1, 1), Up, Wall);
|
||||
|
||||
// Bounds that fit in the available space
|
||||
{
|
||||
// Bounds that fits perfectly in the available space
|
||||
// + + +
|
||||
//
|
||||
// +-o-+ +
|
||||
// |000|
|
||||
// +---+ +
|
||||
FVoxelBounds BoundsA;
|
||||
BoundsA.AddCell(FIntVector(0, 0, 0));
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), South, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
|
||||
|
||||
// Like BoundsA but with a door facing a wall
|
||||
// + + +
|
||||
//
|
||||
// +-o-+ +
|
||||
// |000o
|
||||
// +---+ +
|
||||
FVoxelBounds BoundsB;
|
||||
BoundsB.AddCell(FIntVector(0, 0, 0));
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), North, Door);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), East, Door);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), South, Wall);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), Down, Wall);
|
||||
|
||||
// Bounds bigger that fits perfectly in the available space
|
||||
// +-o-+ +
|
||||
// |101|
|
||||
// + + +
|
||||
// |001o
|
||||
// +---+ +
|
||||
FVoxelBounds BoundsC;
|
||||
BoundsC.AddCell(FIntVector(0, 0, 1));
|
||||
BoundsC.AddCell(FIntVector(1, 0, 1));
|
||||
|
||||
SET_CONNECTION(BoundsC, (0, 0, 1), East, Door);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 1), South, Wall);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 1), West, Wall);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 1), Up, Wall);
|
||||
SET_CONNECTION(BoundsC, (0, 0, 1), Down, Wall);
|
||||
|
||||
SET_CONNECTION(BoundsC, (1, 0, 1), North, Door);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 1), West, Wall);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 1), East, Wall);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 1), Up, Wall);
|
||||
SET_CONNECTION(BoundsC, (1, 0, 1), Down, Wall);
|
||||
|
||||
// Like BoundsC but with a door facing a wall
|
||||
// +---+ +
|
||||
// |101o
|
||||
// + + +
|
||||
// |001o
|
||||
// +---+ +
|
||||
FVoxelBounds BoundsD;
|
||||
BoundsD.AddCell(FIntVector(0, 0, 1));
|
||||
BoundsD.AddCell(FIntVector(1, 0, 1));
|
||||
|
||||
SET_CONNECTION(BoundsD, (0, 0, 1), East, Door);
|
||||
SET_CONNECTION(BoundsD, (0, 0, 1), South, Wall);
|
||||
SET_CONNECTION(BoundsD, (0, 0, 1), West, Wall);
|
||||
SET_CONNECTION(BoundsD, (0, 0, 1), Up, Wall);
|
||||
SET_CONNECTION(BoundsD, (0, 0, 1), Down, Wall);
|
||||
|
||||
SET_CONNECTION(BoundsD, (1, 0, 1), North, Wall);
|
||||
SET_CONNECTION(BoundsD, (1, 0, 1), West, Wall);
|
||||
SET_CONNECTION(BoundsD, (1, 0, 1), East, Door);
|
||||
SET_CONNECTION(BoundsD, (1, 0, 1), Up, Wall);
|
||||
SET_CONNECTION(BoundsD, (1, 0, 1), Down, Wall);
|
||||
|
||||
// Bounds outside the dungeon bounds
|
||||
// + +---+ + +-o-+
|
||||
// |210| |211|
|
||||
// + +-o-+ + +---+
|
||||
FVoxelBounds BoundsE;
|
||||
BoundsE.AddCell(FIntVector(2, 1, 0));
|
||||
BoundsE.AddCell(FIntVector(2, 1, 1));
|
||||
SET_CONNECTION(BoundsE, (2, 1, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 0), South, Door);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 0), Down, Wall);
|
||||
|
||||
SET_CONNECTION(BoundsE, (2, 1, 1), North, Door);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 1), East, Wall);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 1), South, Wall);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 1), West, Wall);
|
||||
SET_CONNECTION(BoundsE, (2, 1, 1), Up, Wall);
|
||||
|
||||
// Like BoundsE but with a door facing a wall
|
||||
// + +---+ + +---+
|
||||
// |210| |211|
|
||||
// + +-o-+ + +-o-+
|
||||
FVoxelBounds BoundsF;
|
||||
BoundsF.AddCell(FIntVector(2, 1, 0));
|
||||
BoundsF.AddCell(FIntVector(2, 1, 1));
|
||||
SET_CONNECTION(BoundsF, (2, 1, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 0), South, Door);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 0), Down, Wall);
|
||||
|
||||
SET_CONNECTION(BoundsF, (2, 1, 1), North, Wall);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 1), East, Wall);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 1), South, Door);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 1), West, Wall);
|
||||
SET_CONNECTION(BoundsF, (2, 1, 1), Up, Wall);
|
||||
|
||||
// Bounds bigger with a connected door
|
||||
// + + +
|
||||
//
|
||||
// +-o-+-o-+
|
||||
// |001 011|
|
||||
// +---+---+
|
||||
FVoxelBounds BoundsG;
|
||||
BoundsG.AddCell(FIntVector(0, 0, 1));
|
||||
BoundsG.AddCell(FIntVector(0, 1, 1));
|
||||
|
||||
SET_CONNECTION(BoundsG, (0, 0, 1), North, Door);
|
||||
SET_CONNECTION(BoundsG, (0, 0, 1), South, Wall);
|
||||
SET_CONNECTION(BoundsG, (0, 0, 1), West, Wall);
|
||||
SET_CONNECTION(BoundsG, (0, 0, 1), Up, Wall);
|
||||
SET_CONNECTION(BoundsG, (0, 0, 1), Down, Wall);
|
||||
|
||||
SET_CONNECTION(BoundsG, (0, 1, 1), North, Door);
|
||||
SET_CONNECTION(BoundsG, (0, 1, 1), South, Wall);
|
||||
SET_CONNECTION(BoundsG, (0, 1, 1), East, Wall);
|
||||
SET_CONNECTION(BoundsG, (0, 1, 1), Up, Wall);
|
||||
SET_CONNECTION(BoundsG, (0, 1, 1), Down, Wall);
|
||||
|
||||
int32 _ScoreA, _ScoreB, _ScoreC, _ScoreD, _ScoreE, _ScoreF, _ScoreG;
|
||||
TestTrue(TEXT("BoundsA fit in DungeonBounds"), BoundsA.GetCompatibilityScore(DungeonBounds, _ScoreA));
|
||||
TestTrue(TEXT("BoundsB fit in DungeonBounds"), BoundsB.GetCompatibilityScore(DungeonBounds, _ScoreB));
|
||||
TestTrue(TEXT("BoundsC fit in DungeonBounds"), BoundsC.GetCompatibilityScore(DungeonBounds, _ScoreC));
|
||||
TestTrue(TEXT("BoundsD fit in DungeonBounds"), BoundsD.GetCompatibilityScore(DungeonBounds, _ScoreD));
|
||||
TestTrue(TEXT("BoundsE fit in DungeonBounds"), BoundsE.GetCompatibilityScore(DungeonBounds, _ScoreE));
|
||||
TestTrue(TEXT("BoundsF fit in DungeonBounds"), BoundsF.GetCompatibilityScore(DungeonBounds, _ScoreF));
|
||||
TestTrue(TEXT("BoundsG fit in DungeonBounds"), BoundsG.GetCompatibilityScore(DungeonBounds, _ScoreG));
|
||||
|
||||
// Order of fitting: E(G) > A > C(F) > B > D
|
||||
TestTrue(TEXT("BoundsE fit better than BoundsA"), _ScoreE > _ScoreA);
|
||||
TestTrue(TEXT("BoundsA fit better than BoundsC"), _ScoreA > _ScoreC);
|
||||
TestTrue(TEXT("BoundsC fit better than BoundsB"), _ScoreC > _ScoreB);
|
||||
TestTrue(TEXT("BoundsB fit better than BoundsD"), _ScoreB > _ScoreD);
|
||||
TestTrue(TEXT("BoundsG fit equally as BoundsE"), _ScoreG == _ScoreE);
|
||||
TestTrue(TEXT("BoundsC fit equally as BoundsF"), _ScoreC == _ScoreF);
|
||||
}
|
||||
|
||||
// Bounds that do not fit in the available space
|
||||
{
|
||||
// Bounds that does not fit in the available space
|
||||
// +---+ +
|
||||
// |100|
|
||||
// +-o-+ +
|
||||
//
|
||||
// + + +
|
||||
FVoxelBounds BoundsA;
|
||||
BoundsA.AddCell(FIntVector(1, 0, 0));
|
||||
SET_CONNECTION(BoundsA, (1, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsA, (1, 0, 0), North, Door);
|
||||
SET_CONNECTION(BoundsA, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsA, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsA, (1, 0, 0), Down, Wall);
|
||||
|
||||
int32 ScoreA;
|
||||
TestFalse(TEXT("BoundsA does not fit in DungeonBounds"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreA));
|
||||
|
||||
// Bigger bounds that does not fit in the available space
|
||||
// +---+ +
|
||||
// |100|
|
||||
// + + +
|
||||
// |000|
|
||||
// +-o-+ +
|
||||
FVoxelBounds BoundsB;
|
||||
BoundsB.AddCell(FIntVector(0, 0, 0));
|
||||
BoundsB.AddCell(FIntVector(1, 0, 0));
|
||||
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), South, Door);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsB, (0, 0, 0), Up, Wall);
|
||||
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
|
||||
|
||||
int32 ScoreB;
|
||||
TestFalse(TEXT("BoundsB does not fit in DungeonBounds"), BoundsB.GetCompatibilityScore(DungeonBounds, ScoreB));
|
||||
}
|
||||
|
||||
// Custom Score Test
|
||||
{
|
||||
UCustomScoreCallback* CustomCallbacks = NewObject<UCustomScoreCallback>();
|
||||
|
||||
FScoreCallback ZeroScore;
|
||||
ZeroScore.BindDynamic(CustomCallbacks, &UCustomScoreCallback::ZeroScore);
|
||||
|
||||
FScoreCallback NeverPassScore;
|
||||
NeverPassScore.BindDynamic(CustomCallbacks, &UCustomScoreCallback::NeverPass);
|
||||
|
||||
FVoxelBounds BoundsA;
|
||||
BoundsA.AddCell(FIntVector(0, 0, 0));
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), East, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), South, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), West, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
|
||||
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
|
||||
|
||||
int32 ScoreA;
|
||||
TestTrue(TEXT("BoundsA does fit in DungeonBounds with ZeroScore"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreA, ZeroScore));
|
||||
TestEqual(TEXT("BoundsA ZeroScore should have score of 0"), ScoreA, 0);
|
||||
|
||||
int32 ScoreB;
|
||||
TestFalse(TEXT("BoundsA does not fit in DungeonBounds ith NeverPass"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreB, NeverPassScore));
|
||||
TestNotEqual(TEXT("BoundsA NeverPass should have score different from ZeroScore"), ScoreA, ScoreB);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef SET_CONNECTION
|
||||
|
||||
#endif //WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "TriggerDoor.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Room.h"
|
||||
#include "RoomLevel.h"
|
||||
|
||||
ATriggerDoor::ATriggerDoor()
|
||||
{
|
||||
BoxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxComponent"));
|
||||
|
||||
if (IsValid(RootComponent))
|
||||
BoxComponent->SetupAttachment(RootComponent);
|
||||
}
|
||||
|
||||
void ATriggerDoor::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
check(BoxComponent);
|
||||
BoxComponent->OnComponentBeginOverlap.AddUniqueDynamic(this, &ATriggerDoor::OnTriggerEnter);
|
||||
BoxComponent->OnComponentEndOverlap.AddUniqueDynamic(this, &ATriggerDoor::OnTriggerExit);
|
||||
}
|
||||
|
||||
void ATriggerDoor::OnTriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
if (!ActorList.Contains(OtherActor) && IsValidActor(OtherActor, OtherComp))
|
||||
{
|
||||
ActorList.Add(OtherActor);
|
||||
UpdateOpenState();
|
||||
}
|
||||
}
|
||||
|
||||
void ATriggerDoor::OnTriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
|
||||
{
|
||||
if (ActorList.Contains(OtherActor))
|
||||
{
|
||||
ActorList.Remove(OtherActor);
|
||||
UpdateOpenState();
|
||||
}
|
||||
}
|
||||
|
||||
bool ATriggerDoor::IsValidActor_Implementation(AActor* Actor, UPrimitiveComponent* Component)
|
||||
{
|
||||
ACharacter* Character = Cast<ACharacter>(Actor);
|
||||
UCapsuleComponent* Capsule = Cast<UCapsuleComponent>(Component);
|
||||
return IsValid(Character) && IsValid(Capsule) && Capsule == Character->GetCapsuleComponent();
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "TriggerType.h"
|
||||
#include "TimerManager.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
// Sets default values for this component's properties
|
||||
UTriggerType::UTriggerType()
|
||||
{
|
||||
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
|
||||
// off to improve performance if you don't need them.
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
|
||||
TickDuration = 0.5f;
|
||||
ActivationDelay = 0.0;
|
||||
}
|
||||
|
||||
// Called when the game starts
|
||||
void UTriggerType::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (GetNetMode() != ENetMode::NM_Client)
|
||||
{
|
||||
OnComponentBeginOverlap.AddUniqueDynamic(this, &UTriggerType::OnTriggerEnter);
|
||||
OnComponentEndOverlap.AddUniqueDynamic(this, &UTriggerType::OnTriggerExit);
|
||||
GetWorld()->GetTimerManager().SetTimer(TickTimer, this, &UTriggerType::TriggerTick, TickDuration, true);
|
||||
}
|
||||
}
|
||||
|
||||
void UTriggerType::OnTriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
if (ActorType == nullptr || (OtherActor != nullptr && OtherActor->IsA(ActorType)))
|
||||
{
|
||||
if (!ActorList.Contains(OtherActor))
|
||||
{
|
||||
ActorList.Add(OtherActor);
|
||||
OnActorEnter.Broadcast(OtherActor);
|
||||
|
||||
if (ActorList.Num() >= requiredActorCountToActivate)
|
||||
{
|
||||
if (ActivationDelay > 0)
|
||||
{
|
||||
GetWorld()->GetTimerManager().SetTimer(ActivationTimer, this, &UTriggerType::TriggerActivate, ActivationDelay, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
TriggerActivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UTriggerType::OnTriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
|
||||
{
|
||||
if (ActorType == nullptr || (OtherActor != nullptr && OtherActor->IsA(ActorType)))
|
||||
{
|
||||
if (ActorList.Contains(OtherActor))
|
||||
{
|
||||
ActorList.Remove(OtherActor);
|
||||
OnActorExit.Broadcast(OtherActor);
|
||||
|
||||
GetWorld()->GetTimerManager().ClearTimer(ActivationTimer);
|
||||
TriggerDeactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UTriggerType::TriggerTick()
|
||||
{
|
||||
OnTriggerTick.Broadcast(ActorList);
|
||||
}
|
||||
|
||||
void UTriggerType::TriggerActivate()
|
||||
{
|
||||
if (!bIsActivated)
|
||||
{
|
||||
bIsActivated = true;
|
||||
OnActivation.Broadcast(ActorList);
|
||||
}
|
||||
}
|
||||
|
||||
void UTriggerType::TriggerDeactivate()
|
||||
{
|
||||
if (bIsActivated)
|
||||
{
|
||||
bIsActivated = false;
|
||||
OnDeactivation.Broadcast(ActorList);
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "Utils/DungeonSaveUtils.h"
|
||||
#include "Serialization/MemoryReader.h"
|
||||
#include "Serialization/MemoryWriter.h"
|
||||
#include "Serialization/StructuredArchive.h"
|
||||
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "Serialization/Formatters/JsonArchiveInputFormatter.h"
|
||||
#include "Serialization/Formatters/JsonArchiveOutputFormatter.h"
|
||||
#include "UObject/Class.h"
|
||||
|
||||
#if UE_VERSION_NEWER_THAN(5, 0, 0)
|
||||
#include "Serialization/StructuredArchiveSlotBase.h"
|
||||
#endif
|
||||
|
||||
TUniquePtr<FArchiveFormatterType> CreateArchiveFormatterFromArchive(FArchive& Ar, bool bTextFormat)
|
||||
{
|
||||
#if !UE_BUILD_SHIPPING && WITH_TEXT_ARCHIVE_SUPPORT
|
||||
if (bTextFormat)
|
||||
{
|
||||
if (Ar.IsSaving())
|
||||
return MakeUnique<FJsonArchiveOutputFormatter>(Ar);
|
||||
else
|
||||
return MakeUnique<FJsonArchiveInputFormatter>(Ar);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
return MakeUnique<FBinaryArchiveFormatter>(Ar);
|
||||
}
|
||||
}
|
||||
|
||||
bool SerializeUObject(FStructuredArchive::FRecord& Record, UObject* Obj, bool bIsLoading)
|
||||
{
|
||||
check(IsValid(Obj));
|
||||
|
||||
const bool bImplementsDungeonSave = Obj->Implements<UDungeonSaveInterface>();
|
||||
|
||||
// Allow modification of saved variables just before serializing into the saved dungeon.
|
||||
if (bImplementsDungeonSave)
|
||||
IDungeonSaveInterface::Execute_DungeonPreSerialize(Obj, bIsLoading);
|
||||
|
||||
bool bSuccess = true;
|
||||
Obj->SerializeScriptProperties(Record.EnterField(AR_FIELD_NAME("Properties")));
|
||||
if (auto* SaveableObj = Cast<IDungeonCustomSerialization>(Obj))
|
||||
{
|
||||
bSuccess &= SaveableObj->SerializeObject(Record, bIsLoading);
|
||||
}
|
||||
|
||||
// Allow some setup code right after the deserialization of its properties from the saved dungeon.
|
||||
if (bImplementsDungeonSave)
|
||||
IDungeonSaveInterface::Execute_DungeonPostSerialize(Obj, bIsLoading);
|
||||
|
||||
return bSuccess;
|
||||
}
|
||||
|
||||
bool SerializeUObject(FArchive& Ar, UObject* Obj, bool bIsLoading, bool bTextFormat)
|
||||
{
|
||||
TUniquePtr<FArchiveFormatterType> Formatter = CreateArchiveFormatterFromArchive(Ar, bTextFormat);
|
||||
FStructuredArchive StructuredArchive(*Formatter);
|
||||
//FStructuredArchiveFromArchive StructuredArchive(Ar);
|
||||
FStructuredArchive::FRecord Record = StructuredArchive.Open().EnterRecord();
|
||||
SerializeUObject(Record, Obj, bIsLoading);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SerializeUObject(TArray<uint8>& Data, UObject* Obj, bool bIsLoading, bool bTextFormat)
|
||||
{
|
||||
TSharedPtr<FArchive> Ar {nullptr};
|
||||
if (bIsLoading)
|
||||
{
|
||||
Ar = MakeShared<FMemoryReader>(Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
Ar = MakeShared<FMemoryWriter>(Data);
|
||||
}
|
||||
|
||||
check(Ar != nullptr);
|
||||
|
||||
FObjectAndNameAsStringProxyArchive Archive(*Ar, true);
|
||||
Archive.ArIsSaveGame = true;
|
||||
|
||||
SerializeUObject(Archive, Obj, bIsLoading, bTextFormat);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SerializeUClass(FStructuredArchiveSlot Slot, UClass*& Class)
|
||||
{
|
||||
SerializeUObjectRef(Slot, Class);
|
||||
}
|
||||
|
||||
void SerializeUObjectRef(FStructuredArchiveSlot Slot, UObject*& Object)
|
||||
{
|
||||
auto ObjPath = FSoftObjectPath(Object);
|
||||
Slot << ObjPath;
|
||||
|
||||
if (Slot.GetArchiveState().IsLoading())
|
||||
{
|
||||
// Resolve potential redirectors before trying to resolve the object
|
||||
ObjPath.FixupCoreRedirects();
|
||||
|
||||
Object = ObjPath.ResolveObject();
|
||||
if (nullptr == Object && !ObjPath.IsNull())
|
||||
{
|
||||
Object = ObjPath.TryLoad();
|
||||
}
|
||||
|
||||
if (nullptr == Object)
|
||||
{
|
||||
DungeonLog_Error("Failed to load class from path: %s", *ObjPath.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsLoading(const FStructuredArchiveSlotBase& Slot)
|
||||
{
|
||||
return Slot.GetArchiveState().IsLoading();
|
||||
}
|
||||
|
||||
bool IsSaving(const FStructuredArchiveSlotBase& Slot)
|
||||
{
|
||||
return Slot.GetArchiveState().IsSaving();
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "VoxelBounds/VoxelBounds.h"
|
||||
|
||||
const FIntVector FVoxelBounds::Directions[] = {
|
||||
{1, 0, 0}, // North
|
||||
{0, 1, 0}, // East
|
||||
{-1, 0, 0}, // South
|
||||
{0, -1, 0}, // West
|
||||
{0, 0, 1}, // Up
|
||||
{0, 0, -1} // Down
|
||||
};
|
||||
|
||||
bool FVoxelBoundsConnection::operator==(const FVoxelBoundsConnection& Other) const
|
||||
{
|
||||
if (Type != Other.Type)
|
||||
return false;
|
||||
if (EVoxelBoundsConnectionType::Door == Type)
|
||||
return DoorType == Other.DoorType;
|
||||
return true;
|
||||
}
|
||||
|
||||
int32 FVoxelBoundsConnection::GetCompatibilityScore(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B)
|
||||
{
|
||||
// No connection is always compatible with any other (that measn it's inside the bounds)
|
||||
if (A.Type == EVoxelBoundsConnectionType::None || B.Type == EVoxelBoundsConnectionType::None)
|
||||
return 0;
|
||||
|
||||
// When types are mismatching, it's not compatible
|
||||
if (A.Type != B.Type)
|
||||
{
|
||||
// Penalty when a door is not aligned with another door
|
||||
if (EVoxelBoundsConnectionType::Door == A.Type || EVoxelBoundsConnectionType::Door == B.Type)
|
||||
return -10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (EVoxelBoundsConnectionType::Door == A.Type)
|
||||
{
|
||||
// High score when doors are aligned and matching together
|
||||
if (A.DoorType == B.DoorType)
|
||||
return 10;
|
||||
|
||||
// Penalty when doors are aligned but not matching together
|
||||
return -10;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
FVoxelBounds::EDirection FVoxelBounds::Opposite(EDirection Direction)
|
||||
{
|
||||
static const EDirection OppositeDirections[] = {
|
||||
EDirection::South,
|
||||
EDirection::West,
|
||||
EDirection::North,
|
||||
EDirection::East,
|
||||
EDirection::Down,
|
||||
EDirection::Up
|
||||
};
|
||||
|
||||
const uint8 Index = static_cast<uint8>(Direction);
|
||||
if (Index < static_cast<uint8>(EDirection::NbDirection))
|
||||
{
|
||||
return OppositeDirections[Index];
|
||||
}
|
||||
|
||||
return EDirection::NbDirection;
|
||||
}
|
||||
|
||||
TArray<FVoxelBoundsConnection>& FVoxelBounds::AddCell(FIntVector Cell)
|
||||
{
|
||||
auto& Connections = Cells.Add(Cell);
|
||||
Connections.SetNum(static_cast<uint8>(EDirection::NbDirection));
|
||||
Bounds.Extend(FBoxMinAndMax(Cell, Cell + FIntVector(1)));
|
||||
return Connections;
|
||||
}
|
||||
|
||||
void FVoxelBounds::AddBox(const FBoxMinAndMax& Box)
|
||||
{
|
||||
Bounds.Extend(Box);
|
||||
Cells.Reserve(Cells.Num() + Box.GetSize().X * Box.GetSize().Y * Box.GetSize().Z);
|
||||
|
||||
for (int32 X = Box.GetMin().X; X < Box.GetMax().X; ++X)
|
||||
{
|
||||
for (int32 Y = Box.GetMin().Y; Y < Box.GetMax().Y; ++Y)
|
||||
{
|
||||
for (int32 Z = Box.GetMin().Z; Z < Box.GetMax().Z; ++Z)
|
||||
{
|
||||
auto& Connections = Cells.Add(FIntVector(X, Y, Z));
|
||||
Connections.SetNum(static_cast<uint8>(EDirection::NbDirection));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FVoxelBoundsConnection* FVoxelBounds::GetCellConnection(FIntVector Cell, EDirection Direction) const
|
||||
{
|
||||
auto* CellConnections = Cells.Find(Cell);
|
||||
if (nullptr == CellConnections)
|
||||
return nullptr;
|
||||
return &(*CellConnections)[static_cast<uint8>(Direction)];
|
||||
}
|
||||
|
||||
bool FVoxelBounds::SetCellConnection(FIntVector Cell, EDirection Direction, const FVoxelBoundsConnection& Connection)
|
||||
{
|
||||
auto* CellConnections = Cells.Find(Cell);
|
||||
if (nullptr == CellConnections)
|
||||
return false;
|
||||
|
||||
(*CellConnections)[static_cast<uint8>(Direction)] = Connection;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FVoxelBounds::ResetToWalls()
|
||||
{
|
||||
static const FVoxelBoundsConnection NoneConnection(EVoxelBoundsConnectionType::None);
|
||||
static const FVoxelBoundsConnection WallConnection(EVoxelBoundsConnectionType::Wall);
|
||||
|
||||
for (auto& Cell : Cells)
|
||||
{
|
||||
for (uint8 i = 0; i < static_cast<uint8>(EDirection::NbDirection); ++i)
|
||||
{
|
||||
const FIntVector OtherCell = Cell.Key + Directions[i];
|
||||
const auto* FoundOtherCell = Cells.Find(OtherCell);
|
||||
Cell.Value[i] = (FoundOtherCell) ? NoneConnection : WallConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FVoxelBounds::GetCompatibilityScore(const FVoxelBounds& Other, int32& Score, const FScoreCallback& CustomScore) const
|
||||
{
|
||||
// Each cell add 1 to the score, so the bigger volume the higher score.
|
||||
Score = Cells.Num();
|
||||
|
||||
bool bAreOverlapping = FBoxMinAndMax::Overlap(Bounds, Other.Bounds);
|
||||
|
||||
// @TODO: for now, treating a coincident face as overlapping
|
||||
// There is room for further optimizations here later
|
||||
bAreOverlapping |= Bounds.GetMin().X == Other.Bounds.GetMax().X;
|
||||
bAreOverlapping |= Bounds.GetMax().X == Other.Bounds.GetMin().X;
|
||||
bAreOverlapping |= Bounds.GetMin().Y == Other.Bounds.GetMax().Y;
|
||||
bAreOverlapping |= Bounds.GetMax().Y == Other.Bounds.GetMin().Y;
|
||||
bAreOverlapping |= Bounds.GetMin().Z == Other.Bounds.GetMax().Z;
|
||||
bAreOverlapping |= Bounds.GetMax().Z == Other.Bounds.GetMin().Z;
|
||||
|
||||
// When not overlapping, the score is equal to the number of cell
|
||||
// and it does always fit outside too.
|
||||
if (!bAreOverlapping)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto& Cell : Cells)
|
||||
{
|
||||
// When a cell is defined in both bounds, it does not fit outside
|
||||
if (Other.Cells.Contains(Cell.Key))
|
||||
{
|
||||
Score = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case when this cell is not defined in the other bounds
|
||||
// We set a score depending on the connection compatibility
|
||||
// @TODO: top and bottom are not yet relevant, but will be when doors on top/bottom will be implemented
|
||||
for (uint8 i = 0; i < static_cast<uint8>(EDoorDirection::NbDirection); ++i)
|
||||
{
|
||||
// Get Neighbor cell
|
||||
const FIntVector Neighbor = Cell.Key + Directions[i];
|
||||
auto* NeighConns = Other.Cells.Find(Neighbor);
|
||||
if (nullptr == NeighConns)
|
||||
continue;
|
||||
|
||||
const auto& Connection = Cell.Value[i];
|
||||
const auto& OtherConnection = (*NeighConns)[static_cast<uint8>(Opposite(static_cast<EDirection>(i)))];
|
||||
|
||||
if (CustomScore.IsBound())
|
||||
{
|
||||
if (!CustomScore.Execute(Connection, OtherConnection, Score))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Score += FVoxelBoundsConnection::GetCompatibilityScore(Connection, OtherConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FVoxelBounds::operator+=(const FIntVector& Offset)
|
||||
{
|
||||
*this = *this + Offset;
|
||||
}
|
||||
|
||||
void FVoxelBounds::operator-=(const FIntVector& Offset)
|
||||
{
|
||||
*this = *this - Offset;
|
||||
}
|
||||
|
||||
FVoxelBounds operator+(const FVoxelBounds& Bounds, const FIntVector& Offset)
|
||||
{
|
||||
FVoxelBounds NewBounds;
|
||||
for (const auto& Cell : Bounds.Cells)
|
||||
{
|
||||
NewBounds.Cells.Add(Cell.Key + Offset, Cell.Value);
|
||||
}
|
||||
NewBounds.Bounds = Bounds.Bounds + Offset;
|
||||
return NewBounds;
|
||||
}
|
||||
|
||||
FVoxelBounds operator-(const FVoxelBounds& Bounds, const FIntVector& Offset)
|
||||
{
|
||||
return Bounds + (FIntVector::ZeroValue - Offset);
|
||||
}
|
||||
|
||||
void FVoxelBounds::operator+=(const FVoxelBounds& Other)
|
||||
{
|
||||
for (const auto& Cell : Other.Cells)
|
||||
{
|
||||
// Ignore incoming cells that are already defined
|
||||
// @TODO: how to manage different connections?
|
||||
if (Cells.Contains(Cell.Key))
|
||||
continue;
|
||||
|
||||
auto& Connections = AddCell(Cell.Key);
|
||||
for (uint8 i = 0; i < static_cast<uint8>(EDirection::NbDirection); ++i)
|
||||
{
|
||||
// Get neighbor cell
|
||||
const FIntVector Neighbor = Cell.Key + Directions[i];
|
||||
if (auto* NeighConns = Cells.Find(Neighbor))
|
||||
{
|
||||
// If neighbor is defined, we clear the neigbor's connection
|
||||
// Also, we don't copy the connection of other bounds
|
||||
const uint8 OppositeDir = static_cast<uint8>(Opposite(static_cast<EDirection>(i)));
|
||||
(*NeighConns)[OppositeDir] = FVoxelBoundsConnection();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just copy connection if no neighbors
|
||||
Connections[i] = Cell.Value[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FVoxelBounds::operator-=(const FVoxelBounds& Other)
|
||||
{
|
||||
for (const auto& Cell : Other.Cells)
|
||||
{
|
||||
if (!Cells.Remove(Cell.Key))
|
||||
continue;
|
||||
|
||||
for (uint8 i = 0; i < static_cast<uint8>(EDirection::NbDirection); ++i)
|
||||
{
|
||||
// Get neighbor cell
|
||||
const FIntVector Neighbor = Cell.Key + Directions[i];
|
||||
if (auto* NeighConns = Cells.Find(Neighbor))
|
||||
{
|
||||
// If neighbor is defined, we copy this connection into it
|
||||
const uint8 OppositeDir = static_cast<uint8>(Opposite(static_cast<EDirection>(i)));
|
||||
(*NeighConns)[OppositeDir] = Cell.Value[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FVoxelBounds operator+(const FVoxelBounds& A, const FVoxelBounds& B)
|
||||
{
|
||||
FVoxelBounds Result = A;
|
||||
Result += B;
|
||||
return Result;
|
||||
}
|
||||
|
||||
FVoxelBounds operator-(const FVoxelBounds& A, const FVoxelBounds& B)
|
||||
{
|
||||
FVoxelBounds Result = A;
|
||||
Result -= B;
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool FVoxelBounds::operator==(const FVoxelBounds& Other) const
|
||||
{
|
||||
if (Cells.Num() != Other.Cells.Num())
|
||||
return false;
|
||||
|
||||
for (const auto& Cell : Cells)
|
||||
{
|
||||
const auto* OtherConnections = Other.Cells.Find(Cell.Key);
|
||||
if (!OtherConnections)
|
||||
return false;
|
||||
if (Cell.Value.Num() != OtherConnections->Num())
|
||||
return false;
|
||||
for (uint8 i = 0; i < Cell.Value.Num(); ++i)
|
||||
{
|
||||
if (Cell.Value[i] != (*OtherConnections)[i])
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FVoxelBounds::Overlap(const FVoxelBounds& A, const FVoxelBounds& B)
|
||||
{
|
||||
if (!FBoxMinAndMax::Overlap(A.Bounds, B.Bounds))
|
||||
return false;
|
||||
|
||||
// @TODO: Maybe it will be more performant to use a hierarchical partitioning
|
||||
// especially when using really small RoomUnits (like (1,1,1))
|
||||
for (const auto& Cell : A.Cells)
|
||||
{
|
||||
if (B.Cells.Contains(Cell.Key))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelBounds Rotate(const FVoxelBounds& Bounds, const EDoorDirection& Rot)
|
||||
{
|
||||
FVoxelBounds NewBounds;
|
||||
|
||||
for (const auto& Cell : Bounds.Cells)
|
||||
{
|
||||
const FIntVector NewCell = Rotate(Cell.Key, Rot);
|
||||
auto& NewConnections = NewBounds.AddCell(NewCell);
|
||||
// @TODO: will need to update that when doors on top/bottom will be implemented
|
||||
for (uint8 i = 0; i < static_cast<uint8>(EDoorDirection::NbDirection); ++i)
|
||||
{
|
||||
NewConnections[static_cast<uint8>(static_cast<EDoorDirection>(i) + Rot)] = Cell.Value[i];
|
||||
}
|
||||
// @TODO: Currently, no rotation are applied on top/bottom connections
|
||||
// but they will be when doors on top/bottom will be implemented
|
||||
NewConnections[static_cast<uint8>(FVoxelBounds::EDirection::Up)] = Cell.Value[static_cast<uint8>(FVoxelBounds::EDirection::Up)];
|
||||
NewConnections[static_cast<uint8>(FVoxelBounds::EDirection::Down)] = Cell.Value[static_cast<uint8>(FVoxelBounds::EDirection::Down)];
|
||||
}
|
||||
return NewBounds;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class ProceduralDungeon : ModuleRules
|
||||
{
|
||||
public ProceduralDungeon(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
// Uncomment that to detect when there are missing includes in cpp files
|
||||
//bUseUnity = false;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] {
|
||||
"Core",
|
||||
"NavigationSystem",
|
||||
#if UE_5_2_OR_LATER
|
||||
"IrisCore",
|
||||
#endif
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] {
|
||||
"Engine",
|
||||
"CoreUObject",
|
||||
"NetCore",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "BoundsParams.generated.h"
|
||||
|
||||
// Holds the settings for the dungeon limits.
|
||||
// These values are expressed in Room cells, and are based on the origin of the first room (0,0,0).
|
||||
// For example, if the first room is only 1 room cell (`FirstPoint = (0,0,0)`, `SecondPoint = (1,1,1)`), then this is the cell (0,0,0).
|
||||
// If you set a `MinY=2` et `MaxY=2`, then on the Y axis the dungeon can go from the cell -2 to cell 2,
|
||||
// Making an effective range of 5 cells, centered on the first room.
|
||||
USTRUCT(BlueprintType, meta = (ShortToolTip = "Holds the settings for the dungeon limits."))
|
||||
struct FBoundsParams
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Enables the X limit in positive axis (north from the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
|
||||
bool bLimitMaxX {false};
|
||||
|
||||
// The X positive limit (north) of the dungeon in room units (starting from the origin of the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMaxX", UIMin = 0, ClampMin = 0))
|
||||
int32 MaxX {0};
|
||||
|
||||
// Enables the X limit in negative axis (south from the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
|
||||
bool bLimitMinX {false};
|
||||
|
||||
// The X negative limit (south) of the dungeon in room units (starting from the origin of the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMinX", UIMin = 0, ClampMin = 0))
|
||||
int32 MinX {0};
|
||||
|
||||
// Enables the Y limit in positive axis (east from the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
|
||||
bool bLimitMaxY {false};
|
||||
|
||||
// The Y positive limit (east) of the dungeon in room units (starting from the origin of the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMaxY", UIMin = 0, ClampMin = 0))
|
||||
int32 MaxY {0};
|
||||
|
||||
// Enables the Y limit in negative axis (west from the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
|
||||
bool bLimitMinY {false};
|
||||
|
||||
// The Y negative limit (west) of the dungeon in room units (starting from the origin of the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMinY", UIMin = 0, ClampMin = 0))
|
||||
int32 MinY {0};
|
||||
|
||||
// Enables the Z limit in positive axis (up from the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
|
||||
bool bLimitMaxZ {false};
|
||||
|
||||
// The Z positive limit (up) of the dungeon in room units (starting from the origin of the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMaxZ", UIMin = 0, ClampMin = 0))
|
||||
int32 MaxZ {0};
|
||||
|
||||
// Enables the Z limit in negative axis (down from the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
|
||||
bool bLimitMinZ {false};
|
||||
|
||||
// The Z negative limit (down) of the dungeon in room units (starting from the origin of the first room).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMinZ", UIMin = 0, ClampMin = 0))
|
||||
int32 MinZ {0};
|
||||
|
||||
public:
|
||||
FBoxMinAndMax GetBox() const;
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "DeterministicRandomComponent.generated.h"
|
||||
|
||||
// A simple component that adds a RandomStream to any actor placed in a room level.
|
||||
// It will uses the actor's guid (provided by a IRoomActorGuid interface) and the owning room's ID
|
||||
// to generate an initial seed unique for this actor but deterministic with the dungeon's seed.
|
||||
UCLASS(BlueprintType, Blueprintable, ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
|
||||
class PROCEDURALDUNGEON_API UDeterministicRandomComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDeterministicRandomComponent();
|
||||
|
||||
protected:
|
||||
//~ Begin UActorComponent Interface
|
||||
virtual void OnRegister() override;
|
||||
//~ End UActorComponent Interface
|
||||
|
||||
static int32 GenerateDeterministicSeed(AActor* Actor);
|
||||
|
||||
// This is the C++ accessor.
|
||||
const FRandomStream& GetRandom() const { return Random; }
|
||||
|
||||
private:
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Deterministic Random", meta = (AllowPrivateAccess = true))
|
||||
FRandomStream Random;
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Interfaces/DoorInterface.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "DoorComponent.generated.h"
|
||||
|
||||
class URoom;
|
||||
class UDoorType;
|
||||
class UDoorComponent;
|
||||
class URoomConnection;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDoorComponentLockedDelegate, UDoorComponent*, Component, bool, IsLocked);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDoorComponentOpenedDelegate, UDoorComponent*, Component, bool, IsOpened);
|
||||
|
||||
// Component that manages open/close of a door, as well as a locking state.
|
||||
// Multiplayer and GameSave ready
|
||||
UCLASS(BlueprintType, Blueprintable, ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
|
||||
class PROCEDURALDUNGEON_API UDoorComponent : public UActorComponent, public IDoorInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDoorComponent();
|
||||
|
||||
public:
|
||||
//~ Begin AActor interface
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
//~ End AActor interface
|
||||
|
||||
//~ Begin IDoorInterface interface
|
||||
virtual const UDoorType* GetDoorType_Implementation() const override { return Type; }
|
||||
virtual void SetRoomConnection_Implementation(URoomConnection* RoomConnection) override;
|
||||
//~ End IDoorInterface interface
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Locked"))
|
||||
FORCEINLINE bool IsLocked() const { return bLocked; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Open"))
|
||||
FORCEINLINE bool IsOpen() const { return bIsOpen; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door")
|
||||
void Open(bool bOpen);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door")
|
||||
void Lock(bool bLock);
|
||||
|
||||
bool ShouldBeOpen() const;
|
||||
bool ShouldBeLocked() const;
|
||||
|
||||
URoom* GetRoomA() const;
|
||||
URoom* GetRoomB() const;
|
||||
URoomConnection* GetRoomConnection() const { return RoomConnection; }
|
||||
|
||||
bool IsAlwaysVisible() const { return bAlwaysVisible; }
|
||||
bool IsAlwaysUnlocked() const { return bAlwaysUnlocked; }
|
||||
void SetAlwaysVisible(bool bInAlwaysVisible) { bAlwaysVisible = bInAlwaysVisible; }
|
||||
void SetAlwaysUnlocked(bool bInAlwaysUnlocked) { bAlwaysUnlocked = bInAlwaysUnlocked; }
|
||||
|
||||
void SetDoorType(UDoorType* DoorType) { Type = DoorType; }
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintAssignable, Category = "Door")
|
||||
FDoorComponentOpenedDelegate OnDoorOpened;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Door")
|
||||
FDoorComponentLockedDelegate OnDoorLocked;
|
||||
|
||||
protected:
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Locked"))
|
||||
void OnDoorLock_BP(bool bIsLocked);
|
||||
virtual void OnDoorLock(bool bIsLocked) {}
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Open"))
|
||||
void OnDoorOpen_BP(bool bIsOpened);
|
||||
virtual void OnDoorOpen(bool bIsOpened) {}
|
||||
|
||||
bool OwnerHasAuthority() const;
|
||||
|
||||
protected:
|
||||
bool bLocked {false};
|
||||
bool bIsOpen {false};
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Door")
|
||||
URoomConnection* RoomConnection {nullptr};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Door", meta = (DisplayName = "Always Visible"))
|
||||
bool bAlwaysVisible {false};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Door", meta = (DisplayName = "Always Unlocked"))
|
||||
bool bAlwaysUnlocked {false};
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Door", meta = (DisplayName = "Door Type"))
|
||||
UDoorType* Type {nullptr};
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "RoomVisitor.h"
|
||||
#include "RoomObserverComponent.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomObserverEvent, ARoomLevel*, RoomLevel, AActor*, Actor);
|
||||
|
||||
// Room Observer that auto-(un)bind itself when it enters/exits a dungeon room.
|
||||
// Could observe (be bound) multiple rooms at once if the actor overlaps multiple room.
|
||||
// This component **does** track its own Room, thus the actor can move between rooms (use StaticRoomObserverComponent instead if this behavior is not needed).
|
||||
UCLASS(ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
|
||||
class PROCEDURALDUNGEON_API URoomObserverComponent : public UActorComponent, public IRoomVisitor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
URoomObserverComponent();
|
||||
|
||||
FRoomObserverEvent& OnActorEnterRoomEvent() { return ActorEnterRoomEvent; }
|
||||
FRoomObserverEvent& OnActorExitRoomEvent() { return ActorExitRoomEvent; }
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Enter Room"))
|
||||
FRoomObserverEvent ActorEnterRoomEvent;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Exit Room"))
|
||||
FRoomObserverEvent ActorExitRoomEvent;
|
||||
|
||||
private:
|
||||
//~ BEGIN IRoomVisitor
|
||||
virtual void OnRoomEnter_Implementation(ARoomLevel* RoomLevel) override;
|
||||
virtual void OnRoomExit_Implementation(ARoomLevel* RoomLevel) override;
|
||||
//~ END IRoomVisitor
|
||||
|
||||
void BindToLevel(ARoomLevel* RoomLevel, bool Bind);
|
||||
|
||||
UFUNCTION()
|
||||
void OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Actor);
|
||||
|
||||
UFUNCTION()
|
||||
void OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Actor);
|
||||
|
||||
private:
|
||||
TSet<ARoomLevel*> BoundLevels;
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Interfaces/RoomActorGuid.h"
|
||||
#include "SimpleGuidComponent.generated.h"
|
||||
|
||||
#define GUID_DEBUG_ENABLED 0
|
||||
|
||||
// A simple guid component that will retrieve the Editor's ActorGuid
|
||||
// to save/load it in packaged games.
|
||||
//
|
||||
// :::warning
|
||||
//
|
||||
// This component will work only on placed actor, not actors spawned during runtime!!!
|
||||
//
|
||||
// :::
|
||||
UCLASS(BlueprintType, Blueprintable, ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
|
||||
class PROCEDURALDUNGEON_API USimpleGuidComponent : public UActorComponent, public IRoomActorGuid
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USimpleGuidComponent();
|
||||
|
||||
protected:
|
||||
//~ Begin UActorComponent Interface
|
||||
virtual void OnRegister() override;
|
||||
//~ End UActorComponent Interface
|
||||
|
||||
//~ Begin IRoomActorGuid Interface
|
||||
virtual FGuid GetGuid_Implementation() const override;
|
||||
virtual bool ShouldSaveActor_Implementation() const override;
|
||||
//~ End IRoomActorGuid Interface
|
||||
|
||||
// Unfortunately I can't place them in the #if block below. UE will complain about it.
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
virtual void Serialize(FStructuredArchive::FRecord Record) override;
|
||||
|
||||
#if GUID_DEBUG_ENABLED // Enable some logs to debug lifecycle of the component.
|
||||
//~ Begin UObject Interface
|
||||
virtual void PostInitProperties() override;
|
||||
virtual void PreSave(FObjectPreSaveContext SaveContext);
|
||||
virtual void PostLoad() override;
|
||||
//~ End UObject Interface
|
||||
|
||||
//~ Begin UActorComponent Interface
|
||||
virtual void OnComponentCreated() override;
|
||||
virtual void InitializeComponent() override;
|
||||
virtual void BeginPlay() override;
|
||||
//~ End UActorComponent Interface
|
||||
#endif
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, NonPIEDuplicateTransient, TextExportTransient, Category = "Guid")
|
||||
FGuid Guid;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Dungeon Save")
|
||||
bool bSaveActorInDungeon {true};
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "StaticRoomObserverComponent.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FStaticRoomObserverEvent, ARoomLevel*, RoomLevel, AActor*, Actor);
|
||||
|
||||
// Room Observer that auto-(un)bind itself at BeginPlay and EndPlay.
|
||||
// This component will bind to the level it belongs to. So it needs to be placed directly in the Room map.
|
||||
// This component does **not** track its own Room, thus the actor should not move between rooms (use RoomObserverComponent instead).
|
||||
UCLASS(ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
|
||||
class PROCEDURALDUNGEON_API UStaticRoomObserverComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UStaticRoomObserverComponent();
|
||||
|
||||
FStaticRoomObserverEvent& OnActorEnterRoomEvent() { return ActorEnterRoomEvent; }
|
||||
FStaticRoomObserverEvent& OnActorExitRoomEvent() { return ActorExitRoomEvent; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(EEndPlayReason::Type Reason) override;
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Enter Room"))
|
||||
FStaticRoomObserverEvent ActorEnterRoomEvent;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Exit Room"))
|
||||
FStaticRoomObserverEvent ActorExitRoomEvent;
|
||||
|
||||
private:
|
||||
void BindToLevel(bool Bind);
|
||||
|
||||
UFUNCTION()
|
||||
void OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Visitor);
|
||||
|
||||
UFUNCTION()
|
||||
void OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Visitor);
|
||||
|
||||
private:
|
||||
bool bBound {false};
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "StaticRoomVisibilityComponent.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomVisibilityEvent, AActor*, Actor, bool, IsInVisibleRoom);
|
||||
|
||||
// Component to manage the visibility of an actor in the dungeon.
|
||||
// Use this one if the actor remains in the same room.
|
||||
// If the actor is able to move room, use URoomVisibilityComponent instead.
|
||||
UCLASS(ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent, DisplayName = "Room Visibility (Static)"))
|
||||
class PROCEDURALDUNGEON_API UStaticRoomVisibilityComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UStaticRoomVisibilityComponent();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(EEndPlayReason::Type Reason) override;
|
||||
|
||||
void SetVisible(UObject* Owner, bool Visible);
|
||||
void ResetVisible(UObject* Owner); // Same as SetVisible(Owner, false)
|
||||
|
||||
// Returns true if the actor is in a visible room.
|
||||
// Always returns true when "Occlude Dynamic Actors" is unchecked in the plugin's settings
|
||||
// Useful with "Custom" visibility.
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon", meta = (CompactNodeTitle = "Is In Visible Room", DisplayName = "Is In Visible Room"))
|
||||
bool IsVisible();
|
||||
|
||||
UFUNCTION(BlueprintSetter)
|
||||
void SetVisibilityMode(EVisibilityMode Mode);
|
||||
|
||||
UFUNCTION(BlueprintGetter)
|
||||
FORCEINLINE EVisibilityMode GetVisibilityMode() const { return VisibilityMode; }
|
||||
|
||||
public:
|
||||
// Called when the visibility from rooms changed (either by a room visibility change or by this actor moving between rooms).
|
||||
// Useful to update a "Custom" visibility.
|
||||
UPROPERTY(BlueprintAssignable, Category = "Procedural Dungeon")
|
||||
FRoomVisibilityEvent OnRoomVisibilityChanged;
|
||||
|
||||
protected:
|
||||
ARoomLevel* GetOwnerRoomLevel() const;
|
||||
void UpdateVisibility();
|
||||
void RegisterVisibilityDelegate(ARoomLevel* RoomLevel, bool Register);
|
||||
|
||||
UFUNCTION()
|
||||
void RoomVisibilityChanged(class ARoomLevel* RoomLevel, bool IsVisible);
|
||||
|
||||
private:
|
||||
void CleanEnablers();
|
||||
|
||||
TSet<TWeakObjectPtr<UObject>> VisibilityEnablers {};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintGetter = GetVisibilityMode, BlueprintSetter = SetVisibilityMode, Category = "Procedural Dungeon", meta = (AllowPrivateAccess = true))
|
||||
EVisibilityMode VisibilityMode {EVisibilityMode::Default};
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Door.generated.h"
|
||||
|
||||
class UDoorType;
|
||||
class UDoorComponent;
|
||||
|
||||
// Base class for all door actors in the dungeon.
|
||||
// Use this class even if you want to create a wall to place instead of a door (when the door is not connected to another room for example).
|
||||
UCLASS(Blueprintable, ClassGroup = "Procedural Dungeon")
|
||||
class PROCEDURALDUNGEON_API ADoor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ADoor();
|
||||
|
||||
//~ Begin AActor interface
|
||||
virtual void PostInitializeComponents() override;
|
||||
//~ End AActor interface
|
||||
|
||||
public:
|
||||
UDoorComponent* GetDoorComponent() const { return DoorComponent; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Locked", DeprecatedFunction, DeprecationMessage = "Use DoorComponent->IsLocked instead."))
|
||||
bool IsLocked() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Open", DeprecatedFunction, DeprecationMessage = "Use DoorComponent->IsOpen instead."))
|
||||
bool IsOpen() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door", meta = (DeprecatedFunction, DeprecationMessage = "Use DoorComponent->Open instead."))
|
||||
void Open(bool open);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door", meta = (DeprecatedFunction, DeprecationMessage = "Use DoorComponent->Lock instead."))
|
||||
void Lock(bool lock);
|
||||
|
||||
bool ShouldBeOpened() const;
|
||||
bool ShouldBeLocked() const;
|
||||
|
||||
UFUNCTION(BlueprintGetter)
|
||||
const UDoorType* GetDoorType() const;
|
||||
|
||||
UFUNCTION(BlueprintGetter)
|
||||
URoom* GetRoomA() const;
|
||||
|
||||
UFUNCTION(BlueprintGetter)
|
||||
URoom* GetRoomB() const;
|
||||
|
||||
// Used only to migrate from old save games. Must not be used anywhere else than the URoomConnection.
|
||||
bool GetLegacyShouldBeLocked() const { return bShouldBeLocked; }
|
||||
bool GetLegacyShouldBeOpen() const { return bShouldBeOpen; }
|
||||
bool GetLegacyAlwaysVisible() const { return bAlwaysVisible; }
|
||||
bool GetLegacyAlwaysUnlocked() const { return bAlwaysUnlocked; }
|
||||
UDoorType* GetLegacyDoorType() const { return Type; }
|
||||
|
||||
protected:
|
||||
UFUNCTION()
|
||||
virtual void OnDoorLock() {}
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Locked", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorLocked(true) instead."))
|
||||
void OnDoorLock_BP();
|
||||
|
||||
UFUNCTION()
|
||||
virtual void OnDoorUnlock() {}
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Unlocked", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorLocked(false) instead."))
|
||||
void OnDoorUnlock_BP();
|
||||
|
||||
UFUNCTION()
|
||||
virtual void OnDoorOpen() {}
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Open", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorOpened(true) instead."))
|
||||
void OnDoorOpen_BP();
|
||||
|
||||
UFUNCTION()
|
||||
virtual void OnDoorClose() {}
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Close", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorOpened(false) instead."))
|
||||
void OnDoorClose_BP();
|
||||
|
||||
UFUNCTION()
|
||||
void DispatchDoorLock(UDoorComponent* Component, bool IsLocked);
|
||||
|
||||
UFUNCTION()
|
||||
void DispatchDoorOpen(UDoorComponent* Component, bool IsOpened);
|
||||
|
||||
protected:
|
||||
// DEPRECATED: Ghost property for retro-compatibility with older plugin versions.
|
||||
UPROPERTY(BlueprintGetter = GetAlwaysVisible, BlueprintSetter = SetAlwaysVisible, SaveGame, Category = "Door", meta = (DisplayName = "Always Visible", DeprecatedProperty, DeprecationMessage = "Use DoorComponent->AlwaysVisible instead."))
|
||||
bool bAlwaysVisible {false};
|
||||
|
||||
// DEPRECATED: Ghost property for retro-compatibility with older plugin versions.
|
||||
UPROPERTY(BlueprintGetter = GetAlwaysUnlocked, BlueprintSetter = SetAlwaysUnlocked, SaveGame, Category = "Door", meta = (DisplayName = "Always Unlocked", DeprecatedProperty, DeprecationMessage = "Use DoorComponent->AlwaysUnlocked instead."))
|
||||
bool bAlwaysUnlocked {false};
|
||||
|
||||
// DEPRECATED: Ghost property for retro-compatibility with older plugin versions.
|
||||
UPROPERTY(BlueprintGetter = GetDoorType, Category = "Door", meta = (DisplayName = "Door Type", DeprecatedProperty, DeprecationMessage = "Use DoorComponent->DoorType instead."))
|
||||
UDoorType* Type {nullptr};
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Door")
|
||||
USceneComponent* DefaultSceneComponent {nullptr};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Door")
|
||||
UDoorComponent* DoorComponent {nullptr};
|
||||
|
||||
private:
|
||||
UFUNCTION(BlueprintGetter)
|
||||
bool GetAlwaysVisible() const;
|
||||
|
||||
UFUNCTION(BlueprintGetter)
|
||||
bool GetAlwaysUnlocked() const;
|
||||
|
||||
UFUNCTION(BlueprintSetter)
|
||||
void SetAlwaysVisible(bool bInAlwaysVisible);
|
||||
|
||||
UFUNCTION(BlueprintSetter)
|
||||
void SetAlwaysUnlocked(bool bInAlwaysUnlocked);
|
||||
|
||||
private:
|
||||
// Ghost properties for retro-compatibility. Redirect to the DoorComponent->RoomA/B internally.
|
||||
UPROPERTY(BlueprintGetter = GetRoomA, Category = "Door", meta = (AllowPrivateAccess = true, DeprecatedProperty, DeprecationMessage = "Use DoorComponent->RoomConnection->GetRoomA instead."))
|
||||
URoom* RoomA {nullptr};
|
||||
UPROPERTY(BlueprintGetter = GetRoomB, Category = "Door", meta = (AllowPrivateAccess = true, DeprecatedProperty, DeprecationMessage = "Use DoorComponent->RoomConnection->GetRoomB instead."))
|
||||
URoom* RoomB {nullptr};
|
||||
|
||||
// DEPRECATED: Ghost property for retro-compatibility with older save games.
|
||||
UPROPERTY(SaveGame, meta = (AllowPrivateAccess = true))
|
||||
bool bShouldBeLocked {false};
|
||||
|
||||
// DEPRECATED: Ghost property for retro-compatibility with older save games.
|
||||
UPROPERTY(SaveGame, meta = (AllowPrivateAccess = true))
|
||||
bool bShouldBeOpen {false};
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "DoorType.generated.h"
|
||||
|
||||
// Data asset to define a door type.
|
||||
// A door type is used to define the size, offset, and color of a door bounds.
|
||||
// Doors with different types are not compatible with each others.
|
||||
UCLASS(BlueprintType)
|
||||
class PROCEDURALDUNGEON_API UDoorType : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDoorType();
|
||||
|
||||
// Returns the door size from the door type asset,
|
||||
// or the default door size in plugin's settings if no door type defined.
|
||||
static FVector GetSize(const UDoorType* DoorType);
|
||||
|
||||
// Returns the door offset from the door type asset,
|
||||
// or the default door offset in plugin's settings if no door type defined.
|
||||
static float GetOffset(const UDoorType* DoorType);
|
||||
|
||||
// Returns the door color from the door type asset,
|
||||
// or the default door color in plugin's settings if no door type defined.
|
||||
static FColor GetColor(const UDoorType* DoorType);
|
||||
|
||||
// Returns true if one of the door type is explicitely set to be compatible with the other.
|
||||
static bool AreCompatible(const UDoorType* A, const UDoorType* B);
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
// For unit tests only, to set the compatibility of a door type.
|
||||
void SetCompatibility(const TArray<UDoorType*>& InCompatibility) { Compatibility = InCompatibility; }
|
||||
void SetCompatibleWithItself(bool bInCompatibleWithItself) { bCompatibleWithItself = bInCompatibleWithItself; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Size of the door bounds, only used by the debug draw as a visual hint for designers and artists.
|
||||
UPROPERTY(EditInstanceOnly, Category = "Door Type", meta = (ClampMin = 0))
|
||||
FVector Size;
|
||||
|
||||
// The offset of the door bounds from the room's base (in percentage of the room unit Z).
|
||||
UPROPERTY(EditInstanceOnly, Category = "Door Type", meta = (ClampMin = 0, ClampMax = 1, UIMin = 0, UIMax = 1))
|
||||
float Offset;
|
||||
|
||||
// The color used to draw the door bounds in the editor.
|
||||
UPROPERTY(EditInstanceOnly, Category = "Door Type")
|
||||
FColor Color;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// Just a description, used nowhere.
|
||||
UPROPERTY(EditInstanceOnly, Category = "Door Type")
|
||||
FText Description;
|
||||
#endif
|
||||
|
||||
// Can this door type be connected with itself?
|
||||
UPROPERTY(EditInstanceOnly, Category = "Door Type")
|
||||
bool bCompatibleWithItself;
|
||||
|
||||
// Which door types are compatible with this one
|
||||
UPROPERTY(EditInstanceOnly, Category = "Door Type")
|
||||
TArray<UDoorType*> Compatibility;
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "DungeonBlueprintLibrary.generated.h"
|
||||
|
||||
class URoom;
|
||||
class URoomCustomData;
|
||||
class ARoomLevel;
|
||||
|
||||
UCLASS()
|
||||
class PROCEDURALDUNGEON_API UDungeonBlueprintLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities|Procedural Dungeon")
|
||||
static bool IsDoorOfType(const TSubclassOf<class AActor> DoorClass, const class UDoorType* DoorType);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities", meta = (DisplayName = "Equal (Data Table Row Handle)", CompactNodeTitle = "=="))
|
||||
static bool CompareDataTableRows(const FDataTableRowHandle& A, const FDataTableRowHandle& B);
|
||||
|
||||
static const ARoomLevel* GetLevelScript(const AActor* Target);
|
||||
|
||||
// Returns the room instance the actor is in.
|
||||
// If the actor is spawned at runtime or the owning level is not a room level, returns null.
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities|Procedural Dungeon", meta = (DefaultToSelf = "Target"))
|
||||
static URoom* GetOwningRoom(const AActor* Target);
|
||||
|
||||
// Returns the first RoomCustomData of the provided type in the owning room.
|
||||
// If no owning room or no custom data of this type, returns null.
|
||||
UFUNCTION(BlueprintCallable, Category = "Utilities|Procedural Dungeon", meta = (DefaultToSelf = "Target", ExpandBoolAsExecs = "ReturnValue", DeterminesOutputType = "CustomDataClass", DynamicOutputParam = "CustomData"))
|
||||
static bool GetOwningRoomCustomData(const AActor* Target, TSubclassOf<URoomCustomData> CustomDataClass, URoomCustomData*& CustomData);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities|Procedural Dungeon", meta = (DefaultToSelf = "Target"))
|
||||
static const URoomData* GetLevelRoomData(const AActor* Target);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "DoorDef", meta = (DisplayName = "Opposite", CompactNodeTitle = "Opposite"))
|
||||
static FDoorDef DoorDef_GetOpposite(const FDoorDef& DoorDef);
|
||||
|
||||
// ===== DoorDirection Math Utility Functions =====
|
||||
|
||||
// True if the value is set (either North, South, East, West)
|
||||
// False otherwise
|
||||
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Is Valid", ExpandBoolAsExecs = "ReturnValue"))
|
||||
static bool DoorDirection_Valid(const EDoorDirection& A) { return !!A; }
|
||||
|
||||
// Addition (A + B)
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Door Direction", meta = (DisplayName = "Direction + Direction", CompactNodeTitle = "+", AutoCreateRefTerm = "A,B"))
|
||||
static EDoorDirection DoorDirection_Add(const EDoorDirection& A, const EDoorDirection& B) { return A + B; }
|
||||
|
||||
// Subtraction (A - B)
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Door Direction", meta = (DisplayName = "Direction - Direction", CompactNodeTitle = "-", AutoCreateRefTerm = "A,B"))
|
||||
static EDoorDirection DoorDirection_Sub(const EDoorDirection& A, const EDoorDirection& B) { return A - B; }
|
||||
|
||||
// Increment the direction and set it
|
||||
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Increment Door Direction", CompactNodeTitle = "++"))
|
||||
static EDoorDirection& DoorDirection_Increment(UPARAM(ref) EDoorDirection& A) { return ++A; }
|
||||
|
||||
// Decrement the direction and set it
|
||||
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Decrement Door Direction", CompactNodeTitle = "--"))
|
||||
static EDoorDirection& DoorDirection_Decrement(UPARAM(ref) EDoorDirection& A) { return --A; }
|
||||
|
||||
// Negate the direction and set it (same as North - Direction)
|
||||
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Negate Door Direction", CompactNodeTitle = "-"))
|
||||
static EDoorDirection& DoorDirection_Negate(UPARAM(ref) EDoorDirection& A) { A = -A; return A; }
|
||||
|
||||
// Transforms North into South and East into West (and vice versa)
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Door Direction", meta = (DisplayName = "Opposite", CompactNodeTitle = "Opposite", AutoCreateRefTerm = "A"))
|
||||
static EDoorDirection DoorDirection_Opposite(const EDoorDirection& A) { return ~A; }
|
||||
|
||||
// Convert a DoorDirection enum value into a unit IntVector pointing in that direction.
|
||||
UFUNCTION(BlueprintPure, Category = "Conversion|Door Direction", meta = (BlueprintAutocast, DisplayName = "To Int Vector", AutoCreateRefTerm = "A"))
|
||||
static FIntVector DoorDirection_ToIntVector(const EDoorDirection& A) { return ToIntVector(A); }
|
||||
|
||||
// Convert a DoorDirection enum value into a unit IntVector pointing in that direction.
|
||||
UFUNCTION(BlueprintPure, Category = "Conversion|Door Direction", meta = (BlueprintAutocast, DisplayName = "To Angle", AutoCreateRefTerm = "A"))
|
||||
static float DoorDirection_ToAngle(const EDoorDirection& A) { return ToAngle(A); }
|
||||
|
||||
// ===== Dungeon Math Transform =====
|
||||
|
||||
// Returns the neighbor at the provided direction.
|
||||
// Same as Vector + ToIntVector(Direction)
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Transform", meta = (DisplayName = "Next (Int Vector)"))
|
||||
static FIntVector IntVector_Next(const FIntVector& Vector, const EDoorDirection& Direction);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Transform", meta = (DisplayName = "Rotate (Int Vector)"))
|
||||
static FIntVector IntVector_Rotate(const FIntVector& Vector, const EDoorDirection& Direction);
|
||||
|
||||
// Transform a cell position from local coordinates into the dungeon coordinates
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Transform Position (Dungeon)", AutoCreateRefTerm = "Rotation"))
|
||||
static FIntVector Dungeon_TransformPosition(const FIntVector& LocalPos, const FIntVector& Translation, const EDoorDirection& Rotation);
|
||||
|
||||
// Inverse transform a cell position from the dungeon coordinates into a local coordinates
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Inverse Transform Position (Dungeon)", AutoCreateRefTerm = "Rotation"))
|
||||
static FIntVector Dungeon_InverseTransformPosition(const FIntVector& DungeonPos, const FIntVector& Translation, const EDoorDirection& Rotation);
|
||||
|
||||
// Transform a DoorDef structure from local coordinates into the dungeon coordinates
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Transform DoorDef (Dungeon)", AutoCreateRefTerm = "Rotation"))
|
||||
static FDoorDef Dungeon_TransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation);
|
||||
|
||||
// Inverse transform a DoorDef structure from the dungeon coordinates into a local coordinates
|
||||
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Inverse Transform DoorDef (Dungeon)", AutoCreateRefTerm = "Rotation"))
|
||||
static FDoorDef Dungeon_InverseTransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation);
|
||||
|
||||
// ===== Int Vector Operators =====
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Add (Int Vector)", CompactNodeTitle = "+", CallableWithoutWorldContext, CommutativeAssociativeBinaryOperator))
|
||||
static FIntVector IntVector_Add(const FIntVector& A, const FIntVector& B);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Subtract (Int Vector)", CompactNodeTitle = "-", CallableWithoutWorldContext, CommutativeAssociativeBinaryOperator))
|
||||
static FIntVector IntVector_Subtract(const FIntVector& A, const FIntVector& B);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Equal (Int Vector)", Keywords = "==", CompactNodeTitle = "==", CallableWithoutWorldContext))
|
||||
static bool IntVector_Equal(const FIntVector& A, const FIntVector& B);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Not Equal (Int Vector)", Keywords = "!=", CompactNodeTitle = "!=", CallableWithoutWorldContext))
|
||||
static bool IntVector_NotEqual(const FIntVector& A, const FIntVector& B);
|
||||
|
||||
// ===== Plugin Settings Accessors =====
|
||||
|
||||
// Returns the room unit size in unreal units
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Default Room Unit", DeprecatedFunction, DeprecationMessage = "Use the GetRoomUnit from the DungeonSettings class instead."))
|
||||
static FVector Settings_RoomUnit();
|
||||
|
||||
// Returns the default door type's size
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Default Door Size"))
|
||||
static FVector Settings_DefaultDoorSize();
|
||||
|
||||
// Returns the room offset as a percentage of the height of a room unit
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Door Offset"))
|
||||
static float Settings_DoorOffset();
|
||||
|
||||
// Returns true if the plugin's occlusion system is enabled
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Is Occlusion Culling Enabled"))
|
||||
static bool Settings_OcclusionCulling();
|
||||
|
||||
// Enable/disable the plugin's occlusion system
|
||||
UFUNCTION(BlueprintCallable, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Enable Occlusion Culling"))
|
||||
static void Settings_SetOcclusionCulling(bool Enable);
|
||||
|
||||
// Returns the number of visible room from the player's room (1 mean only the player room is visible)
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Occlusion Culling Distance"))
|
||||
static int32 Settings_OcclusionDistance();
|
||||
|
||||
// Set the number of visible rooms from the player's room (1 mean only the player room is visible)
|
||||
UFUNCTION(BlueprintCallable, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Set Occlusion Culling Distance"))
|
||||
static void Settings_SetOcclusionDistance(int32 Distance);
|
||||
|
||||
// Returns true if actors with a RoomVisibility component should have their visibility toggled with the rooms
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Should Dynamic Actors Be Occluded"))
|
||||
static bool Settings_OccludeDynamicActors();
|
||||
|
||||
// ===== Gameplay Utility Functions =====
|
||||
|
||||
// Set player to spectate
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Player", meta = (DefaultToSelf = "Controller"))
|
||||
static void Spectate(APlayerController* Controller, bool DestroyPawn = true);
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "DungeonGeneratorBase.h"
|
||||
#include "BoundsParams.h"
|
||||
#include "QueueOrStack.h"
|
||||
#include "DungeonGenerator.generated.h"
|
||||
|
||||
class IReadOnlyRoom;
|
||||
|
||||
// This is the main actor of the plugin. The dungeon generator is responsible to generate dungeons and replicate them over the network.
|
||||
UCLASS(Blueprintable, ClassGroup = "Procedural Dungeon", HideCategories = "GenerationAlgorithm")
|
||||
class PROCEDURALDUNGEON_API ADungeonGenerator : public ADungeonGeneratorBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ADungeonGenerator();
|
||||
|
||||
protected:
|
||||
//~ Begin ADungeonGeneratorBase Interface
|
||||
virtual bool CreateDungeon_Implementation() override;
|
||||
//~ End ADungeonGeneratorBase Interface
|
||||
|
||||
public:
|
||||
// ===== Methods that should be overriden in blueprint =====
|
||||
|
||||
// Return the RoomData you want as root of the dungeon generation
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose First Room"))
|
||||
URoomData* ChooseFirstRoomData();
|
||||
|
||||
/** Return the RoomData that will be connected to the Current Room
|
||||
* @param CurrentRoom [DEPRECATED] Use CurrentRoomInstance->GetRoomData instead.
|
||||
* @param CurrentRoomInstance The room instance to which the generator will connect the next room. This interface allows access only to some data.
|
||||
* @param DoorData The door of the CurrentRoom on which the next room will be connected (its location in room units, its orientation and its type).
|
||||
* @param DoorIndex The index of the door used on the next room to connect to the CurrentRoom.
|
||||
* Use -1 for a random (compatible) door, or the door index from the RoomData door array (0 is the first door).
|
||||
* WARNING: If the RandomDoor boolean of the RoomData is checked, then it will be considered -1 whatever you set here.
|
||||
* @return The room data asset used to instantiate the new room instance (must not be null)
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Next Room", ReturnDisplayName = "Room Data", AutoCreateRefTerm = "DoorIndex"))
|
||||
URoomData* ChooseNextRoomData(const URoomData* CurrentRoom, const TScriptInterface<IReadOnlyRoom>& CurrentRoomInstance, const FDoorDef& DoorData, int& DoorIndex);
|
||||
|
||||
// Condition to validate a dungeon Generation
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Is Valid Dungeon"))
|
||||
bool IsValidDungeon();
|
||||
|
||||
// Condition to continue or stop adding room to the dungeon
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Continue To Add Room"))
|
||||
bool ContinueToAddRoom();
|
||||
|
||||
// ===== Utility functions you can use in blueprint =====
|
||||
|
||||
// Return true if a specific RoomData is already in the dungeon
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
bool HasAlreadyRoomData(URoomData* RoomData);
|
||||
|
||||
// Return true if at least one of the RoomData from the list provided is already in the dungeon
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
bool HasAlreadyOneRoomDataFrom(TArray<URoomData*> RoomDataList);
|
||||
|
||||
// Return the number of a specific RoomData in the dungeon
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
int CountRoomData(URoomData* RoomData);
|
||||
|
||||
// Return the total number of RoomData in the dungeon from the list provided
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
int CountTotalRoomData(TArray<URoomData*> RoomDataList);
|
||||
|
||||
// Return true if a specific RoomData type is already in the dungeon
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
bool HasAlreadyRoomType(TSubclassOf<URoomData> RoomType);
|
||||
|
||||
// Return true if at least one of the RoomData type from the list provided is already in the dungeon
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
bool HasAlreadyOneRoomTypeFrom(TArray<TSubclassOf<URoomData>> RoomTypeList);
|
||||
|
||||
// Return the number of a specific RoomData type in the dungeon
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
int CountRoomType(TSubclassOf<URoomData> RoomType);
|
||||
|
||||
// Return the total number of RoomData type in the dungeon from the list provided
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
int CountTotalRoomType(TArray<TSubclassOf<URoomData>> RoomTypeList);
|
||||
|
||||
// Returns the current number of room in the generated dungeon.
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Generator", meta = (DisplayName = "Room Count", CompactNodeTitle = "Room Count", DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
|
||||
int GetNbRoom();
|
||||
|
||||
// Must be called in "Choose Next Room" function to be used.
|
||||
// Tell explicitely the generator we don't want to place a room for a specific door.
|
||||
// No error will be thrown when returning a null room data and no further room placement tries occur for this door (skip directly to the next door).
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
|
||||
void DiscardRoom() { bDiscardRoom = true; }
|
||||
|
||||
private:
|
||||
// Adds some new rooms linked to ParentRoom into Rooms list output
|
||||
// AddedRooms contains only the new rooms added to Rooms list
|
||||
// Returns true if the dungeon should keep adding new rooms
|
||||
bool AddNewRooms(URoom& ParentRoom, TArray<URoom*>& AddedRooms);
|
||||
|
||||
public:
|
||||
// In which order the dungeon generate rooms.
|
||||
// Depth First: Dungeon will use the last generated room to place the next one. Resulting in a more linear dungeon.
|
||||
// Breadth First: Dungeon will generate a room at each door of the current one before going to the next room. Resulting in a more spread dungeon.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation")
|
||||
EGenerationType GenerationType;
|
||||
|
||||
// If ticked, newly placed room will check if any door is aligned with another room, and if so will connect them.
|
||||
// If unticked, only the doors between CurrentRoom and NextRoom (in the function ChooseNextRoom) will be connected.
|
||||
// (will only have effect if the deprecated CanLoop in the plugin settings is ticked too, until it is removed in a future version)
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation")
|
||||
bool bCanLoop {true};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation")
|
||||
FBoundsParams DungeonLimits;
|
||||
|
||||
// If true, returning null in ChooseNextRoom is the same as calling DiscardRoom.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation", AdvancedDisplay)
|
||||
bool bAutoDiscardRoomIfNull = false;
|
||||
|
||||
// Number of room as parent room to process per tick.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Procedural Generation", AdvancedDisplay)
|
||||
int RoomBatchSize {10};
|
||||
|
||||
// Flag to explicitely tell we don't want to place a room.
|
||||
bool bDiscardRoom = false;
|
||||
|
||||
private:
|
||||
|
||||
enum class EState : uint8
|
||||
{
|
||||
Idle,
|
||||
Initializing,
|
||||
AddingRooms,
|
||||
Finalizing,
|
||||
Completed
|
||||
};
|
||||
|
||||
EState CurrentState {EState::Idle};
|
||||
|
||||
// Holds rooms pending to have new rooms added to them.
|
||||
TQueueOrStack<URoom*> PendingRooms;
|
||||
|
||||
int CurrentTriesLeft {0};
|
||||
};
|
||||
@@ -0,0 +1,435 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Math/RandomStream.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "UObject/ScriptInterface.h"
|
||||
#include "Serialization/Archive.h"
|
||||
#include "DungeonGeneratorBase.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGenerationEvent);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomEvent, const URoomData*, Room, const TScriptInterface<IReadOnlyRoom>&, RoomInstance);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomDoorEvent, const URoomData*, Room, const FDoorDef&, Door);
|
||||
|
||||
class URoom;
|
||||
class UDoorType;
|
||||
class UDungeonGraph;
|
||||
|
||||
UENUM()
|
||||
enum class EGenerationResult : uint8
|
||||
{
|
||||
None,
|
||||
Error,
|
||||
Success
|
||||
};
|
||||
|
||||
UENUM(meta = (Bitflags))
|
||||
enum class EGeneratorFlags
|
||||
{
|
||||
None = 0,
|
||||
Generating = 1 << 0,
|
||||
LoadSavedDungeon = 1 << 1,
|
||||
All = 0b11 // add new 1 for each new flags
|
||||
};
|
||||
ENUM_CLASS_FLAGS(EGeneratorFlags);
|
||||
|
||||
// Holds the data for saving a dungeon state
|
||||
USTRUCT(BlueprintType)
|
||||
struct FDungeonSaveData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadOnly, Category = "GUID")
|
||||
FGuid GeneratorId;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<uint8> Data {};
|
||||
|
||||
friend FArchive& operator<<(FArchive& Ar, FDungeonSaveData& Data);
|
||||
friend void operator<<(FStructuredArchiveSlot Slot, FDungeonSaveData& Data);
|
||||
};
|
||||
|
||||
// This is the main actor of the plugin. The dungeon generator is responsible to generate dungeons and replicate them over the network.
|
||||
// This base class is abstract. You need to override the `CreateDungeon` function to write your own generation algorithm.
|
||||
UCLASS(Abstract, Blueprintable, BlueprintType, ClassGroup = "Procedural Dungeon")
|
||||
class PROCEDURALDUNGEON_API ADungeonGeneratorBase : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ADungeonGeneratorBase();
|
||||
|
||||
protected:
|
||||
//~ Begin AActor Interface
|
||||
virtual void PostInitializeComponents() override;
|
||||
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) override;
|
||||
virtual void PostActorCreated() override;
|
||||
//~ End AActor Interface
|
||||
|
||||
void SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading);
|
||||
|
||||
public:
|
||||
// Update the seed and call the generation on all clients
|
||||
// Do nothing when called on clients
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Dungeon Generator")
|
||||
void Generate();
|
||||
|
||||
// Unload the current dungeon
|
||||
// Do nothing when called on clients
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Dungeon Generator")
|
||||
void Unload();
|
||||
|
||||
// Create a saved data from the current dungeon state
|
||||
UFUNCTION(BlueprintPure = false, Category = "Dungeon Generator")
|
||||
void SaveDungeon(FDungeonSaveData& SaveData);
|
||||
|
||||
// Load a dungeon from a previously saved data
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
|
||||
void LoadDungeon(const FDungeonSaveData& SaveData);
|
||||
|
||||
void SerializeDungeon(FArchive& Archive);
|
||||
|
||||
// ===== Methods that should be overriden in blueprint =====
|
||||
|
||||
// Return the door which will be spawned between Current Room and Next Room
|
||||
// @param CurrentRoom The first of both rooms to have been generated. By default the door will face this room. [DEPRECATED] Use `CurrentRoomInstance->GetRoomData` instead.
|
||||
// @param CurrentRoomInstance The room instance of one side of the door. By default the door will face this room.
|
||||
// @param NextRoom The second of both rooms to have been generated. Set Flipped to true to make the door facing this room. [DEPRECATED] Use `NextRoomInstance->GetRoomData` instead.
|
||||
// @param NextRoomInstance The room instance of the other side of the door. Set Flipped to true to make the door facing this room.
|
||||
// @param DoorType The door type set by both room data. Use IsDoorOfType function to compare a door actor class with this.
|
||||
// @param Flipped Tells which room the door is facing between CurrentRoom (false) and NextRoom (true).
|
||||
// @return The door actor class to spawn between CurrentRoom and NextRoom.
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Door"))
|
||||
TSubclassOf<AActor> ChooseDoor(const URoomData* CurrentRoom, const URoom* CurrentRoomInstance, const URoomData* NextRoom, const URoom* NextRoomInstance, const UDoorType* DoorType, const UDoorType* OtherDoorType, bool& Flipped);
|
||||
|
||||
// ===== Optional functions to override =====
|
||||
|
||||
// Initialize the room instances during the generation step
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator")
|
||||
void InitializeDungeon(const UDungeonGraph* Rooms);
|
||||
|
||||
// Returns which pawn is used for the room culling system.
|
||||
// This pawn will also affect the PlayerInside variable of the rooms.
|
||||
// By default returns GetPlayerController(0)->GetPawnOrSpectator().
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator")
|
||||
APawn* GetVisibilityPawn(APlayerController* PlayerController);
|
||||
|
||||
// ===== Optional events =====
|
||||
|
||||
// Called once before anything else when generating a new dungeon.
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Pre Generation"))
|
||||
void OnPreGeneration();
|
||||
|
||||
// Called once after all the dungeon generation (even if failed).
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Post Generation"))
|
||||
void OnPostGeneration();
|
||||
|
||||
// Called before trying to generate a new dungeon and each time IsValidDungeon return false.
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Generation Init"))
|
||||
void OnGenerationInit();
|
||||
|
||||
// Called when a dungeon has been successfully generated (IsValidDungeon returned true).
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Generation Success"))
|
||||
void OnGenerationSuccess();
|
||||
|
||||
// Called when all dungeon generation tries are exhausted (IsValidDungeon always return false).
|
||||
// No dungeon had been generated.
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Generation Failed"))
|
||||
void OnGenerationFailed();
|
||||
|
||||
// Called each time a room is added in the dungeon (but not spawned yet).
|
||||
// Those rooms can be destroyed without loading them if the generation try is not valid.
|
||||
// @param NewRoom The room data successfully added to the dungeon [DEPRECATED: will be removed in future version, use RoomInstance->GetRoomData instead]
|
||||
// @param RoomInstance The room successfully added to the dungeon
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "On Room Added"))
|
||||
void OnRoomAdded(const URoomData* NewRoom, const TScriptInterface<IReadOnlyRoom>& RoomInstance);
|
||||
|
||||
// Called each time no room could have been placed at a door (all room placement tries have been exhausted).
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Failed To Add Room"))
|
||||
void OnFailedToAddRoom(const URoomData* FromRoom, const FDoorDef& FromDoor);
|
||||
|
||||
// ===== Utility functions you can use in blueprint =====
|
||||
|
||||
// Return a random RoomData from the array provided
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
|
||||
URoomData* GetRandomRoomData(TArray<URoomData*> RoomDataArray);
|
||||
|
||||
// Return a random RoomData from the weighted map provided.
|
||||
// For example: you have RoomA with weight 1 and RoomB with weight 2,
|
||||
// then RoomA has proba of 1/3 and RoomB 2/3 to be returned.
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
|
||||
URoomData* GetRandomRoomDataWeighted(const TMap<URoomData*, int>& RoomDataWeightedMap);
|
||||
|
||||
// Returns a random RoomCandidate from the array provided
|
||||
// When the scores are used as weights, zero and negative scores are discarded automatically
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph", meta = (AdvancedDisplay = "bUseScoresAsWeights"))
|
||||
const FRoomCandidate& GetRandomRoomCandidate(const TArray<FRoomCandidate>& RoomCandidates, bool bUseScoresAsWeights = true) const;
|
||||
|
||||
// Returns an array of room data with at least one compatible door with the door data provided.
|
||||
// @param bSuccess True if at least one compatible room data was found.
|
||||
// @param CompatibleRooms Filled with all compatible room data found.
|
||||
// @param RoomDataArray The list of room data to check for compatibility.
|
||||
// @param DoorData The door used to check if a room is compatible.
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Generator")
|
||||
void GetCompatibleRoomData(bool& bSuccess, TArray<URoomData*>& CompatibleRooms, const TArray<URoomData*>& RoomDataArray, const FDoorDef& DoorData);
|
||||
|
||||
// Access to the random stream of the procedural dungeon. You should always use this for the procedural generation.
|
||||
// @return The random stream used by the dungeon generator.
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "This one is buggy, use the `Random Stream` variable getter instead."))
|
||||
const FRandomStream& GetRandomStream() const { return Random; }
|
||||
|
||||
// Returns the current generation progress.
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Generator")
|
||||
float GetProgress() const;
|
||||
|
||||
// @TODO: remove this function and use Graph->GetRoomByIndex() instead.
|
||||
URoom* GetRoomByIndex(int64 Index) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator", meta = (WorldContext = "WorldContextObject"))
|
||||
static void SaveAllDungeons(const UObject* WorldContextObject, TArray<FDungeonSaveData>& SavedData);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator", meta = (WorldContext = "WorldContextObject"))
|
||||
static void LoadAllDungeons(const UObject* WorldContextObject, const TArray<FDungeonSaveData>& SavedData);
|
||||
|
||||
// ===== Events =====
|
||||
|
||||
// Called once before anything else when generating a new dungeon.
|
||||
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
|
||||
FGenerationEvent OnPreGenerationEvent;
|
||||
|
||||
// Called once after all the dungeon generation (even if failed).
|
||||
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
|
||||
FGenerationEvent OnPostGenerationEvent;
|
||||
|
||||
// Called before trying to generate a new dungeon and each time IsValidDungeon return false.
|
||||
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
|
||||
FGenerationEvent OnGenerationInitEvent;
|
||||
|
||||
// Called when a dungeon has been successfully generated (IsValidDungeon returned true).
|
||||
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
|
||||
FGenerationEvent OnGenerationSuccessEvent;
|
||||
|
||||
// Called when all dungeon generation tries are exhausted (IsValidDungeon always return false).
|
||||
// No dungeon had been generated.
|
||||
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
|
||||
FGenerationEvent OnGenerationFailedEvent;
|
||||
|
||||
// Called each time a room is added in the dungeon (but not spawned yet).
|
||||
// Those rooms can be destroyed without loading them if the generation try is not valid.
|
||||
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
|
||||
FRoomEvent OnRoomAddedEvent;
|
||||
|
||||
// Called each time no room could have been placed at a door (all room placement tries have been exhausted).
|
||||
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
|
||||
FRoomDoorEvent OnFailedToAddRoomEvent;
|
||||
|
||||
protected:
|
||||
// Create virtually the dungeon (no load nor initialization of room levels)
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "GenerationAlgorithm")
|
||||
bool CreateDungeon();
|
||||
|
||||
// ===== Functions for dungeon creation =====
|
||||
|
||||
// Clear current graph and call GenerationInit event.
|
||||
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
|
||||
void StartNewDungeon();
|
||||
|
||||
// Initialize room instances after all rooms have been placed and connected (call InitializeDungeon).
|
||||
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
|
||||
void FinalizeDungeon();
|
||||
|
||||
// Create and initialize a new room instance using the room data provided.
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
|
||||
URoom* CreateRoomInstance(URoomData* RoomData);
|
||||
|
||||
// Set the position and rotation of a room instance and return true if there is nothing colliding with it.
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GenerationAlgorithm", meta = (BlueprintProtected, ReturnDisplayName = "Success", HidePin = "World"))
|
||||
bool TryPlaceRoom(URoom* const& Room, int DoorIndex, const FDoorDef& TargetDoor, const UWorld* World = nullptr) const;
|
||||
|
||||
// Set the position and rotation of a room instance and return true if there is nothing colliding with it.
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GenerationAlgorithm", meta = (BlueprintProtected, ReturnDisplayName = "Success", HidePin = "World"))
|
||||
bool TryPlaceRoomAtLocation(URoom* const& Room, FIntVector Location, EDoorDirection Rotation, const UWorld* World = nullptr) const;
|
||||
|
||||
// Check if the room instance provided is overlapping with existing rooms in the dungeon graph.
|
||||
// Also checks if bUseWorldCollisionChecks is true, in which case a box overlap test is made in the persistent world.
|
||||
bool CheckRoomOverlap(const URoom* const& Room, const UWorld* World = nullptr) const;
|
||||
|
||||
// Finalize the room creation by adding it to the dungeon graph. OnRoomAdded is called here.
|
||||
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected, ReturnDisplayName = "Success", AutoCreateRefTerm = "DoorsToConnect", AdvancedDisplay = "DoorsToConnect,bFailIfNotConnected"))
|
||||
bool AddRoomToDungeon(URoom* const& Room, const TArray<int>& DoorsToConnect, bool bFailIfNotConnected = true);
|
||||
bool AddRoomToDungeon(URoom* const& Room);
|
||||
|
||||
// Tells the generator to wait next frame to continue the generation process.
|
||||
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
|
||||
void YieldGeneration();
|
||||
|
||||
private:
|
||||
// Choose the door classes for all room connections.
|
||||
// This must happen *after* Graph->InitRooms() to be able to choose door class for unconnected doors.
|
||||
void ChooseDoorClasses();
|
||||
|
||||
// Update the player rooms based on the player position
|
||||
void UpdatePlayerRooms();
|
||||
|
||||
// Update the rooms visibility based on the player position
|
||||
void UpdateRoomVisibility(bool bForceUpdate = false);
|
||||
|
||||
// Update the rooms relevancy based on the player position
|
||||
void UpdateRoomRelevancy();
|
||||
|
||||
// Reset all data from a specific generation
|
||||
void Reset();
|
||||
|
||||
// Initialize the seed depending on the seed type setting
|
||||
void UpdateSeed();
|
||||
|
||||
bool IsGenerating() const { return EnumHasAllFlags(Flags, EGeneratorFlags::Generating); }
|
||||
bool IsLoadingSavedDungeon() const { return EnumHasAllFlags(Flags, EGeneratorFlags::LoadSavedDungeon); }
|
||||
|
||||
void DrawDebug() const;
|
||||
|
||||
// ===== FSM =====
|
||||
|
||||
void SetState(EGenerationState NewState);
|
||||
void OnStateBegin(EGenerationState State);
|
||||
void OnStateTick(EGenerationState State);
|
||||
void OnStateEnd(EGenerationState State);
|
||||
|
||||
public:
|
||||
// If ticked, the rooms location and rotation will be relative to this actor transform.
|
||||
// If unticked, the rooms will be placed relatively to the world's origin.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Procedural Generation")
|
||||
bool bUseGeneratorTransform;
|
||||
|
||||
// How to handle the seed at each generation call.
|
||||
// Random: Generate and use a random seed.
|
||||
// Auto Increment: Use Seed for first generation, and increment it by SeedIncrement in each subsequent generation.
|
||||
// Fixed: Use only Seed for each generation.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Procedural Generation|Seed")
|
||||
ESeedType SeedType;
|
||||
|
||||
// The increment number for each subsequent dungeon generation when SeedType is AutoIncrement.
|
||||
UPROPERTY(EditAnywhere, SaveGame, Category = "Procedural Generation|Seed", meta = (EditCondition = "SeedType==ESeedType::AutoIncrement", EditConditionHides, DisplayAfter = "Seed"))
|
||||
uint32 SeedIncrement;
|
||||
|
||||
// If ticked, when trying to place a new room during a dungeon generation,
|
||||
// a box overlap test will be made to make sure the room will not spawn
|
||||
// inside existing meshes in the persistent world.
|
||||
// This is a heavy work and should be ticked only when necessary.
|
||||
// Does not have impact during gameplay. Only during the generation process.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Procedural Generation", AdvancedDisplay)
|
||||
bool bUseWorldCollisionChecks {false};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation", AdvancedDisplay)
|
||||
class UDungeonSettings* SettingsOverrides {nullptr};
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
|
||||
void SetSeed(int32 NewSeed);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Generator", meta = (CompactNodeTitle = "Seed"))
|
||||
int32 GetSeed() const;
|
||||
|
||||
FGuid GetGuid() const { return Id; }
|
||||
|
||||
inline bool UseGeneratorTransform() const { return bUseGeneratorTransform; }
|
||||
FVector GetDungeonOffset() const;
|
||||
FQuat GetDungeonRotation() const;
|
||||
const FTransform& GetDungeonTransform() const;
|
||||
const UDungeonSettings* GetSettings() const { return SettingsOverrides; }
|
||||
|
||||
FORCEINLINE const UDungeonGraph* GetRooms() const { return Graph; }
|
||||
FORCEINLINE EGenerationState GetCurrentState() const { return CurrentState; }
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, Instanced, Category = "Dungeon Generator", meta = (DisplayName = "Rooms", ExposeFunctionCategories = "Dungeon Graph"))
|
||||
UDungeonGraph* Graph;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, VisibleInstanceOnly, NonPIEDuplicateTransient, TextExportTransient, Category = "GUID")
|
||||
FGuid Id;
|
||||
|
||||
private:
|
||||
UPROPERTY(Replicated, EditAnywhere, SaveGame, Category = "Procedural Generation|Seed", meta = (EditCondition = "SeedType!=ESeedType::Random", EditConditionHides))
|
||||
uint32 Seed;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Dungeon Generator", meta = (DisplayName = "Random Stream", AllowPrivateAccess = true))
|
||||
FRandomStream Random;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// If true the dungeon will be saved in a human readable json format.
|
||||
// *WARNING*: This is only available in editor and dev builds and will not change anything in packaged builds. It should be used for debugging purposes only.
|
||||
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
|
||||
bool bUseJsonSave {false};
|
||||
|
||||
// Draws the computed dungeon bounding box.
|
||||
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
|
||||
bool bDrawDebugDungeonBounds {false};
|
||||
#endif
|
||||
|
||||
// If true, the generator will manage the default UE navmesh system to rebuild it at the end of a generation.
|
||||
// If false, the generator will do nothing with the navigation system.
|
||||
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
|
||||
bool bRebuildNavmesh {true};
|
||||
|
||||
// Maximum distance (in number of rooms) at which a room is considered relevant for a player.
|
||||
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
|
||||
int32 RoomRelevanceMaxDistance {5};
|
||||
|
||||
EGenerationState CurrentState {EGenerationState::Idle};
|
||||
EGeneratorFlags Flags {EGeneratorFlags::None};
|
||||
|
||||
// Set to avoid adding increment the seed after we've set manually the seed
|
||||
bool bShouldIncrement {false};
|
||||
|
||||
struct FPlayerRooms
|
||||
{
|
||||
// The rooms the player has left this frame
|
||||
TSet<URoom*> OldRooms;
|
||||
// The rooms the player is currently inside this frame
|
||||
TSet<URoom*> CurrentRooms;
|
||||
// Whether the current rooms has changed this frame
|
||||
bool bHasChanged {false};
|
||||
|
||||
// Move current rooms to old rooms and clear current rooms
|
||||
void Roll()
|
||||
{
|
||||
OldRooms = MoveTemp(CurrentRooms);
|
||||
CurrentRooms.Empty();
|
||||
}
|
||||
|
||||
void AddCurrentRoom(URoom* Room)
|
||||
{
|
||||
OldRooms.Remove(Room);
|
||||
CurrentRooms.Add(Room);
|
||||
}
|
||||
};
|
||||
|
||||
// Occlusion culling system
|
||||
TMap<int32, FPlayerRooms> PlayerRooms;
|
||||
|
||||
// Transient. Only used to detect when occlusion setting is changed.
|
||||
bool bWasOcclusionEnabled {false};
|
||||
|
||||
// Transient. Only used to detect when occlusion distance is changed.
|
||||
uint32 PreviousOcclusionDistance {0};
|
||||
|
||||
// Transient. Used to count unloaded/loaded/initialized rooms during generation.
|
||||
int32 CachedTmpRoomCount {0};
|
||||
|
||||
// Transient. Cached collision params used when bUseWorldCollisionChecks is true
|
||||
FCollisionQueryParams WorldCollisionParams;
|
||||
|
||||
// Transient. Current generation status
|
||||
EGenerationStatus GenerationStatus {EGenerationStatus::NotStarted};
|
||||
};
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ReplicableObject.h"
|
||||
#include "Interfaces/RoomContainer.h"
|
||||
#include "Interfaces/GeneratorProvider.h"
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
#include "Templates/Function.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "VoxelBounds/VoxelBounds.h"
|
||||
#include "DungeonOctree.h"
|
||||
#include "DungeonGraph.generated.h"
|
||||
|
||||
class URoom;
|
||||
class URoomData;
|
||||
class URoomCustomData;
|
||||
class URoomConnection;
|
||||
class ADungeonGeneratorBase;
|
||||
|
||||
// Holds the generated dungeon.
|
||||
// You can access the rooms using many functions.
|
||||
UCLASS(BlueprintType)
|
||||
class PROCEDURALDUNGEON_API UDungeonGraph : public UReplicableObject, public IRoomContainer, public IGeneratorProvider, public IDungeonCustomSerialization, public IDungeonSaveInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
friend ADungeonGeneratorBase;
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
friend class FDungeonGraphTest;
|
||||
#endif
|
||||
|
||||
public:
|
||||
UDungeonGraph();
|
||||
|
||||
//~ Begin IRoomContainer Interface
|
||||
// Returns the room instance with the provided index.
|
||||
// Returns null if no room exists with the provided index.
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
virtual URoom* GetRoomByIndex(int64 Index) const final;
|
||||
|
||||
virtual URoomConnection* GetConnectionByIndex(int32 Index) const override;
|
||||
//~ End IRoomContainer Interface
|
||||
|
||||
//~ Begin IDungeonCustomSerialization Interface
|
||||
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override;
|
||||
//~ End IDungeonCustomSerialization Interface
|
||||
|
||||
//~ Begin IDungeonSaveInterface Interface
|
||||
virtual void PostLoadDungeon_Implementation() override;
|
||||
//~ End IDungeonSaveInterface Interface
|
||||
|
||||
//~ Begin IGeneratorProvider Interface
|
||||
virtual ADungeonGeneratorBase* GetGenerator() const override { return Generator.Get(); }
|
||||
//~ End IGeneratorProvider Interface
|
||||
|
||||
void AddRoom(URoom* Room);
|
||||
void InitRooms();
|
||||
void Clear();
|
||||
|
||||
bool CanRoomFit(const URoom* Room) const;
|
||||
bool TryConnectDoor(URoom* Room, int32 DoorIndex);
|
||||
bool TryConnectToExistingDoors(URoom* Room);
|
||||
|
||||
TArray<URoom*> GetAllRoomsOverlapping(const FBox& Box) const;
|
||||
|
||||
bool HasRooms() const { return Rooms.Num() > 0; }
|
||||
bool IsDirty() const { return bIsDirty; }
|
||||
|
||||
// Returns all rooms
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
const TArray<URoom*>& GetAllRooms() const { return Rooms; }
|
||||
|
||||
// Returns all room connections
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
const TArray<URoomConnection*>& GetAllConnections() const { return RoomConnections; }
|
||||
|
||||
// Returns all room instances of the provided room data
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
|
||||
void GetAllRoomsFromData(const URoomData* Data, TArray<URoom*>& Rooms);
|
||||
|
||||
// Returns all room instances of any of the provided room data
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
|
||||
void GetAllRoomsFromDataList(const TArray<URoomData*>& Data, TArray<URoom*>& Rooms);
|
||||
|
||||
// Returns the first found room instance of the provided room data
|
||||
// (no defined order, so could be any room of the dungeon)
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
|
||||
const URoom* GetFirstRoomFromData(const URoomData* Data);
|
||||
|
||||
// Returns all room instances having the provided custom data
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph", meta = (AutoCreateRefTerm = "CustomData"))
|
||||
void GetAllRoomsWithCustomData(const TSubclassOf<URoomCustomData>& CustomData, TArray<URoom*>& Rooms);
|
||||
|
||||
// Returns all room instances having ALL the provided custom data
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
|
||||
void GetAllRoomsWithAllCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& Rooms);
|
||||
|
||||
// Returns all room instances having at least one of the provided custom data
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
|
||||
void GetAllRoomsWithAnyCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& Rooms);
|
||||
|
||||
// Returns a random room from an array of room
|
||||
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
|
||||
URoom* GetRandomRoom(const TArray<URoom*>& RoomList) const;
|
||||
|
||||
// Returns the total number of room
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph", meta = (CompactNodeTitle = "Count"))
|
||||
int32 Count() const { return Rooms.Num(); }
|
||||
|
||||
// Returns true if a specific RoomData is already in the dungeon
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
bool HasAlreadyRoomData(const URoomData* RoomData) const;
|
||||
|
||||
// Returns true if at least one of the RoomData from the list provided is already in the dungeon
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
bool HasAlreadyOneRoomDataFrom(const TArray<URoomData*>& RoomDataList) const;
|
||||
|
||||
// Returns the number of a specific RoomData in the dungeon
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
int CountRoomData(const URoomData* RoomData) const;
|
||||
|
||||
// Returns the total number of RoomData in the dungeon from the list provided
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
int CountTotalRoomData(const TArray<URoomData*>& RoomDataList) const;
|
||||
|
||||
// Returns true if a specific RoomData type is already in the dungeon
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph", meta = (AutoCreateRefTerm = "RoomType"))
|
||||
bool HasAlreadyRoomType(const TSubclassOf<URoomData>& RoomType) const;
|
||||
|
||||
// Returns true if at least one of the RoomData type from the list provided is already in the dungeon
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
bool HasAlreadyOneRoomTypeFrom(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const;
|
||||
|
||||
// Returns the number of a specific RoomData type in the dungeon
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph", meta = (AutoCreateRefTerm = "RoomType"))
|
||||
int CountRoomType(const TSubclassOf<URoomData>& RoomType) const;
|
||||
|
||||
// Returns the total number of RoomData type in the dungeon from the list provided
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
int CountTotalRoomType(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const;
|
||||
|
||||
// Returns wether a path is valid between 2 rooms (no locked room blocking the way)
|
||||
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
|
||||
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (ReturnDisplayName = "Yes"))
|
||||
bool HasValidPath(const URoom* From, const URoom* To, bool IgnoreLockedRooms = false) const;
|
||||
|
||||
// Returns the minimum number of connected rooms between A and B.
|
||||
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
|
||||
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph")
|
||||
int32 NumberOfRoomBetween(const URoom* A, const URoom* B, bool IgnoreLockedRooms = false) const;
|
||||
|
||||
// Returns the minimum number of connected rooms between A and B.
|
||||
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
|
||||
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (DisplayName = "Number Of Room Between (using ReadOnlyRoom)"))
|
||||
int32 NumberOfRoomBetween_ReadOnly(TScriptInterface<IReadOnlyRoom> A, TScriptInterface<IReadOnlyRoom> B) const;
|
||||
|
||||
// Returns the path between A and B.
|
||||
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
|
||||
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (ReturnDisplayName = "Has Path"))
|
||||
bool GetPathBetween(const URoom* A, const URoom* B, TArray<URoom*>& ResultPath, bool IgnoreLockedRooms = false) const;
|
||||
|
||||
// Returns the room instance at the provided room cell (expressed in Room Units, not Unreal Units!!!).
|
||||
// Returns null if no room located at the provided cell.
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
URoom* GetRoomAt(FIntVector RoomCell) const;
|
||||
|
||||
// Returns the center of the bounding box of the dungeon.
|
||||
// @see GetDungeonBoundsExtents
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
FVector GetDungeonBoundsCenter() const;
|
||||
|
||||
// Returns the extent (half size) of the bounding box of the dungeon.
|
||||
// @see GetDungeonBoundsCenter
|
||||
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
|
||||
FVector GetDungeonBoundsExtent() const;
|
||||
|
||||
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (ExpandBoolAsExecs = "ReturnValue", AdvancedDisplay = "CustomFilter", AutoCreateRefTerm = "CustomScore"))
|
||||
bool FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms, const FScoreCallback& CustomScore) const;
|
||||
bool FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms) const;
|
||||
|
||||
// Returns the computed dungeon bounds.
|
||||
class FBoxCenterAndExtent GetDungeonBounds(const FTransform& Transform = FTransform::Identity) const;
|
||||
struct FBoxMinAndMax GetIntBounds() const;
|
||||
FVoxelBounds GetVoxelBounds() const { return Bounds; }
|
||||
|
||||
// Returns in OutRooms all the rooms in the Distance from each InRooms and optionally apply Func on each rooms.
|
||||
// Distance is the number of room connection between 2 rooms, not the distance in any unit.
|
||||
static void TraverseRooms(const TSet<URoom*>& InRooms, TSet<URoom*>* OutRooms, uint32 Distance, TFunction<void(URoom*, uint32)> Func);
|
||||
|
||||
static bool FindPath(const URoom* From, const URoom* To, TArray<const URoom*>* OutPath = nullptr, bool IgnoreLocked = false);
|
||||
|
||||
protected:
|
||||
int CountRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const;
|
||||
void GetRoomsByPredicate(TArray<URoom*>& OutRooms, TFunction<bool(const URoom*)> Predicate) const;
|
||||
const URoom* FindFirstRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const;
|
||||
|
||||
//~ Begin UReplicableObject Interface
|
||||
virtual bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) override;
|
||||
virtual void RegisterReplicableSubobjects(bool bRegister) override;
|
||||
//~ End UReplicableObject Interface
|
||||
|
||||
// Sync Rooms and ReplicatedRooms arrays
|
||||
void SynchronizeRooms();
|
||||
|
||||
// Replace existing room array from the one loaded in saved data.
|
||||
// This does nothing if there is no data loaded from a saved dungeon.
|
||||
void RetrieveRoomsFromLoadedData();
|
||||
|
||||
// Create and store a new connection between two rooms in RoomConnections.
|
||||
void Connect(URoom* RoomA, int32 DoorA, URoom* RoomB, int32 DoorB);
|
||||
|
||||
bool AreRoomsLoaded(int32& NbRoomLoaded) const;
|
||||
bool AreRoomsUnloaded(int32& NbRoomUnloaded) const;
|
||||
bool AreRoomsInitialized(int32& NbRoomInitialized) const;
|
||||
bool AreRoomsReady() const;
|
||||
|
||||
void SpawnAllDoors();
|
||||
void LoadAllRooms();
|
||||
void UnloadAllRooms();
|
||||
|
||||
void MarkDirty() { bIsDirty = true; }
|
||||
|
||||
// Extends the bounds if necessary to include the provided room.
|
||||
void UpdateBounds(const URoom* Room);
|
||||
|
||||
// Recreate the bounds using the whole room list.
|
||||
void RebuildBounds();
|
||||
|
||||
void UpdateOctree(URoom* Room);
|
||||
void RebuildOctree();
|
||||
|
||||
private:
|
||||
UPROPERTY(Transient)
|
||||
TArray<URoom*> Rooms;
|
||||
|
||||
UPROPERTY(Replicated, Transient)
|
||||
TArray<URoomConnection*> RoomConnections;
|
||||
|
||||
// This array is synchronized with the server
|
||||
// We keep it separated to be able to unload previous rooms on clients
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Rooms, Transient)
|
||||
TArray<URoom*> ReplicatedRooms;
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_Rooms();
|
||||
|
||||
bool bIsDirty {false};
|
||||
|
||||
// @TODO: Make something to decouple the ADungeonGenerator from the UDungeonGraph.
|
||||
// It is currently used only to get its random stream in the `Get Random Room` function.
|
||||
// We could instead either:
|
||||
// - Use an interface that provides a random stream => good way to not induce breaking changes in the code.
|
||||
// - Pass the random stream as an input to that function => will need to make some changes in existing projects.
|
||||
TWeakObjectPtr<ADungeonGeneratorBase> Generator {nullptr};
|
||||
|
||||
// Transient. The computed bounds of the dungeon. Updated each time the room list changes.
|
||||
FVoxelBounds Bounds;
|
||||
|
||||
// Transient, used for room collision checks.
|
||||
FDungeonOctree Octree;
|
||||
|
||||
private:
|
||||
struct FSaveData
|
||||
{
|
||||
TArray<URoom*> Rooms;
|
||||
TArray<URoomConnection*> Connections;
|
||||
};
|
||||
|
||||
TUniquePtr<FSaveData> SavedData {nullptr};
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Math/GenericOctree.h"
|
||||
#include "Misc/EngineVersionComparison.h"
|
||||
|
||||
#if UE_VERSION_OLDER_THAN(4, 26, 0)
|
||||
#define USE_LEGACY_OCTREE 1
|
||||
#else
|
||||
#define USE_LEGACY_OCTREE 0
|
||||
#endif
|
||||
|
||||
class URoom;
|
||||
|
||||
struct FDungeonOctreeElement
|
||||
{
|
||||
URoom* Room;
|
||||
FBoxCenterAndExtent Bounds;
|
||||
int32 Index;
|
||||
|
||||
FDungeonOctreeElement(URoom* Room, int32 BoxIndex);
|
||||
};
|
||||
|
||||
struct FDungeonOctreeSemantics
|
||||
{
|
||||
enum { MaxElementsPerLeaf = 16 };
|
||||
enum { MinInclusiveElementsPerNode = 7 };
|
||||
enum { MaxNodeDepth = 12 };
|
||||
|
||||
typedef TInlineAllocator<MaxElementsPerLeaf> ElementAllocator;
|
||||
|
||||
FORCEINLINE static const FBoxCenterAndExtent& GetBoundingBox(const FDungeonOctreeElement& Element)
|
||||
{
|
||||
return Element.Bounds;
|
||||
}
|
||||
|
||||
FORCEINLINE static bool AreElementsEqual(const FDungeonOctreeElement& A, const FDungeonOctreeElement& B)
|
||||
{
|
||||
return A.Room == B.Room && A.Index == B.Index;
|
||||
}
|
||||
|
||||
FORCEINLINE static void SetElementId(const FDungeonOctreeElement& Element
|
||||
#if USE_LEGACY_OCTREE
|
||||
, FOctreeElementId Id)
|
||||
#else
|
||||
, FOctreeElementId2 Id)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
FORCEINLINE static void ApplyOffset(FDungeonOctreeElement& Element, FVector Offset)
|
||||
{
|
||||
Element.Bounds.Center += Offset;
|
||||
}
|
||||
};
|
||||
|
||||
using FDungeonOctree =
|
||||
#if USE_LEGACY_OCTREE
|
||||
TOctree<FDungeonOctreeElement, FDungeonOctreeSemantics>;
|
||||
#else
|
||||
TOctree2<FDungeonOctreeElement, FDungeonOctreeSemantics>;
|
||||
#endif
|
||||
|
||||
template<typename IterateBoundsFunc>
|
||||
inline void FindElementsWithBoundsTest(const FDungeonOctree& Octree, const FBoxCenterAndExtent& Bounds, const IterateBoundsFunc& Func)
|
||||
{
|
||||
#if USE_LEGACY_OCTREE
|
||||
for (FDungeonOctree::TConstElementBoxIterator<> OctreeIt(Octree, Bounds); OctreeIt.HasPendingElements(); OctreeIt.Advance())
|
||||
{
|
||||
const FDungeonOctreeElement& Element = OctreeIt.GetCurrentElement();
|
||||
Func(Element);
|
||||
}
|
||||
#else
|
||||
Octree.FindElementsWithBoundsTest(Bounds, Func);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
#include "UObject/SoftObjectPath.h"
|
||||
#include "Serialization/StructuredArchive.h"
|
||||
|
||||
// Archive proxy specialized for the dungeon.
|
||||
struct FDungeonSaveProxyArchive : public FObjectAndNameAsStringProxyArchive
|
||||
{
|
||||
public:
|
||||
FDungeonSaveProxyArchive(FArchive& InInnerArchive)
|
||||
: FObjectAndNameAsStringProxyArchive(InInnerArchive, true)
|
||||
{
|
||||
ArIsSaveGame = true;
|
||||
//ArNoDelta = true;
|
||||
}
|
||||
|
||||
virtual FArchive& operator<<(FSoftObjectPath& Value) override
|
||||
{
|
||||
// Calls Value.SerializePath()
|
||||
FObjectAndNameAsStringProxyArchive::operator<<(Value);
|
||||
|
||||
//UE_LOG(LogTemp, Warning, TEXT("Custom serialization of a SoftObjectPath!"));
|
||||
|
||||
// If we have a defined core redirect, make sure that it's applied
|
||||
if (IsLoading() && !Value.IsNull())
|
||||
{
|
||||
Value.FixupCoreRedirects();
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "DungeonSettings.generated.h"
|
||||
|
||||
// Data asset to allow overriding the plugin's settings
|
||||
UCLASS(BlueprintType)
|
||||
class PROCEDURALDUNGEON_API UDungeonSettings : public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UDungeonSettings();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings")
|
||||
static FVector GetRoomUnit(const UDungeonSettings* Settings = nullptr);
|
||||
|
||||
private:
|
||||
// Size of a room unit. Room's size in data assets will express the multiple of this unit size.
|
||||
// For example a room size of (5, 10, 1) with a unit size of (100, 100, 400) will result of a real room size of (500, 1000, 400).
|
||||
UPROPERTY(EditAnywhere, Category = "General", meta = (ClampMin = 0, AllowPrivateAccess=true))
|
||||
FVector RoomUnit;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright Benoit Pelletier 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "DoorInterface.generated.h"
|
||||
|
||||
class UDoorType;
|
||||
class URoomConnection;
|
||||
|
||||
UINTERFACE(MinimalAPI, Blueprintable, BlueprintType)
|
||||
class UDoorInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to implement on any Actor or ActorComponent used as a Door in the dungeon.
|
||||
*/
|
||||
class PROCEDURALDUNGEON_API IDoorInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Door Interface")
|
||||
const UDoorType* GetDoorType() const;
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Door Interface")
|
||||
void SetRoomConnection(URoomConnection* RoomConnection);
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "DungeonCustomSerialization.generated.h"
|
||||
|
||||
// This class does not need to be modified.
|
||||
UINTERFACE(MinimalAPI, NotBlueprintable, NotBlueprintType, meta = (CannotImplementInterfaceInBlueprint))
|
||||
class UDungeonCustomSerialization : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class PROCEDURALDUNGEON_API IDungeonCustomSerialization
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
|
||||
public:
|
||||
// Serialize non-trivial object properties (e.g. UObject pointers)
|
||||
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) = 0;
|
||||
|
||||
// Fixup object references after loading
|
||||
virtual bool FixupReferences(UObject* Context) { return true; }
|
||||
|
||||
// Calls FixupReferences on Obj and its subobjects.
|
||||
static bool DispatchFixupReferences(UObject* Obj, UObject* Context);
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "DungeonSaveInterface.generated.h"
|
||||
|
||||
UINTERFACE(BlueprintType, Blueprintable, meta = (Tooltip = "Give access to some serialization events to actors saved within a dungeon."))
|
||||
class UDungeonSaveInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to add some events to the saved actors/objects during the save/load process of the dungeon.
|
||||
*/
|
||||
class PROCEDURALDUNGEON_API IDungeonSaveInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Called just before serializing this object into the dungeon save.
|
||||
// Useful to initialize some saved variables based on actor states.
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
|
||||
void DungeonPreSerialize(bool bIsLoading);
|
||||
|
||||
// Called just after deserializing this object from the dungeon save
|
||||
// Useful to initialize some actor states based on saved variables.
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
|
||||
void DungeonPostSerialize(bool bIsLoading);
|
||||
|
||||
// Called first before saving the dungeon
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
|
||||
void PreSaveDungeon();
|
||||
|
||||
// Called last after loading the dungeon
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
|
||||
void PostLoadDungeon();
|
||||
|
||||
static void DispatchPreSaveEvent(UObject* Obj);
|
||||
static void DispatchPostLoadEvent(UObject* Obj);
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "GeneratorProvider.generated.h"
|
||||
|
||||
UINTERFACE(MinimalAPI, NotBlueprintable, NotBlueprintType, meta = (CannotImplementInterfaceInBlueprint))
|
||||
class UGeneratorProvider : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for classes that give access to a ADungeonGeneratorBase instance.
|
||||
* @TODO: Currently only used to resolve URoom::GeneratorOwner references when loading a saved dungeon.
|
||||
* It would be better in a future version to decouple the URoom from the DungeonGenerator and instead
|
||||
* pass some Interface references to the functions needed (currently a Transform and a RandomStream).
|
||||
* I just want to say that this interface is just temporary and must not be used by users of the plugin,
|
||||
* as it will certainly be removed in a near future version of the plugin.
|
||||
*/
|
||||
class IGeneratorProvider
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual class ADungeonGeneratorBase* GetGenerator() const = 0;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "RoomActorGuid.generated.h"
|
||||
|
||||
UINTERFACE(BlueprintType, Blueprintable, meta = (Tooltip = "Interface to access a custom Guid for actors saved within a dungeon."))
|
||||
class URoomActorGuid : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
// Interface for all saveable actors placed in room levels
|
||||
// The guid must be constant across game sessions to be able to save/load the actors.
|
||||
// It can be placed on ActorComponents too, but the interface on the Actor itself will be prioritized.
|
||||
// Only the first component found that implements the interface will be used. Make sure to have only one to prevent any confusions.
|
||||
class PROCEDURALDUNGEON_API IRoomActorGuid
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Return the guid associated with this actor.
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Room Actor Id")
|
||||
FGuid GetGuid() const;
|
||||
|
||||
// Returns true if the actor should be included in the saved dungeon.
|
||||
// Returns false to just use a Guid without the need to include the actor in saved games.
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Room Actor Id")
|
||||
bool ShouldSaveActor() const;
|
||||
|
||||
// Return the object implementing the IRoomActorGuid interface from the provided actor.
|
||||
// It can be implemented on the Actor itself or its components.
|
||||
// If both, the actor implementation will be returned.
|
||||
static UObject* GetImplementer(AActor* Actor);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "RoomContainer.generated.h"
|
||||
|
||||
class URoom;
|
||||
class URoomConnection;
|
||||
|
||||
UINTERFACE(MinimalAPI, NotBlueprintable, NotBlueprintType, meta = (CannotImplementInterfaceInBlueprint))
|
||||
class URoomContainer : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Common interface for all containers that holds rooms and their connections.
|
||||
* Currently used to get back references in URoom and URoomConnection when loaded from a saved dungeon.
|
||||
*/
|
||||
class PROCEDURALDUNGEON_API IRoomContainer
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual URoom* GetRoomByIndex(int64 Index) const = 0;
|
||||
virtual URoomConnection* GetConnectionByIndex(int32 Index) const = 0;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FProceduralDungeonModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
virtual bool SupportsDynamicReloading() override { return true; }
|
||||
|
||||
private:
|
||||
void RegisterSettings();
|
||||
void UnregisterSettings();
|
||||
bool HandleSettingsSaved();
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Misc/Guid.h"
|
||||
|
||||
// Custom serialization version for Procedural Dungeon
|
||||
struct FProceduralDungeonCustomVersion
|
||||
{
|
||||
enum Type
|
||||
{
|
||||
// Before any version changes were made
|
||||
InitialVersion = 0,
|
||||
SoftObjectPtrFix, // Fixed issues with SoftObjectPtr replication in Room.h
|
||||
RoomDataBoundingBoxesMigration, // Migrated FirstPoint/SecondPoint to BoundingBoxes in RoomData.h
|
||||
DoorLogicRefactored, // Migrated logic for the ADoor into DoorComponent + DoorState
|
||||
|
||||
// -----<new versions can be added above this line>-------------------------------------------------
|
||||
VersionPlusOne,
|
||||
LatestVersion = VersionPlusOne - 1
|
||||
};
|
||||
|
||||
// The GUID for this custom version number
|
||||
PROCEDURALDUNGEON_API const static FGuid GUID;
|
||||
|
||||
FProceduralDungeonCustomVersion() = delete;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "ProceduralDungeonSettings.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogProceduralDungeon, Log, All);
|
||||
|
||||
void LogOnScreen(const FString& Message, FColor Color, bool bForceOnScreen = false);
|
||||
|
||||
#if NO_LOGGING
|
||||
#define _DungeonLog_PrivateImpl(...) {}
|
||||
#else
|
||||
// Private implementation. Dot not use it.
|
||||
#define _DungeonLog_PrivateImpl(ShowOnScreen, ForceOnScreen, Detailed, Color, Verbosity, Format, ...) \
|
||||
{ \
|
||||
if constexpr (Detailed) \
|
||||
{ UE_LOG(LogProceduralDungeon, Verbosity, TEXT("[%s:%d] " Format), *FString(__FUNCTION__), __LINE__, ##__VA_ARGS__); } \
|
||||
else \
|
||||
{ UE_LOG(LogProceduralDungeon, Verbosity, TEXT(Format), ##__VA_ARGS__); } \
|
||||
if constexpr (ShowOnScreen) \
|
||||
LogOnScreen(FString::Printf(TEXT(Format), ##__VA_ARGS__), Color, ForceOnScreen); \
|
||||
}
|
||||
#endif // NO_LOGGING
|
||||
|
||||
// Logs error message to output and on screen
|
||||
#define DungeonLog_Debug(Format, ...) \
|
||||
_DungeonLog_PrivateImpl(false, false, true, FColor::White, VeryVerbose, Format, ##__VA_ARGS__)
|
||||
|
||||
// Logs info message to output and on screen
|
||||
#define DungeonLog_Info(Format, ...) \
|
||||
_DungeonLog_PrivateImpl(true, false, false, FColor::White, Log, Format, ##__VA_ARGS__)
|
||||
|
||||
// Logs info message *only* to output
|
||||
#define DungeonLog_InfoSilent(Format, ...) \
|
||||
_DungeonLog_PrivateImpl(false, false, false, FColor::White, Log, Format, ##__VA_ARGS__)
|
||||
|
||||
// Logs warning message to output and on screen
|
||||
#define DungeonLog_Warning(Format, ...) \
|
||||
_DungeonLog_PrivateImpl(true, false, true, FColor::Yellow, Warning, Format, ##__VA_ARGS__)
|
||||
|
||||
// Logs warning message *only* to output
|
||||
#define DungeonLog_WarningSilent(Format, ...) \
|
||||
_DungeonLog_PrivateImpl(false, false, true, FColor::Yellow, Warning, Format, ##__VA_ARGS__)
|
||||
|
||||
// Logs error message to output and on screen
|
||||
#define DungeonLog_Error(Format, ...) \
|
||||
_DungeonLog_PrivateImpl(true, true, true, FColor::Red, Error, Format, ##__VA_ARGS__)
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "Engine/EngineTypes.h"
|
||||
#include "ProceduralDungeonSettings.generated.h"
|
||||
|
||||
// Holds the plugin's settings.
|
||||
UCLASS(Config = Game, DefaultConfig)
|
||||
class PROCEDURALDUNGEON_API UProceduralDungeonSettings : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UProceduralDungeonSettings(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
// Size of a room unit. Room's size in data assets will express the multiple of this unit size.
|
||||
// For example a room size of (5, 10, 1) with a unit size of (100, 100, 400) will result of a real room size of (500, 1000, 400).
|
||||
UPROPERTY(EditAnywhere, config, Category = "General", meta = (ClampMin = 0))
|
||||
FVector RoomUnit;
|
||||
|
||||
// The bounding size of the doors. It is used only to display the door's blue box when "Draw Debug" is set to true.
|
||||
UPROPERTY(EditAnywhere, config, Category = "General", meta = (DisplayName = "Default Door Size", ClampMin = 0))
|
||||
FVector DoorSize;
|
||||
|
||||
// The height of the door's base from the room's base (in percentage of the room unit Z)
|
||||
UPROPERTY(EditAnywhere, config, Category = "General", meta = (DisplayName = "Default Door Offset", ClampMin = 0, ClampMax = 1, UIMin = 0, UIMax = 1))
|
||||
float DoorOffset;
|
||||
|
||||
// When true, doors will be connected as long they are at the same place.
|
||||
// When false, only the doors between the previous and the new generated room will be connected.
|
||||
// DEPRECATED: Keep it true and use the CanLoop setting in the DungeonGenerator actor instead. This project-wide setting will be removed in a future version.
|
||||
UPROPERTY(EditAnywhere, config, Category = "General")
|
||||
bool CanLoop;
|
||||
|
||||
// The object type used for the dungeon rooms trigger boxes.
|
||||
// Defaulted to Engine Trace Channel 6.
|
||||
// You can create new ones in your project settings under the Collision tab.
|
||||
UPROPERTY(EditAnywhere, config, Category = "General")
|
||||
TEnumAsByte<ECollisionChannel> RoomObjectType {ECollisionChannel::ECC_EngineTraceChannel6};
|
||||
|
||||
// The number of dungeon generation retry before the generator gives up.
|
||||
UPROPERTY(EditAnywhere, config, Category = "General", AdvancedDisplay, meta = (UIMin = 1, ClampMin = 1))
|
||||
int32 MaxGenerationTry;
|
||||
|
||||
// The number of room placement retry on a specific door before the generator gives up and continues with the next door.
|
||||
UPROPERTY(EditAnywhere, config, Category = "General", AdvancedDisplay, meta = (UIMin = 1, ClampMin = 1))
|
||||
int32 MaxRoomPlacementTry;
|
||||
|
||||
// The number of room placement retry on a specific door before the generator gives up and continues with the next door.
|
||||
UPROPERTY(EditAnywhere, config, Category = "General", AdvancedDisplay, meta = (UIMin = 1, ClampMin = 1))
|
||||
int32 RoomLimit;
|
||||
|
||||
// The rooms visibility will be toggled off when the player is not inside it or in a room next to it.
|
||||
UPROPERTY(EditAnywhere, config, Category = "Occlusion Culling", meta = (DisplayName = "Enable Occlusion Culling"))
|
||||
bool OcclusionCulling;
|
||||
|
||||
// The legacy occlusion culling system only toggles the visibility of the actors in the rooms, keeping the collisions, ticking and all oher things.
|
||||
// The new system toggles instead the whole room levels visibility, shutting off the ticking and other things of the actors and the level script.
|
||||
//UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon", meta=(EditCondition="OcclusionCulling"))
|
||||
//bool LegacyOcclusion;
|
||||
|
||||
// Defines how many connected rooms are visible from the player's room (1 means only the room where the player is).
|
||||
UPROPERTY(EditAnywhere, config, Category = "Occlusion Culling", meta = (EditCondition = "OcclusionCulling", UIMin = 1, ClampMin = 1))
|
||||
int32 OcclusionDistance;
|
||||
|
||||
// Keep track of dynamic actors entering and leaving the room to be able to show/hide them with the room.
|
||||
// TODO: Still useful? It was there for performance issues, but there is none anymore...
|
||||
// Maybe moving it in a console variable only for debug purpose?
|
||||
UPROPERTY(EditAnywhere, config, Category = "Occlusion Culling", meta = (EditCondition = "OcclusionCulling"))
|
||||
bool OccludeDynamicActors;
|
||||
|
||||
// Show room and door outlines in editor and development builds
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug")
|
||||
bool DrawDebug;
|
||||
|
||||
// Show room and door outlines in editor and development builds
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
|
||||
bool bDrawOnlyWhenEditingRooms;
|
||||
|
||||
// Show the room origin in magenta
|
||||
// DEPRECATED: This setting will be removed in a future release of the plugin.
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
|
||||
bool ShowRoomOrigin;
|
||||
|
||||
// Flip side the arrow that shows door facing direction.
|
||||
// True means that the arrow gets inside the room (opposite of door actor's forward).
|
||||
// False means that the arrow goes outside the room (same as door actor's forward).
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
|
||||
bool bFlipDoorArrowSide;
|
||||
|
||||
// Length of the door's debug arrow.
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
|
||||
float DoorArrowLength;
|
||||
|
||||
// Size of the door's debug arrow head.
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
|
||||
float DoorArrowHeadSize;
|
||||
|
||||
// Show some logs on the screen
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug")
|
||||
bool OnScreenPrintDebug;
|
||||
|
||||
// Duration of the screen logs
|
||||
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "OnScreenPrintDebug"))
|
||||
float PrintDebugDuration;
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Math/GenericOctree.h" // FBoxCenterAndExtent
|
||||
#include "ProceduralDungeonTypes.generated.h"
|
||||
|
||||
UENUM()
|
||||
enum class EGenerationState : uint8
|
||||
{
|
||||
Idle UMETA(DisplayName = "Idle"),
|
||||
Generation UMETA(DisplayName = "Generation"),
|
||||
Load UMETA(DisplayName = "Load"),
|
||||
Initialization UMETA(DisplayName = "Initialization"),
|
||||
Unload UMETA(DisplayName = "Unload"),
|
||||
Play UMETA(DisplayName = "Play"),
|
||||
NbState UMETA(Hidden)
|
||||
};
|
||||
|
||||
UENUM()
|
||||
enum class EGenerationStatus : uint8
|
||||
{
|
||||
NotStarted UMETA(DisplayName = "Not Started"),
|
||||
InProgress UMETA(DisplayName = "In Progress"),
|
||||
Completed UMETA(DisplayName = "Completed"),
|
||||
Failed UMETA(DisplayName = "Failed"),
|
||||
NbStatus UMETA(Hidden)
|
||||
};
|
||||
|
||||
// The different directions a door can face.
|
||||
UENUM(BlueprintType, meta = (DisplayName = "Door Direction"))
|
||||
enum class EDoorDirection : uint8
|
||||
{
|
||||
North = 0 UMETA(DisplayName = "North", ToolTip = "rotation = 0 | positive X (world forward)"),
|
||||
East = 1 UMETA(DisplayName = "East", ToolTip = "rotation = 90 | positive Y (world right)"),
|
||||
South = 2 UMETA(DisplayName = "South", ToolTip = "rotation = 180 | negative X (world backward)"),
|
||||
West = 3 UMETA(DisplayName = "West", ToolTip = "rotation = 270 | negative Y (world left)"),
|
||||
NbDirection = 4 UMETA(Hidden)
|
||||
};
|
||||
|
||||
bool PROCEDURALDUNGEON_API operator!(const EDoorDirection& Direction);
|
||||
EDoorDirection PROCEDURALDUNGEON_API operator+(const EDoorDirection& A, const EDoorDirection& B);
|
||||
EDoorDirection PROCEDURALDUNGEON_API operator-(const EDoorDirection& A, const EDoorDirection& B);
|
||||
// TODO: Don't know how to export these...
|
||||
EDoorDirection& operator+=(EDoorDirection& A, const EDoorDirection& B);
|
||||
EDoorDirection& operator-=(EDoorDirection& A, const EDoorDirection& B);
|
||||
EDoorDirection& operator++(EDoorDirection& Direction);
|
||||
EDoorDirection& operator--(EDoorDirection& Direction);
|
||||
EDoorDirection PROCEDURALDUNGEON_API operator++(EDoorDirection& Direction, int);
|
||||
EDoorDirection PROCEDURALDUNGEON_API operator--(EDoorDirection& Direction, int);
|
||||
EDoorDirection PROCEDURALDUNGEON_API operator-(const EDoorDirection& Direction);
|
||||
EDoorDirection PROCEDURALDUNGEON_API operator~(const EDoorDirection& Direction);
|
||||
inline EDoorDirection PROCEDURALDUNGEON_API Opposite(const EDoorDirection& Direction) { return ~Direction; }
|
||||
FIntVector PROCEDURALDUNGEON_API ToIntVector(const EDoorDirection& Direction);
|
||||
FVector PROCEDURALDUNGEON_API ToVector(const EDoorDirection& Direction);
|
||||
FQuat PROCEDURALDUNGEON_API ToQuaternion(const EDoorDirection& Direction);
|
||||
float PROCEDURALDUNGEON_API ToAngle(const EDoorDirection& Direction);
|
||||
FIntVector PROCEDURALDUNGEON_API Rotate(const FIntVector& Pos, const EDoorDirection& Rot);
|
||||
FVector PROCEDURALDUNGEON_API Rotate(const FVector& Pos, const EDoorDirection& Rot);
|
||||
|
||||
FIntVector PROCEDURALDUNGEON_API Transform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation);
|
||||
FIntVector PROCEDURALDUNGEON_API InverseTransform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation);
|
||||
|
||||
// Those ones are just for consistent naming and centralized code
|
||||
EDoorDirection PROCEDURALDUNGEON_API Transform(const EDoorDirection& Direction, const EDoorDirection& Rotation);
|
||||
EDoorDirection PROCEDURALDUNGEON_API InverseTransform(const EDoorDirection& Direction, const EDoorDirection& Rotation);
|
||||
|
||||
//The different types of generation algorithms.
|
||||
UENUM(BlueprintType, meta = (DisplayName = "Generation Type"))
|
||||
enum class EGenerationType : uint8
|
||||
{
|
||||
DFS = 0 UMETA(DisplayName = "Depth First", Tooltip = "Make the dungeon more linear"),
|
||||
BFS = 1 UMETA(DisplayName = "Breadth First", Tooltip = "Make the dungeon less linear"),
|
||||
NbType = 2 UMETA(Hidden)
|
||||
};
|
||||
|
||||
// The different types of seed update at each generation.
|
||||
UENUM(BlueprintType, meta = (DisplayName = "Seed Type"))
|
||||
enum class ESeedType : uint8
|
||||
{
|
||||
Random = 0 UMETA(DisplayName = "Random", Tooltip = "Random seed at each generation"),
|
||||
AutoIncrement = 1 UMETA(DisplayName = "Auto Increment", Tooltip = "Get the initial seed and increment at each generation"),
|
||||
Fixed = 2 UMETA(DisplayName = "Fixed", Tooltip = "Always use initial seed (or you can set it manually via blueprint)"),
|
||||
NbType = 3 UMETA(Hidden)
|
||||
};
|
||||
|
||||
// Visibility mode for Room Visibilty Components.
|
||||
UENUM(BlueprintType, meta = (DisplayName = "Room Visibility"))
|
||||
enum class EVisibilityMode : uint8
|
||||
{
|
||||
Default UMETA(DisplayName = "Same As Room"),
|
||||
ForceVisible UMETA(DisplayName = "Force Visible"),
|
||||
ForceHidden UMETA(DisplayName = "Force Hidden"),
|
||||
Custom UMETA(DisplayName = "Custom"),
|
||||
NbMode UMETA(Hidden)
|
||||
};
|
||||
|
||||
// Structure that defines a door.
|
||||
// A door is defined by its position, its direction, and its type.
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROCEDURALDUNGEON_API FDoorDef
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
static const FDoorDef Invalid;
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DoorDef")
|
||||
FIntVector Position {FIntVector::ZeroValue};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DoorDef")
|
||||
EDoorDirection Direction {EDoorDirection::North};
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DoorDef", meta = (DisplayThumbnail = false))
|
||||
class UDoorType* Type {nullptr};
|
||||
|
||||
public:
|
||||
FDoorDef() = default;
|
||||
FDoorDef(const FIntVector& InPosition, EDoorDirection InDirection, class UDoorType* InType = nullptr);
|
||||
|
||||
bool IsValid() const;
|
||||
operator bool() const { return IsValid(); }
|
||||
bool operator==(const FDoorDef& Other) const;
|
||||
|
||||
static bool AreCompatible(const FDoorDef& A, const FDoorDef& B);
|
||||
|
||||
FVector GetDoorSize() const;
|
||||
float GetDoorOffset() const;
|
||||
FColor GetDoorColor() const;
|
||||
FString GetTypeName() const;
|
||||
FString ToString() const;
|
||||
FDoorDef GetOpposite() const;
|
||||
FBoxCenterAndExtent GetBounds(const FVector RoomUnit, bool bIncludeOffset = true) const;
|
||||
|
||||
static FVector GetRealDoorPosition(const FDoorDef& DoorDef, const FVector RoomUnit, bool bIncludeOffset = true);
|
||||
static FVector GetRealDoorPosition(FIntVector DoorCell, EDoorDirection DoorRot, const FVector RoomUnit, float DoorOffset = 0.0f);
|
||||
static FQuat GetRealDoorRotation(const FDoorDef& DoorDef, bool bFlipped = false);
|
||||
|
||||
static FDoorDef Transform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation);
|
||||
static FDoorDef InverseTransform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation);
|
||||
|
||||
#if !UE_BUILD_SHIPPING
|
||||
static void DrawDebug(const class UWorld* World, const FDoorDef& DoorDef, const FVector RoomUnit, const FTransform& Transform = FTransform::Identity, bool bIncludeOffset = false, bool bIsConnected = true);
|
||||
static void DrawDebug(const class UWorld* World, const FColor& Color, const FVector& DoorSize, const FVector RoomUnit, const FIntVector& DoorCell = FIntVector::ZeroValue, const EDoorDirection& DoorRot = EDoorDirection::NbDirection, const FTransform& Transform = FTransform::Identity, float DoorOffset = 0.0f, bool bIsConnected = true);
|
||||
#endif // !UE_BUILD_SHIPPING
|
||||
};
|
||||
|
||||
// TODO: Use UE built-in TBox<FIntVector> instead?
|
||||
// The downside of doing that would be the Center and Extent computation that is slightly different...
|
||||
// Also, the IsInside with another box does not consider coincident faces as inside...
|
||||
// Also, operators + and += don't mean the same (extending box to include a point instead of shifting the box)...
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROCEDURALDUNGEON_API FBoxMinAndMax
|
||||
{
|
||||
GENERATED_BODY();
|
||||
|
||||
protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Box")
|
||||
FIntVector Min {0};
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Box")
|
||||
FIntVector Max {1};
|
||||
|
||||
public:
|
||||
FBoxMinAndMax() = default;
|
||||
FBoxMinAndMax(const FIntVector& A, const FIntVector& B);
|
||||
|
||||
void SetMinAndMax(const FIntVector& A, const FIntVector& B);
|
||||
FIntVector GetMin() const { return Min; }
|
||||
FIntVector GetMax() const { return Max; }
|
||||
|
||||
bool IsValid() const;
|
||||
FIntVector GetSize() const;
|
||||
FBoxCenterAndExtent ToCenterAndExtent() const;
|
||||
bool IsInside(const FIntVector& Cell) const;
|
||||
bool IsInside(const FBoxMinAndMax& Other) const;
|
||||
void Rotate(const EDoorDirection& Rot);
|
||||
void Extend(const FBoxMinAndMax& Other);
|
||||
FString ToString() const;
|
||||
FIntVector GetClosestPoint(const FIntVector& Point) const;
|
||||
|
||||
static bool Overlap(const FBoxMinAndMax& A, const FBoxMinAndMax& B);
|
||||
|
||||
FBoxMinAndMax& operator+=(const FIntVector& X);
|
||||
FBoxMinAndMax& operator-=(const FIntVector& X);
|
||||
FBoxMinAndMax operator+(const FIntVector& X) const;
|
||||
FBoxMinAndMax operator-(const FIntVector& X) const;
|
||||
bool operator==(const FBoxMinAndMax& Other) const;
|
||||
bool operator!=(const FBoxMinAndMax& Other) const;
|
||||
|
||||
public:
|
||||
static const FBoxMinAndMax Invalid;
|
||||
};
|
||||
|
||||
FBoxMinAndMax PROCEDURALDUNGEON_API Rotate(const FBoxMinAndMax& Box, const EDoorDirection& Rot);
|
||||
|
||||
// Describe a potential room to be added to the dungeon.
|
||||
// Mainly used by FilterAndSortRooms function.
|
||||
USTRUCT(BlueprintType)
|
||||
struct PROCEDURALDUNGEON_API FRoomCandidate
|
||||
{
|
||||
GENERATED_BODY();
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Room Candidate")
|
||||
class URoomData* Data {nullptr};
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Room Candidate")
|
||||
int32 DoorIndex {-1};
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Room Candidate")
|
||||
int32 Score {-1};
|
||||
|
||||
public:
|
||||
static FRoomCandidate Invalid;
|
||||
};
|
||||
|
||||
USTRUCT()
|
||||
struct FDoorState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(SaveGame)
|
||||
bool bIsLocked {false};
|
||||
|
||||
UPROPERTY(SaveGame)
|
||||
bool bIsOpen {false};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Math/Vector.h"
|
||||
#include "Math/IntVector.h"
|
||||
#include "Engine/EngineTypes.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Utils/CompatUtils.h"
|
||||
|
||||
namespace IntVector
|
||||
{
|
||||
// Returns the component-wise minimum of A and B
|
||||
FIntVector PROCEDURALDUNGEON_API Min(const FIntVector& A, const FIntVector& B);
|
||||
|
||||
// Returns the component-wise maximum of A and B
|
||||
FIntVector PROCEDURALDUNGEON_API Max(const FIntVector& A, const FIntVector& B);
|
||||
|
||||
// Returns the component-wise minimum and maximum of A and B
|
||||
void PROCEDURALDUNGEON_API MinMax(const FIntVector& A, const FIntVector& B, FIntVector& OutMin, FIntVector& OutMax);
|
||||
} //namespace IntVector
|
||||
|
||||
class FBoxCenterAndExtent;
|
||||
struct FBoxMinAndMax;
|
||||
|
||||
namespace Dungeon
|
||||
{
|
||||
// Returns the real world location of a point in room units
|
||||
FVector PROCEDURALDUNGEON_API ToWorldLocation(FIntVector RoomPoint, const FVector RoomUnit);
|
||||
|
||||
// Returns the real world vector (no offset) of a vector in room units
|
||||
FVector PROCEDURALDUNGEON_API ToWorldVector(FIntVector RoomVector, const FVector RoomUnit);
|
||||
|
||||
// Convertthe Box from dungeon coordinate to world coordinate, applying an optional transform on it.
|
||||
FBoxCenterAndExtent PROCEDURALDUNGEON_API ToWorld(const FBoxMinAndMax& Box, const FVector RoomUnit, const FTransform& Transform = FTransform::Identity);
|
||||
|
||||
// Convertthe Box from dungeon coordinate to world coordinate, applying an optional transform on it.
|
||||
FBoxCenterAndExtent PROCEDURALDUNGEON_API ToWorld(const FBoxCenterAndExtent& Box, const FVector RoomUnit, const FTransform& Transform = FTransform::Identity);
|
||||
|
||||
// Returns the location in room units from a point in real world
|
||||
FIntVector PROCEDURALDUNGEON_API ToRoomLocation(FVector WorldPoint, const FVector RoomUnit);
|
||||
|
||||
// Returns the vector (no offset) in room units from a vector in real world
|
||||
FIntVector PROCEDURALDUNGEON_API ToRoomVector(FVector WorldVector, const FVector RoomUnit);
|
||||
|
||||
// Returns the real world snapped location to the nearest point in room units from a real world point
|
||||
FVector PROCEDURALDUNGEON_API SnapPoint(FVector Point, const FVector RoomUnit);
|
||||
|
||||
template<typename T>
|
||||
int GetTotalWeight(const TMap<T, int>& WeightMap)
|
||||
{
|
||||
int Total = 0;
|
||||
for (const auto& Pair : WeightMap)
|
||||
{
|
||||
Total += Pair.Value;
|
||||
}
|
||||
return Total;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T GetWeightedAt(const TMap<T, int>& WeightMap, int Index)
|
||||
{
|
||||
if (Index < 0)
|
||||
return T();
|
||||
|
||||
int Current = 0;
|
||||
for (const auto& Pair : WeightMap)
|
||||
{
|
||||
Current += Pair.Value;
|
||||
if (Current > Index)
|
||||
return Pair.Key;
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
// ===== Plugin's Settings =====
|
||||
|
||||
FVector PROCEDURALDUNGEON_API RoomUnit();
|
||||
FVector PROCEDURALDUNGEON_API DefaultDoorSize();
|
||||
FColor PROCEDURALDUNGEON_API DefaultDoorColor();
|
||||
float PROCEDURALDUNGEON_API DoorOffset();
|
||||
bool PROCEDURALDUNGEON_API OcclusionCulling();
|
||||
bool PROCEDURALDUNGEON_API UseLegacyOcclusion();
|
||||
uint32 PROCEDURALDUNGEON_API OcclusionDistance();
|
||||
bool PROCEDURALDUNGEON_API OccludeDynamicActors();
|
||||
bool PROCEDURALDUNGEON_API DrawDebug();
|
||||
bool PROCEDURALDUNGEON_API DrawOnlyWhenEditingRoom();
|
||||
bool PROCEDURALDUNGEON_API ShowRoomOrigin();
|
||||
bool PROCEDURALDUNGEON_API FlipDoorArrow();
|
||||
float PROCEDURALDUNGEON_API DoorArrowLength();
|
||||
float PROCEDURALDUNGEON_API DoorArrowHeadSize();
|
||||
bool PROCEDURALDUNGEON_API CanLoop();
|
||||
ECollisionChannel PROCEDURALDUNGEON_API RoomObjectType();
|
||||
uint32 PROCEDURALDUNGEON_API MaxGenerationTryBeforeGivingUp();
|
||||
uint32 PROCEDURALDUNGEON_API MaxRoomPlacementTryBeforeGivingUp();
|
||||
int32 PROCEDURALDUNGEON_API RoomLimit();
|
||||
|
||||
void PROCEDURALDUNGEON_API EnableOcclusionCulling(bool Enable);
|
||||
void PROCEDURALDUNGEON_API SetOcclusionDistance(int32 Distance);
|
||||
} //namespace Dungeon
|
||||
|
||||
namespace Random
|
||||
{
|
||||
uint32 PROCEDURALDUNGEON_API Guid2Seed(FGuid Guid, int64 Salt);
|
||||
}
|
||||
|
||||
namespace WorldUtils
|
||||
{
|
||||
template<class T UE_REQUIRES(TIsDerivedFrom<T, AActor>::Value)>
|
||||
void FindAllActors(UWorld* InWorld, TArray<T*>& OutActors)
|
||||
{
|
||||
OutActors.Empty();
|
||||
for (TActorIterator<T> It(InWorld); It; ++It)
|
||||
{
|
||||
T* Actor = *It;
|
||||
OutActors.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T UE_REQUIRES(TIsDerivedFrom<T, AActor>::Value)>
|
||||
void FindAllActorsByPredicate(UWorld* InWorld, TArray<T*>& OutActors, TFunction<bool(const T*)> Predicate)
|
||||
{
|
||||
OutActors.Empty();
|
||||
for (TActorIterator<T> It(InWorld); It; ++It)
|
||||
{
|
||||
T* Actor = *It;
|
||||
if (Predicate(Actor))
|
||||
{
|
||||
OutActors.Add(Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename U, class T UE_REQUIRES(TIsDerivedFrom<T, AActor>::Value)>
|
||||
void MapActors(UWorld* InWorld, TMap<U, T*>& OutActorMap, TFunction<U(const T*)> MapFunction)
|
||||
{
|
||||
OutActorMap.Empty();
|
||||
for (TActorIterator<T> It(InWorld); It; ++It)
|
||||
{
|
||||
T* Actor = *It;
|
||||
OutActorMap.Add(MapFunction(Actor), Actor);
|
||||
}
|
||||
}
|
||||
} //namespace WorldUtils
|
||||
|
||||
namespace ObjectUtils
|
||||
{
|
||||
void PROCEDURALDUNGEON_API DispatchToObjectAndSubobjects(UObject* Obj, TFunction<void(UObject*)> Func, int32 Depth = 0);
|
||||
}
|
||||
|
||||
namespace ActorUtils
|
||||
{
|
||||
// Returns the bounding box of an actor considering only components that would interact with rooms (based on collision settings).
|
||||
FBox PROCEDURALDUNGEON_API GetActorBoundingBoxForRooms(AActor* Actor, const FTransform& DungeonTransform = FTransform::Identity);
|
||||
|
||||
// Returns the player controller associated with the player state id.
|
||||
class APlayerController* GetPlayerControllerFromPlayerId(const UObject* WorldContextObject, int32 PlayerId);
|
||||
|
||||
UObject* GetInterfaceImplementer(AActor* Actor, TSubclassOf<UInterface> InterfaceClass);
|
||||
|
||||
template<typename T UE_REQUIRES(TIsDerivedFrom<T, UInterface>::Value)>
|
||||
UObject* GetInterfaceImplementer(AActor* Actor)
|
||||
{
|
||||
return GetInterfaceImplementer(Actor, T::StaticClass());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright Benoit Pelletier 2021 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Containers/Queue.h"
|
||||
#include "Containers/Array.h"
|
||||
|
||||
template<typename T>
|
||||
class TQueueOrStack
|
||||
{
|
||||
public:
|
||||
enum class EMode { QUEUE, STACK };
|
||||
|
||||
TQueueOrStack(EMode _Mode = EMode::QUEUE)
|
||||
: Mode(_Mode), Queue(), Stack()
|
||||
{
|
||||
}
|
||||
|
||||
// Sets the mode (queue or stack) and clears its content.
|
||||
void SetMode(EMode _Mode)
|
||||
{
|
||||
Empty();
|
||||
Mode = _Mode;
|
||||
}
|
||||
|
||||
void Push(T& Element)
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case EMode::QUEUE:
|
||||
Queue.Enqueue(Element);
|
||||
break;
|
||||
case EMode::STACK:
|
||||
Stack.Push(Element);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
T Pop()
|
||||
{
|
||||
check(!IsEmpty());
|
||||
T item = T();
|
||||
switch (Mode)
|
||||
{
|
||||
case EMode::QUEUE:
|
||||
Queue.Dequeue(item);
|
||||
break;
|
||||
case EMode::STACK:
|
||||
item = Stack.Pop();
|
||||
break;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
bool IsEmpty()
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case EMode::QUEUE:
|
||||
return Queue.IsEmpty();
|
||||
case EMode::STACK:
|
||||
return Stack.Num() <= 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Empty()
|
||||
{
|
||||
Queue.Empty();
|
||||
Stack.Empty();
|
||||
}
|
||||
|
||||
private:
|
||||
EMode Mode;
|
||||
TQueue<T> Queue;
|
||||
TArray<T> Stack;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "ReadOnlyRoom.generated.h"
|
||||
|
||||
class URoomData;
|
||||
|
||||
// This class does not need to be modified.
|
||||
UINTERFACE(MinimalAPI, BlueprintType, NotBlueprintable, meta = (CannotImplementInterfaceInBlueprint, Documentable, Tooltip = "Allow access to only some members of Room instances during the generation process."))
|
||||
class UReadOnlyRoom : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
// Interface to access some room instance's data during the generation process.
|
||||
class PROCEDURALDUNGEON_API IReadOnlyRoom
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Returns the room data asset of this room instance.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual const URoomData* GetRoomData() const { return nullptr; }
|
||||
|
||||
// Returns the unique ID (per-dungeon) of the room.
|
||||
// The first room has ID 0 and then it increases in the order of placed room.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual int64 GetRoomID() const { return -1ll; }
|
||||
|
||||
// Returns the position of the room (in Room Units).
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual FIntVector GetPosition() const { return FIntVector::ZeroValue; }
|
||||
|
||||
// Returns the direction of the room.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual EDoorDirection GetDirection() const { return EDoorDirection::North; }
|
||||
|
||||
// Returns true if all the doors of this room are connected to other rooms.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual bool AreAllDoorsConnected() const { return false; }
|
||||
|
||||
// Returns the number of doors in this room connected to another room.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual int CountConnectedDoors() const { return -1; }
|
||||
|
||||
// Returns the world center position of the room.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual FVector GetBoundsCenter() const { return FVector::ZeroVector; }
|
||||
|
||||
// Returns the world extents (half size) of the room.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
virtual FVector GetBoundsExtent() const { return FVector::ZeroVector; }
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/CoreNetTypes.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "Utils/ReplicationDefines.h"
|
||||
#include "ReplicableObject.generated.h"
|
||||
|
||||
class UActorChannel;
|
||||
class FOutBunch;
|
||||
struct FReplicationFlags;
|
||||
|
||||
enum class EUnregisterSubObjectType : uint8
|
||||
{
|
||||
Unregister = 0, // Simply remove from the subobject list. Subobject on remotes will be destroyed when it's garbage collected on authority.
|
||||
Destroy, // Immediately destroy the subobject on remotes. It's the responsibility of the authority to destroy it later. Imply Unregister.
|
||||
TearOff, // Break replication of this subobject, so it's considered as local subobject on remotes. Imply Unregister.
|
||||
};
|
||||
|
||||
struct FRegisterSubObjectParams
|
||||
{
|
||||
// The NetCondition when registering the subobject. Unused when unregistering.
|
||||
ELifetimeCondition NetCondition = COND_None;
|
||||
// How the subobject is unregistered. Unused when registering.
|
||||
EUnregisterSubObjectType UnregisterType = EUnregisterSubObjectType::Unregister;
|
||||
};
|
||||
|
||||
// Base class for sub-objects that can be replicated.
|
||||
// This class is not blueprintable and should not be used directly in blueprints.
|
||||
UCLASS(NotBlueprintable, NotBlueprintType)
|
||||
class PROCEDURALDUNGEON_API UReplicableObject : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ Begin UObject Interface
|
||||
|
||||
#if WITH_ENGINE
|
||||
virtual UWorld* GetWorld() const override;
|
||||
#endif
|
||||
|
||||
virtual bool IsSupportedForNetworking() const override { return true; }
|
||||
|
||||
#if UE_WITH_IRIS
|
||||
virtual void RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags) override;
|
||||
#endif // UE_WITH_IRIS
|
||||
|
||||
//~ End UObject Interface
|
||||
|
||||
// To be called in place of Channel->ReplicateSubobject(...)
|
||||
// Considered as "Legacy" (used when "Use Registered Subobject List" is false in the actor owner).
|
||||
bool ReplicateSubobject(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags);
|
||||
|
||||
// Call AddReplicatedSubObject to its owner (if valid).
|
||||
// Call it during PostInitializeComponent if subobject was created using CreateDefaultSubobject.
|
||||
// Used when "Use Registered Subobject List" is true in the actor owner.
|
||||
void RegisterAsReplicable(bool bRegister, FRegisterSubObjectParams Params = FRegisterSubObjectParams());
|
||||
|
||||
protected:
|
||||
// Replicating the eventual nested subobjects.
|
||||
// Should not be called directly, except from the overridden function in child classes.
|
||||
// Considered as "Legacy" (used when "Use Registered Subobject List" is false in the actor owner).
|
||||
virtual bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags);
|
||||
|
||||
// Override this to register subobjects of this one as replicable.
|
||||
// Used when "Use Registered Subobject List" is true in the actor owner.
|
||||
virtual void RegisterReplicableSubobjects(bool bRegister) {}
|
||||
|
||||
// Returns true if the owning actor has authority
|
||||
bool HasAuthority() const;
|
||||
|
||||
// Returns "Server" or "Client" based on HasAuthority() result.
|
||||
FString GetAuthorityName() const;
|
||||
|
||||
// Wakes up the owner actor from dormance to indicate that a replicated variable will change.
|
||||
// You have to use it *before* modifying the replicated variables.
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Replication")
|
||||
void WakeUpOwnerActor();
|
||||
};
|
||||
@@ -0,0 +1,335 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ReplicableObject.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "Math/GenericOctree.h" // for FBoxCenterAndExtent (required for UE5.0)
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "UObject/SoftObjectPtr.h"
|
||||
#include "RoomData.h" // for TSoftObjectPtr to compile. @TODO: Would be great to find a way to not include it
|
||||
#include "ReadOnlyRoom.h"
|
||||
#include "VoxelBounds/VoxelBounds.h"
|
||||
#include "Room.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FRelevancyEvent, URoom*, Room, APlayerController*, PlayerController, int32, NewRelevancyLevel);
|
||||
|
||||
class ADungeonGeneratorBase;
|
||||
class ARoomLevel;
|
||||
class URoomCustomData;
|
||||
class ULevelStreamingDynamic;
|
||||
|
||||
// I made this struct instead of a map to allow replication over network.
|
||||
USTRUCT()
|
||||
struct FCustomDataPair
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY()
|
||||
UClass* DataClass {nullptr};
|
||||
UPROPERTY()
|
||||
URoomCustomData* Data {nullptr};
|
||||
};
|
||||
|
||||
// The room instances of the dungeon.
|
||||
// Holds data specific to each room instance, e.g. location, direction, is player inside, room custom data, etc.
|
||||
UCLASS(BlueprintType, meta = (ShortToolTip = "The room instances of the dungeon."))
|
||||
class PROCEDURALDUNGEON_API URoom : public UReplicableObject, public IReadOnlyRoom, public IDungeonCustomSerialization, public IDungeonSaveInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// TODO: Make them private
|
||||
UPROPERTY()
|
||||
ULevelStreamingDynamic* Instance {nullptr};
|
||||
UPROPERTY(Replicated, SaveGame)
|
||||
FIntVector Position {0};
|
||||
UPROPERTY(Replicated, SaveGame)
|
||||
EDoorDirection Direction {EDoorDirection::NbDirection};
|
||||
|
||||
//~ Begin IReadOnlyRoom Interface
|
||||
virtual const URoomData* GetRoomData() const override { return RoomData; }
|
||||
virtual int64 GetRoomID() const override { return Id; }
|
||||
virtual FIntVector GetPosition() const { return Position; }
|
||||
virtual EDoorDirection GetDirection() const { return Direction; }
|
||||
virtual bool AreAllDoorsConnected() const override;
|
||||
virtual int CountConnectedDoors() const override;
|
||||
virtual FVector GetBoundsCenter() const override;
|
||||
virtual FVector GetBoundsExtent() const override;
|
||||
//~ End IReadOnlyRoom Interface
|
||||
|
||||
//~ Begin IDungeonCustomSerialization Interface
|
||||
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override;
|
||||
virtual bool FixupReferences(UObject* Context) override;
|
||||
//~ End IDungeonCustomSerialization Interface
|
||||
|
||||
//~ Begin IDungeonSaveInterface Interface
|
||||
virtual void PreSaveDungeon_Implementation() override;
|
||||
virtual void PostLoadDungeon_Implementation() override;
|
||||
//~ End IDungeonSaveInterface Interface
|
||||
|
||||
const ADungeonGeneratorBase* Generator() const { return GeneratorOwner.Get(); }
|
||||
void SetPlayerInside(int32 PlayerID, bool PlayerInside);
|
||||
void SetVisible(bool Visible, bool bForceUpdate = false);
|
||||
void SetRelevancyLevel(int32 PlayerID, int32 Level);
|
||||
FORCEINLINE bool IsReady() const { return RoomData != nullptr; }
|
||||
|
||||
// Is the player currently inside the room?
|
||||
// A player can be in multiple rooms at once, for example when he stands at the door frame,
|
||||
// the player's capsule is in both rooms.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
bool IsPlayerInside(const APlayerController* PlayerController = nullptr) const;
|
||||
|
||||
// Is the room currently visible?
|
||||
UFUNCTION(BlueprintPure, Category = "Room", meta = (CompactNodeTitle = "Is Visible"))
|
||||
FORCEINLINE bool IsVisible() const { return bIsVisible || bForceVisible; }
|
||||
|
||||
// Force the room to be veisible
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
void ForceVisibility(bool bForce);
|
||||
|
||||
// Get the relevancy level for the specified player.
|
||||
// A relevancy level < 0 means the room is not relevant for the player.
|
||||
// A relevancy level of 0 means the player is inside the room.
|
||||
// A relevancy level > 0 means the player is outside the room, the higher the level, the further away the room is.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
int32 GetRelevancyLevel(APlayerController* PlayerController) const;
|
||||
|
||||
// Get the maximum relevancy level for this room.
|
||||
// The highest value, the farthest the room is from any player.
|
||||
// A value < 0 means no player has this room as relevant.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
int32 GetMaxRelevancyLevel() const;
|
||||
|
||||
// Get minimum relevancy level for this room.
|
||||
// The lowest value, the closest the room is from any player.
|
||||
// A value < 0 means no player has this room as relevant.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
int32 GetMinRelevancyLevel() const;
|
||||
|
||||
// Get all relevancy levels for this room.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
void GetAllRelevancyLevels(TMap<APlayerController*, int32>& OutRelevancyLevels) const;
|
||||
|
||||
// Is the room locked?
|
||||
// If it is, the doors will be locked (except if they have `Alway Unlocked`).
|
||||
UFUNCTION(BlueprintPure, Category = "Room", meta = (CompactNodeTitle = "Is Locked"))
|
||||
FORCEINLINE bool IsLocked() const { return bIsLocked; }
|
||||
|
||||
// Lock or unlock the room instance.
|
||||
// Will lock/unlock the doors too (except if they have `Alway Unlocked`).
|
||||
// @param lock Should the room be locked?
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Room")
|
||||
void Lock(bool lock);
|
||||
|
||||
// Access to custom data of the room.
|
||||
// @param DataType The type of the custom data to retrieve.
|
||||
// @param Data The custom data found, or null if no custom data found.
|
||||
// @return True if a custom data of the specified type has been found, false otherwise.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room", meta = (DisplayName = "Get Custom Data", ExpandBoolAsExecs = "ReturnValue", DeterminesOutputType = "DataType", DynamicOutputParam = "Data"))
|
||||
bool GetCustomData_BP(TSubclassOf<URoomCustomData> DataType, URoomCustomData*& Data);
|
||||
|
||||
// Check if the room instance contains a custom data of a specific type.
|
||||
// @param DataType The type of the custom data to check.
|
||||
// @return True if the rooms has a custom data of the specified type, false otherwise.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room", meta = (DisplayName = "Has Custom Data", ExpandBoolAsExecs = "ReturnValue", AutoCreateRefTerm = "DataType"))
|
||||
bool HasCustomData_BP(const TSubclassOf<URoomCustomData>& DataType);
|
||||
|
||||
bool CreateCustomData(const TSubclassOf<URoomCustomData>& DataType);
|
||||
bool CreateAllCustomData();
|
||||
bool GetCustomData(const TSubclassOf<URoomCustomData>& DataType, URoomCustomData*& Data) const;
|
||||
bool HasCustomData(const TSubclassOf<URoomCustomData>& DataType) const;
|
||||
|
||||
// Returns the RandomStream from the Dungeon Generator
|
||||
// [DEPRECATED] Use a DeterministicRandom component on actors instead.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room", meta = (DeprecatedFunction, DeprecationMessage = "Use a DeterministicRandom component on actors instead."))
|
||||
FRandomStream GetRandomStream() const;
|
||||
|
||||
// Get the door actor from a specific index.
|
||||
// @param DoorIndex The index of the door to retrieve.
|
||||
// @return The door actor at the index, or null if the index is out of range.
|
||||
UFUNCTION(BlueprintCallable, Category = "Room")
|
||||
AActor* GetDoor(int32 DoorIndex) const;
|
||||
|
||||
// Fill an array with all the door actors connected to the room.
|
||||
// @param OutDoors THIS IS NOT AN INPUT! This array will be emptied and then filled with the door actors. This is your result!
|
||||
UFUNCTION(BlueprintPure = false, Category = "Room")
|
||||
void GetAllDoors(UPARAM(ref) TArray<AActor*>& OutDoors) const;
|
||||
|
||||
// Returns true if the door at DoorIndex is connected to another room.
|
||||
// @param DoorIndex The index of the door to check.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
bool IsDoorConnected(int DoorIndex) const;
|
||||
|
||||
// Returns the connected room instance at DoorIndex.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
URoom* GetConnectedRoomAt(int DoorIndex) const;
|
||||
|
||||
// Returns all the room instances connected with this one.
|
||||
// @param ConnectedRooms This array will be filled with the room instances.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
void GetAllConnectedRooms(TArray<URoom*>& ConnectedRooms) const;
|
||||
|
||||
// Returns the index of the provided room, or -1 if room is not connected.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
int32 GetConnectedRoomIndex(const URoom* OtherRoom) const;
|
||||
|
||||
// Returns the door actor shared with the provided room.
|
||||
// Returns null if the provided room is not connected with this.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
void GetDoorsWith(const URoom* OtherRoom, TArray<AActor*>& Doors) const;
|
||||
|
||||
// Returns all the connections of this room.
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
TArray<URoomConnection*> GetConnections() const;
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintAssignable, Category = "Room|Events")
|
||||
FRelevancyEvent OnRelevancyChanged;
|
||||
|
||||
private:
|
||||
// Deprecate old way of storing RoomData.
|
||||
// Must not be used anywhere else than in serialization code.
|
||||
// It has been renamed SoftRoomData, because despite the DEPRECATED suffix,
|
||||
// the engine treats RoomData_DEPRECATED as RoomData, and thus conflicting with the below one.
|
||||
UPROPERTY(SaveGame, Transient, meta=(DeprecatedProperty))
|
||||
TSoftObjectPtr<URoomData> SoftRoomData_DEPRECATED {nullptr};
|
||||
|
||||
// New way to store RoomData.
|
||||
// It must be a hard reference to avoid it being garbage collected on clients.
|
||||
UPROPERTY(ReplicatedUsing = OnRep_RoomData)
|
||||
URoomData* RoomData {nullptr};
|
||||
|
||||
UPROPERTY(Replicated, Transient)
|
||||
TArray<FCustomDataPair> CustomData;
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Connections)
|
||||
TArray<TWeakObjectPtr<class URoomConnection>> Connections;
|
||||
|
||||
UPROPERTY(Replicated)
|
||||
TWeakObjectPtr<ADungeonGeneratorBase> GeneratorOwner {nullptr};
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_Id, SaveGame)
|
||||
int64 Id {-1};
|
||||
|
||||
TSet<int32> PlayerIDInside {};
|
||||
bool bIsVisible {true};
|
||||
bool bForceVisible {false};
|
||||
TMap<int32, int32> RelevancyLevels {};
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_IsLocked, SaveGame)
|
||||
bool bIsLocked {false};
|
||||
|
||||
const FCustomDataPair* GetDataPair(const TSubclassOf<URoomCustomData>& DataType) const;
|
||||
|
||||
protected:
|
||||
//~ Begin UReplicableObject Interface
|
||||
virtual bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) override;
|
||||
virtual void RegisterReplicableSubobjects(bool bRegister) override;
|
||||
//~ End UReplicableObject Interface
|
||||
|
||||
void UpdateVisibility() const;
|
||||
|
||||
UFUNCTION() // Needed macro for replication to work
|
||||
void OnRep_RoomData();
|
||||
|
||||
UFUNCTION() // Needed macro for replication to work
|
||||
void OnRep_Id();
|
||||
|
||||
UFUNCTION() // Needed macro for replication to work
|
||||
void OnRep_Connections();
|
||||
|
||||
UFUNCTION() // Needed macro for replication to work
|
||||
void OnRep_IsLocked();
|
||||
|
||||
UFUNCTION() // needed macro for binding to delegate
|
||||
void OnInstanceLoaded();
|
||||
|
||||
public:
|
||||
void Init(URoomData* RoomData, ADungeonGeneratorBase* Generator, int32 RoomId);
|
||||
|
||||
void Instantiate(UWorld* World);
|
||||
void Destroy();
|
||||
ARoomLevel* GetLevelScript() const;
|
||||
bool IsInstanceLoaded() const;
|
||||
bool IsInstanceUnloaded() const;
|
||||
bool IsInstanceInitialized() const;
|
||||
void CreateLevelComponents(ARoomLevel* LevelActor);
|
||||
|
||||
EDoorDirection GetDoorWorldOrientation(int DoorIndex) const;
|
||||
FIntVector GetDoorWorldPosition(int DoorIndex) const;
|
||||
|
||||
int32 GetConnectionCount() const { return Connections.Num(); }
|
||||
bool IsConnected(int32 DoorIndex) const;
|
||||
void SetConnection(int32 DoorIndex, URoomConnection* Conn);
|
||||
TWeakObjectPtr<URoom> GetConnectedRoom(int32 DoorIndex) const;
|
||||
int32 GetFirstEmptyConnection() const;
|
||||
void GetAllEmptyConnections(TArray<int32>& EmptyConnections) const;
|
||||
const TArray<TWeakObjectPtr<URoomConnection>>& GetAllConnections() const { return Connections; }
|
||||
|
||||
bool IsDoorIndexValid(int32 DoorIndex) const;
|
||||
int32 GetDoorIndexAt(FIntVector WorldPos, EDoorDirection WorldRot) const;
|
||||
int32 GetOtherDoorIndex(int32 DoorIndex) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room")
|
||||
FDoorDef GetDoorDef(int32 DoorIndex) const;
|
||||
|
||||
FDoorDef GetDoorDefAt(FIntVector WorldPos, EDoorDirection WorldRot) const;
|
||||
|
||||
FIntVector WorldToRoom(const FIntVector& WorldPos) const;
|
||||
FIntVector RoomToWorld(const FIntVector& RoomPos) const;
|
||||
EDoorDirection WorldToRoom(const EDoorDirection& WorldRot) const;
|
||||
EDoorDirection RoomToWorld(const EDoorDirection& RoomRot) const;
|
||||
FBoxMinAndMax WorldToRoom(const FBoxMinAndMax& WorldBox) const;
|
||||
FBoxMinAndMax RoomToWorld(const FBoxMinAndMax& RoomBox) const;
|
||||
FDoorDef WorldToRoom(const FDoorDef& WorldDoor) const;
|
||||
FDoorDef RoomToWorld(const FDoorDef& RoomDoor) const;
|
||||
FVoxelBounds WorldToRoom(const FVoxelBounds& WorldBounds) const;
|
||||
FVoxelBounds RoomToWorld(const FVoxelBounds& RoomBounds) const;
|
||||
|
||||
void SetPosition(const FIntVector& NewPosition);
|
||||
void SetDirection(EDoorDirection NewDirection);
|
||||
void SetRotationFromDoor(int DoorIndex, EDoorDirection WorldRot);
|
||||
void SetPositionFromDoor(int DoorIndex, FIntVector WorldPos);
|
||||
void SetPositionAndRotationFromDoor(int DoorIndex, FIntVector WorldPos, EDoorDirection WorldRot);
|
||||
bool IsOccupied(FIntVector Cell);
|
||||
|
||||
FTransform GetTransform() const;
|
||||
FBoxCenterAndExtent GetBounds() const;
|
||||
int32 GetSubBoundsCount() const;
|
||||
FBoxCenterAndExtent GetSubBounds(int32 Index) const;
|
||||
FBoxCenterAndExtent GetLocalBounds() const;
|
||||
FBoxMinAndMax GetIntBounds() const;
|
||||
FVoxelBounds GetVoxelBounds() const;
|
||||
|
||||
private:
|
||||
// Utility functions to load/unload level instances
|
||||
static ULevelStreamingDynamic* LoadInstance(UObject* WorldContextObject, const TSoftObjectPtr<UWorld>& Level, const FString& InstanceNameSuffix, FVector Location, FRotator Rotation);
|
||||
static void UnloadInstance(ULevelStreamingDynamic* Instance);
|
||||
|
||||
private:
|
||||
using FActorSaveDataMap = TMap<FGuid, TArray<uint8>>;
|
||||
|
||||
// This struct holds the data applied at later stages of the loading process.
|
||||
// For example, it holds the connection indices, that will be used later to resolve the connection references.
|
||||
struct FSaveData
|
||||
{
|
||||
TArray<int32> ConnectionIds;
|
||||
TArray<uint8> LevelActor;
|
||||
FActorSaveDataMap Actors;
|
||||
};
|
||||
|
||||
// This is a unique ptr so we have a data only when we need it.
|
||||
TUniquePtr<FSaveData> SaveData {nullptr};
|
||||
|
||||
bool SerializeLevelActors(FSaveData& Data, bool bIsLoading);
|
||||
void DispatchCallbackToSavedLevelActors(TFunction<void(AActor*)> Callback) const;
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ReplicableObject.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "RoomConnection.generated.h"
|
||||
|
||||
class URoom;
|
||||
|
||||
// A DungeonGraph subobject that represents a connection between two rooms.
|
||||
UCLASS(BlueprintType)
|
||||
class PROCEDURALDUNGEON_API URoomConnection : public UReplicableObject, public IDungeonCustomSerialization, public IDungeonSaveInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ Begin IDungeonCustomSerialization Interface
|
||||
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override;
|
||||
virtual bool FixupReferences(UObject* Context) override;
|
||||
//~ End IDungeonCustomSerialization Interface
|
||||
|
||||
//~ Begin IDungeonSaveInterface Interface
|
||||
virtual void PreSaveDungeon_Implementation() override;
|
||||
virtual void PostLoadDungeon_Implementation() override;
|
||||
//~ End IDungeonSaveInterface Interface
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection")
|
||||
int32 GetID() const;
|
||||
|
||||
const TWeakObjectPtr<URoom> GetRoomA() const;
|
||||
const TWeakObjectPtr<URoom> GetRoomB() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection", meta = (DisplayName = "Get Door A Index"))
|
||||
int32 GetRoomADoorId() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection", meta = (DisplayName = "Get Door B Index"))
|
||||
int32 GetRoomBDoorId() const;
|
||||
|
||||
TWeakObjectPtr<URoom> GetOtherRoom(const URoom* FromRoom) const;
|
||||
int32 GetOtherDoorId(const URoom* FromRoom) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection")
|
||||
bool IsDoorInstanced() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection")
|
||||
AActor* GetDoorInstance() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection")
|
||||
FVector GetDoorLocation(bool bIgnoreGeneratorTransform) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection")
|
||||
FRotator GetDoorRotation(bool bIgnoreGeneratorTransform) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection")
|
||||
bool IsDoorOpen() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection")
|
||||
bool IsDoorLocked() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Room Connection")
|
||||
void SetDoorOpen(bool bOpen);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Room Connection")
|
||||
void SetDoorLocked(bool bLocked);
|
||||
|
||||
void SetDoorClass(TSubclassOf<AActor> DoorClass, bool bFlipped);
|
||||
AActor* InstantiateDoor(UWorld* World, AActor* Owner = nullptr, bool bUseOwnerTransform = false);
|
||||
void DestroyDoor();
|
||||
|
||||
// Convenient functions to return a default value if the connection is nullptr.
|
||||
static URoom* GetOtherRoom(const URoomConnection* Conn, const URoom* FromRoom);
|
||||
static int32 GetOtherDoorId(const URoomConnection* Conn, const URoom* FromRoom);
|
||||
static AActor* GetDoorInstance(const URoomConnection* Conn);
|
||||
static class UDoorType* GetDoorType(const URoomConnection* Conn);
|
||||
static void GetBothDoorTypes(const URoomConnection* Conn, UDoorType*& DoorTypeA, UDoorType*& DoorTypeB);
|
||||
|
||||
static URoomConnection* CreateConnection(URoom* RoomA, int32 DoorA, URoom* RoomB, int32 DoorB, UObject* Outer, int32 IdInOuter);
|
||||
|
||||
protected:
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection", meta = (DisplayName = "Get Room A", CompactNodeTitle = "Room A"))
|
||||
const URoom* GetRoomA_BP() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Connection", meta = (DisplayName = "Get Room B", CompactNodeTitle = "Room B"))
|
||||
const URoom* GetRoomB_BP() const;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void OnRep_ID();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_RoomA();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_RoomB();
|
||||
|
||||
UFUNCTION()
|
||||
void OnRep_DoorState();
|
||||
|
||||
private:
|
||||
UPROPERTY(ReplicatedUsing = OnRep_ID, SaveGame)
|
||||
int32 ID {-1};
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_RoomA)
|
||||
TWeakObjectPtr<URoom> RoomA {nullptr};
|
||||
|
||||
UPROPERTY(Replicated, SaveGame)
|
||||
int32 RoomADoorId {-1};
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_RoomB)
|
||||
TWeakObjectPtr<URoom> RoomB {nullptr};
|
||||
|
||||
UPROPERTY(Replicated, SaveGame)
|
||||
int32 RoomBDoorId {-1};
|
||||
|
||||
UPROPERTY()
|
||||
TSubclassOf<AActor> DoorClass {nullptr};
|
||||
|
||||
UPROPERTY(SaveGame)
|
||||
bool bFlipped {false};
|
||||
|
||||
UPROPERTY(Replicated, Transient)
|
||||
TWeakObjectPtr<AActor> DoorInstance {nullptr};
|
||||
|
||||
UPROPERTY(ReplicatedUsing = OnRep_DoorState, SaveGame)
|
||||
FDoorState DoorState;
|
||||
|
||||
private:
|
||||
// Store temporary data used only during saving/loading the game
|
||||
struct FSaveData
|
||||
{
|
||||
int32 Version {-1};
|
||||
int64 RoomAID {-1};
|
||||
int64 RoomBID {-1};
|
||||
TArray<uint8> DoorSavedData;
|
||||
};
|
||||
|
||||
TUniquePtr<FSaveData> SaveData {nullptr};
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "RoomConstraint.generated.h"
|
||||
|
||||
class URoomData;
|
||||
|
||||
// Base class for constraints applied to rooms
|
||||
UCLASS(Abstract, BlueprintType, Blueprintable, EditInlineNew)
|
||||
class PROCEDURALDUNGEON_API URoomConstraint : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Room Constraint")
|
||||
bool Check(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const;
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright Benoit Pelletier 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RoomConstraints/RoomConstraint.h"
|
||||
#include "BoundsParams.h"
|
||||
#include "RoomConstraint_CountLimit.generated.h"
|
||||
|
||||
class URoomData;
|
||||
|
||||
// Constraints the room to be inside the provided bounds
|
||||
UCLASS(meta = (DisplayName = "Count Limit Constraint"))
|
||||
class PROCEDURALDUNGEON_API URoomConstraint_CountLimit : public URoomConstraint
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ Begin URoomConstraint Interface
|
||||
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override;
|
||||
//~ End URoomConstraint Interface
|
||||
|
||||
private:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Constraint", meta = (AllowPrivateAccess = true))
|
||||
int32 MaxCount {1};
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RoomConstraints/RoomConstraint.h"
|
||||
#include "RoomConstraint_Direction.generated.h"
|
||||
|
||||
// Constraint on the possible directions the room can be placed
|
||||
UCLASS(meta = (DisplayName = "Direction Constraint"))
|
||||
class PROCEDURALDUNGEON_API URoomConstraint_Direction : public URoomConstraint
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ Begin URoomConstraint Interface
|
||||
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override;
|
||||
//~ End URoomConstraint Interface
|
||||
|
||||
private:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Constraint", meta = (AllowPrivateAccess=true))
|
||||
TArray<EDoorDirection> AllowedDirections {};
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RoomConstraints/RoomConstraint.h"
|
||||
#include "BoundsParams.h"
|
||||
#include "RoomConstraint_Location.generated.h"
|
||||
|
||||
class URoomData;
|
||||
|
||||
// Constraints the room to be inside the provided bounds
|
||||
UCLASS(meta = (DisplayName = "Location Constraint"))
|
||||
class PROCEDURALDUNGEON_API URoomConstraint_Location : public URoomConstraint
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ Begin URoomConstraint Interface
|
||||
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override;
|
||||
//~ End URoomConstraint Interface
|
||||
|
||||
private:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Constraint", meta = (AllowPrivateAccess = true))
|
||||
FBoundsParams Limits;
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ReplicableObject.h"
|
||||
#include "Interfaces/DungeonCustomSerialization.h"
|
||||
#include "Interfaces/DungeonSaveInterface.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
#include "RoomCustomData.generated.h"
|
||||
|
||||
class UActorComponent;
|
||||
|
||||
// Base class for user custom data embedded in room instances
|
||||
UCLASS(Abstract, BlueprintType, Blueprintable)
|
||||
class PROCEDURALDUNGEON_API URoomCustomData : public UReplicableObject, public IDungeonCustomSerialization, public IDungeonSaveInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
void CreateLevelComponent(class ARoomLevel* LevelActor);
|
||||
|
||||
//~ Begin IDungeonCustomSerialization Interface
|
||||
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override;
|
||||
//~ End IDungeonCustomSerialization Interface
|
||||
|
||||
//~ Begin IDungeonSaveInterface Interface
|
||||
virtual void PreSaveDungeon_Implementation() override;
|
||||
virtual void PostLoadDungeon_Implementation() override;
|
||||
//~ End IDungeonSaveInterface Interface
|
||||
|
||||
private:
|
||||
// Component to create and attach on the Level Script Actor of the instanced room.
|
||||
UPROPERTY(EditAnywhere, Category = "Dungeon Rules", meta = (AllowAbstract = false, AllowPrivateAccess = true))
|
||||
TSubclassOf<UActorComponent> LevelComponent {nullptr};
|
||||
|
||||
// Keep a reference to the created component instance
|
||||
UPROPERTY(Transient)
|
||||
TWeakObjectPtr<UActorComponent> LevelComponentInstance {nullptr};
|
||||
|
||||
private:
|
||||
struct FSaveData
|
||||
{
|
||||
TArray<uint8> ComponentData;
|
||||
};
|
||||
|
||||
TUniquePtr<FSaveData> SavedData;
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "ProceduralDungeonTypes.h"
|
||||
#include "Misc/EngineVersionComparison.h"
|
||||
#include "Math/GenericOctree.h" // for FBoxCenterAndExtent (required for UE5.0)
|
||||
#include "VoxelBounds/VoxelBounds.h"
|
||||
#include "RoomData.generated.h"
|
||||
|
||||
#if UE_VERSION_OLDER_THAN(5, 3, 0)
|
||||
#define USE_LEGACY_DATA_VALIDATION 1
|
||||
#else
|
||||
#define USE_LEGACY_DATA_VALIDATION 0
|
||||
#endif
|
||||
|
||||
class URoom;
|
||||
class UDungeonGraph;
|
||||
class URoomCustomData;
|
||||
class UDoorType;
|
||||
class UDungeonSettings;
|
||||
class URoomConstraint;
|
||||
|
||||
#if WITH_EDITOR
|
||||
class URoomData;
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FRoomDataEditorEvent, URoomData*)
|
||||
#endif
|
||||
|
||||
// Static data shared by multiple room instances.
|
||||
// This data is used to define the room's properties and the room level to spawn.
|
||||
UCLASS()
|
||||
class PROCEDURALDUNGEON_API URoomData : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditInstanceOnly, Category = "Level")
|
||||
TSoftObjectPtr<UWorld> Level {nullptr};
|
||||
|
||||
// This will force a random door to be chosen during the dungeon generation.
|
||||
// DEPRECATED: It will be removed in a future version of the plugin. As a replacement, you should return -1 as DoorIndex in the ChooseNextRoomData of your DungeonGenerator.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Doors")
|
||||
bool RandomDoor {true};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Doors")
|
||||
TArray<FDoorDef> Doors {FDoorDef()};
|
||||
|
||||
UPROPERTY(meta = (DeprecatedProperty, DeprecationMessage = "Use BoundingBoxes instead"))
|
||||
FIntVector FirstPoint {0};
|
||||
|
||||
UPROPERTY(meta = (DeprecatedProperty, DeprecationMessage = "Use BoundingBoxes instead"))
|
||||
FIntVector SecondPoint {1};
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Room")
|
||||
TArray<FBoxMinAndMax> BoundingBoxes;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Room")
|
||||
TSet<TSubclassOf<URoomCustomData>> CustomData;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Room", AdvancedDisplay)
|
||||
UDungeonSettings* SettingsOverrides {nullptr};
|
||||
|
||||
UPROPERTY(EditAnywhere, Instanced, BlueprintReadOnly, Category = "Room")
|
||||
TArray<URoomConstraint*> Constraints;
|
||||
|
||||
public:
|
||||
URoomData();
|
||||
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data", meta = (DisplayName = "Door Count", CompactNodeTitle = "Door Count"))
|
||||
int GetNbDoor() const { return Doors.Num(); }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
const FDoorDef& GetDoorDef(int32 DoorIndex) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
bool HasCompatibleDoor(const FDoorDef& DoorData) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
void GetCompatibleDoors(const FDoorDef& DoorData, TArray<int>& CompatibleDoors) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
bool HasDoorOfType(UDoorType* DoorType) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
bool HasAnyDoorOfType(const TArray<UDoorType*>& DoorTypes) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
bool HasAllDoorOfType(const TArray<UDoorType*>& DoorTypes) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
bool HasCustomData(TSubclassOf<URoomCustomData> CustomDataClass) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
bool HasAnyCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomDataList) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
bool HasAllCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomDataList) const;
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Room Data")
|
||||
void InitializeRoom(URoom* Room, UDungeonGraph* Dungeon) const;
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "Room Data")
|
||||
void CleanupRoom(URoom* Room, UDungeonGraph* Dungeon) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
FIntVector GetSize() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
int GetVolume() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
const UDungeonSettings* GetSettings() const { return SettingsOverrides; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Data")
|
||||
FVector GetRoomUnit() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Room Contraint")
|
||||
static bool DoesPassAllConstraints(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction);
|
||||
|
||||
FBoxCenterAndExtent GetBounds(FTransform Transform = FTransform::Identity) const;
|
||||
FBoxCenterAndExtent GetSubBounds(int32 Index, FTransform Transform = FTransform::Identity) const;
|
||||
FBoxMinAndMax GetIntBounds() const;
|
||||
FVoxelBounds GetVoxelBounds() const;
|
||||
|
||||
bool IsRoomInBounds(const FBoxMinAndMax& Bounds, int DoorIndex, const FDoorDef& DoorDungeonPos) const;
|
||||
|
||||
#if !(UE_BUILD_SHIPPING) || WITH_EDITOR
|
||||
bool IsDoorValid(int DoorIndex) const;
|
||||
bool IsDoorDuplicate(int DoorIndex) const;
|
||||
void DrawDebug(const UWorld* World, const FTransform& Transform, const FColor& Color);
|
||||
#endif // !(UE_BUILD_SHIPPING) || WITH_EDITOR
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#if USE_LEGACY_DATA_VALIDATION
|
||||
virtual EDataValidationResult IsDataValid(TArray<FText>& ValidationErrors) override;
|
||||
#else
|
||||
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
|
||||
#endif // USE_LEGACY_DATA_VALIDATION
|
||||
|
||||
FRoomDataEditorEvent OnPropertiesChanged;
|
||||
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
private:
|
||||
// This is a transient to avoid unnecessary computation
|
||||
// Flagged as mutable because it is computed on the fly when necessary.
|
||||
// This flag will be removed when the VoxelBounds editor will be implemented.
|
||||
mutable FVoxelBounds CachedVoxelBounds;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user