1639 lines
44 KiB
C++
1639 lines
44 KiB
C++
|
// Copyright 2020 Phyronnaz
|
||
|
|
||
|
#include "VoxelWorld.h"
|
||
|
#include "VoxelGenerators/VoxelGenerator.h"
|
||
|
#include "VoxelGenerators/VoxelGeneratorCache.h"
|
||
|
#include "IVoxelPool.h"
|
||
|
#include "VoxelSettings.h"
|
||
|
#include "VoxelDefaultPool.h"
|
||
|
#include "VoxelWorldRootComponent.h"
|
||
|
#include "VoxelRender/IVoxelRenderer.h"
|
||
|
#include "VoxelRender/IVoxelLODManager.h"
|
||
|
#include "VoxelRender/VoxelToolRendering.h"
|
||
|
#include "VoxelRender/LODManager/VoxelDefaultLODManager.h"
|
||
|
#include "VoxelRender/VoxelProceduralMeshComponent.h"
|
||
|
#include "VoxelRender/MaterialCollections/VoxelMaterialCollectionBase.h"
|
||
|
#include "VoxelRender/MaterialCollections/VoxelInstancedMaterialCollection.h"
|
||
|
#include "VoxelRender/Renderers/VoxelDefaultRenderer.h"
|
||
|
#include "VoxelData/VoxelData.h"
|
||
|
#include "VoxelData/VoxelSaveUtilities.h"
|
||
|
#include "VoxelMultiplayer/VoxelMultiplayerTcp.h"
|
||
|
#include "VoxelTools/VoxelBlueprintLibrary.h"
|
||
|
#include "VoxelTools/VoxelDataTools.h"
|
||
|
#include "VoxelTools/VoxelToolHelpers.h"
|
||
|
#include "VoxelPlaceableItems/VoxelPlaceableItemManager.h"
|
||
|
#include "VoxelPlaceableItems/Actors/VoxelPlaceableItemActorHelper.h"
|
||
|
#include "VoxelPlaceableItems/Actors/VoxelAssetActor.h"
|
||
|
#include "VoxelPlaceableItems/Actors/VoxelDisableEditsBox.h"
|
||
|
#include "VoxelComponents/VoxelInvokerComponent.h"
|
||
|
#include "VoxelDebug/VoxelDebugManager.h"
|
||
|
#include "VoxelDebug/VoxelLineBatchComponent.h"
|
||
|
#include "VoxelEvents/VoxelEventManager.h"
|
||
|
#include "VoxelMessages.h"
|
||
|
#include "VoxelFeedbackContext.h"
|
||
|
#include "VoxelUtilities/VoxelThreadingUtilities.h"
|
||
|
|
||
|
#include "EngineUtils.h"
|
||
|
#include "Engine/World.h"
|
||
|
#include "Engine/Engine.h"
|
||
|
#include "Engine/NetDriver.h"
|
||
|
#include "Engine/NetConnection.h"
|
||
|
|
||
|
#include "Misc/MessageDialog.h"
|
||
|
#include "Misc/FileHelper.h"
|
||
|
#include "Misc/FeedbackContext.h"
|
||
|
#include "Components/BillboardComponent.h"
|
||
|
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
|
||
|
#include "UObject/ConstructorHelpers.h"
|
||
|
#include "HAL/PlatformFilemanager.h"
|
||
|
#include "TimerManager.h"
|
||
|
#include "Materials/Material.h"
|
||
|
|
||
|
#include "Framework/Notifications/NotificationManager.h"
|
||
|
#include "Framework/Application/SlateApplication.h"
|
||
|
#include "Widgets/Notifications/SNotificationList.h"
|
||
|
#include "Serialization/BufferArchive.h"
|
||
|
#include "Serialization/MemoryReader.h"
|
||
|
|
||
|
void AVoxelWorld::FGameThreadTasks::Flush()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
check(IsInGameThread());
|
||
|
|
||
|
TFunction<void()> Task;
|
||
|
while (Tasks.Dequeue(Task))
|
||
|
{
|
||
|
Task();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
AVoxelWorld::AVoxelWorld()
|
||
|
: LODDynamicSettings(MakeVoxelShared<FVoxelLODDynamicSettings>())
|
||
|
, RendererDynamicSettings(MakeVoxelShared<FVoxelRendererDynamicSettings>())
|
||
|
{
|
||
|
MultiplayerInterface = UVoxelMultiplayerTcpInterface::StaticClass();
|
||
|
|
||
|
PrimaryActorTick.bCanEverTick = true;
|
||
|
PrimaryActorTick.bHighPriority = true;
|
||
|
|
||
|
WorldRoot = CreateDefaultSubobject<UVoxelWorldRootComponent>("Root");
|
||
|
LineBatchComponent = CreateDefaultSubobject<UVoxelLineBatchComponent>("LineBatchComponent");
|
||
|
|
||
|
RootComponent = WorldRoot;
|
||
|
|
||
|
#if WITH_EDITOR
|
||
|
auto* SpriteComponent = CreateEditorOnlyDefaultSubobject<UBillboardComponent>(TEXT("Sprite"));
|
||
|
if (SpriteComponent)
|
||
|
{
|
||
|
static ConstructorHelpers::FObjectFinder<UTexture2D> SpriteFinder(TEXT("/Engine/EditorResources/S_Terrain"));
|
||
|
SpriteComponent->Sprite = SpriteFinder.Object;
|
||
|
SpriteComponent->SetRelativeScale3D(FVector(0.5f));
|
||
|
SpriteComponent->bHiddenInGame = true;
|
||
|
SpriteComponent->bIsScreenSizeScaled = true;
|
||
|
SpriteComponent->SpriteInfo.Category = TEXT("Voxel World");
|
||
|
SpriteComponent->SpriteInfo.DisplayName = FText::FromString("Voxel World");
|
||
|
SpriteComponent->SetupAttachment(RootComponent);
|
||
|
SpriteComponent->bReceivesDecals = false;
|
||
|
}
|
||
|
|
||
|
// Automatically refresh material on property change/material recompile
|
||
|
const auto RefreshMaterial = [=](UMaterialInterface* Material)
|
||
|
{
|
||
|
if (!Material || !bAutomaticallyRefreshMaterials || !IsCreated())
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
bool bUsed = false;
|
||
|
if (MaterialConfig == EVoxelMaterialConfig::RGB)
|
||
|
{
|
||
|
if (VoxelMaterial == Material)
|
||
|
{
|
||
|
bUsed = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
for (auto& LODMaterial : LODMaterials)
|
||
|
{
|
||
|
if (LODMaterial.Material == Material)
|
||
|
{
|
||
|
bUsed = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (MaterialCollection &&
|
||
|
MaterialCollection->GetMaterialIndex(Material->GetFName()) != -1)
|
||
|
{
|
||
|
bUsed = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
for (auto& LODMaterialCollection : LODMaterialCollections)
|
||
|
{
|
||
|
if (LODMaterialCollection.MaterialCollection &&
|
||
|
LODMaterialCollection.MaterialCollection->GetMaterialIndex(Material->GetFName()) != -1)
|
||
|
{
|
||
|
bUsed = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bUsed)
|
||
|
{
|
||
|
UVoxelBlueprintLibrary::ApplyNewMaterials(this);
|
||
|
}
|
||
|
};
|
||
|
UMaterial::OnMaterialCompilationFinished().AddWeakLambda(this, RefreshMaterial);
|
||
|
|
||
|
const auto TryRefreshMaterials = [=](UObject* Object, FPropertyChangedEvent& PropertyChangedEvent)
|
||
|
{
|
||
|
if (!bAutomaticallyRefreshMaterials)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RefreshMaterial(Cast<UMaterialInterface>(Object));
|
||
|
|
||
|
if (MaterialConfig != EVoxelMaterialConfig::RGB && Object->IsA<UVoxelMaterialCollectionBase>())
|
||
|
{
|
||
|
bool bUsed = false;
|
||
|
if (MaterialCollection == Object)
|
||
|
{
|
||
|
bUsed = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
for (auto& It : LODMaterialCollections)
|
||
|
{
|
||
|
if (It.MaterialCollection == Object)
|
||
|
{
|
||
|
bUsed = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (auto* MaterialCollectionInstance = Cast<UVoxelInstancedMaterialCollectionInstance>(MaterialCollection))
|
||
|
{
|
||
|
if (MaterialCollectionInstance->LayersSource == Object)
|
||
|
{
|
||
|
bUsed = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bUsed)
|
||
|
{
|
||
|
UVoxelBlueprintLibrary::ApplyNewMaterials(this);
|
||
|
|
||
|
if (PropertyChangedEvent.MemberProperty &&
|
||
|
PropertyChangedEvent.MemberProperty->GetFName() == GET_MEMBER_NAME_STATIC(UVoxelInstancedMaterialCollection, MaxMaterialsToBlendAtOnce))
|
||
|
{
|
||
|
// Need to recompute the chunks if MaxMaterialsToBlendAtOnce changed, as they are built with the wrong number of indices
|
||
|
UVoxelBlueprintLibrary::UpdateAll(this);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const auto TryRefreshFoliage = [=](UObject* Object, FPropertyChangedEvent& PropertyChangedEvent)
|
||
|
{
|
||
|
};
|
||
|
|
||
|
FCoreUObjectDelegates::OnObjectPropertyChanged.AddWeakLambda(this, [=](UObject* Object, FPropertyChangedEvent& PropertyChangedEvent)
|
||
|
{
|
||
|
if (PropertyChangedEvent.ChangeType == EPropertyChangeType::Interactive)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!Object || !IsCreated())
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
TryRefreshMaterials(Object, PropertyChangedEvent);
|
||
|
TryRefreshFoliage(Object, PropertyChangedEvent);
|
||
|
});
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
void AVoxelWorld::CreateWorld(FVoxelWorldCreateInfo Info)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
FVoxelMessages::Error("Can't create world: already created");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#if WITH_EDITOR
|
||
|
if (GetWorld()->WorldType == EWorldType::Editor ||
|
||
|
GetWorld()->WorldType == EWorldType::EditorPreview)
|
||
|
{
|
||
|
// Called in editor - probably by a BP construction script
|
||
|
// Forward to CreateInEditor
|
||
|
CreateInEditor(Info);
|
||
|
return;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
PlayType = EVoxelPlayType::Game;
|
||
|
CreateWorldInternal(Info);
|
||
|
if (bUseCameraIfNoInvokersFound && UVoxelInvokerComponentBase::GetInvokers(GetWorld()).Num() == 0)
|
||
|
{
|
||
|
const auto NetMode = GetWorld()->GetNetMode();
|
||
|
if (NetMode != ENetMode::NM_Standalone)
|
||
|
{
|
||
|
if (NetMode != ENetMode::NM_DedicatedServer) // Not spawned yet
|
||
|
{
|
||
|
FVoxelMessages::Warning("Voxel World: Can't use camera as invoker in multiplayer! You need to add a VoxelInvokerComponent to your character");
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
auto* CameraInvoker = NewObject<UVoxelInvokerAutoCameraComponent>(this, NAME_None, RF_Transient);
|
||
|
CameraInvoker->SetupAttachment(GetRootComponent(), NAME_None);
|
||
|
CameraInvoker->RegisterComponent();
|
||
|
|
||
|
LOG_VOXEL(Log, TEXT("No Voxel Invoker found, using camera as invoker"));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::DestroyWorld()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
if (!IsCreated())
|
||
|
{
|
||
|
FVoxelMessages::Error("Can't destroy world: not created");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
OnWorldDestroyed.Broadcast();
|
||
|
|
||
|
DestroyWorldInternal();
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
FVoxelIntBox AVoxelWorld::GetWorldBounds() const
|
||
|
{
|
||
|
if (bUseCustomWorldBounds)
|
||
|
{
|
||
|
return
|
||
|
FVoxelUtilities::GetCustomBoundsForDepth<RENDER_CHUNK_SIZE>(
|
||
|
FVoxelIntBox::SafeConstruct(CustomWorldBounds.Min, CustomWorldBounds.Max),
|
||
|
RenderOctreeDepth);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return FVoxelUtilities::GetBoundsFromDepth<RENDER_CHUNK_SIZE>(RenderOctreeDepth);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
void AVoxelWorld::SetGeneratorObject(UVoxelGenerator* NewGenerator)
|
||
|
{
|
||
|
Generator = NewGenerator;
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::SetGeneratorClass(TSubclassOf<UVoxelGenerator> NewGeneratorClass)
|
||
|
{
|
||
|
Generator = NewGeneratorClass.Get();
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::SetRenderOctreeDepth(int32 NewDepth)
|
||
|
{
|
||
|
RenderOctreeDepth = FMath::Max(1, FVoxelUtilities::ClampDepth<RENDER_CHUNK_SIZE>(NewDepth));
|
||
|
WorldSizeInVoxel = FVoxelUtilities::GetSizeFromDepth<RENDER_CHUNK_SIZE>(RenderOctreeDepth);
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::SetWorldSize(int32 NewWorldSizeInVoxels)
|
||
|
{
|
||
|
SetWorldSize(uint32(FMath::Max(0, NewWorldSizeInVoxels)));
|
||
|
}
|
||
|
void AVoxelWorld::SetWorldSize(uint32 NewWorldSizeInVoxels)
|
||
|
{
|
||
|
SetRenderOctreeDepth(FVoxelUtilities::GetDepthFromSize<RENDER_CHUNK_SIZE>(NewWorldSizeInVoxels));
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
FIntVector AVoxelWorld::GlobalToLocal(const FVector& Position, EVoxelWorldCoordinatesRounding Rounding) const
|
||
|
{
|
||
|
const FVector LocalPosition = GetTransform().InverseTransformPosition(Position) / VoxelSize;
|
||
|
|
||
|
FIntVector VoxelPosition;
|
||
|
switch (Rounding)
|
||
|
{
|
||
|
case EVoxelWorldCoordinatesRounding::RoundToNearest: VoxelPosition = FVoxelUtilities::RoundToInt(LocalPosition); break;
|
||
|
case EVoxelWorldCoordinatesRounding::RoundUp: VoxelPosition = FVoxelUtilities::CeilToInt(LocalPosition); break;
|
||
|
case EVoxelWorldCoordinatesRounding::RoundDown: VoxelPosition = FVoxelUtilities::FloorToInt(LocalPosition); break;
|
||
|
default: ensure(false);
|
||
|
}
|
||
|
return VoxelPosition - *WorldOffset;
|
||
|
}
|
||
|
|
||
|
FVector AVoxelWorld::GlobalToLocalFloatBP(const FVector& Position) const
|
||
|
{
|
||
|
return GlobalToLocalFloat(Position).ToFloat();
|
||
|
}
|
||
|
|
||
|
FVoxelVector AVoxelWorld::GlobalToLocalFloat(const FVector& Position) const
|
||
|
{
|
||
|
return GetTransform().InverseTransformPosition(Position) / VoxelSize - FVoxelVector(*WorldOffset);
|
||
|
}
|
||
|
|
||
|
FVector AVoxelWorld::LocalToGlobal(const FIntVector& Position) const
|
||
|
{
|
||
|
return GetTransform().TransformPosition(VoxelSize * FVector(Position + *WorldOffset));
|
||
|
}
|
||
|
|
||
|
FVector AVoxelWorld::LocalToGlobalFloatBP(const FVector& Position) const
|
||
|
{
|
||
|
return LocalToGlobalFloat(Position);
|
||
|
}
|
||
|
|
||
|
FVector AVoxelWorld::LocalToGlobalFloat(const FVoxelVector& Position) const
|
||
|
{
|
||
|
return GetTransform().TransformPosition((VoxelSize * (Position + *WorldOffset)).ToFloat());
|
||
|
}
|
||
|
|
||
|
TArray<FIntVector> AVoxelWorld::GetNeighboringPositions(const FVector& GlobalPosition) const
|
||
|
{
|
||
|
return TArray<FIntVector>(FVoxelUtilities::GetNeighbors(GlobalToLocalFloat(GlobalPosition)));
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
void AVoxelWorld::SetOffset(const FIntVector& OffsetInVoxels)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
CHECK_VOXELWORLD_IS_CREATED_IMPL(this,);
|
||
|
|
||
|
*WorldOffset = OffsetInVoxels;
|
||
|
Renderer->RecomputeMeshPositions();
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::AddOffset(const FIntVector& OffsetInVoxels, bool bMoveActor)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
CHECK_VOXELWORLD_IS_CREATED_IMPL(this,);
|
||
|
|
||
|
if (bMoveActor)
|
||
|
{
|
||
|
SetActorLocation(GetTransform().TransformPosition(VoxelSize * FVector(OffsetInVoxels)));
|
||
|
}
|
||
|
|
||
|
SetOffset(*WorldOffset - OffsetInVoxels);
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
FVoxelGeneratorInit AVoxelWorld::GetGeneratorInit() const
|
||
|
{
|
||
|
return FVoxelGeneratorInit(
|
||
|
VoxelSize,
|
||
|
FVoxelUtilities::GetSizeFromDepth<RENDER_CHUNK_SIZE>(RenderOctreeDepth),
|
||
|
RenderType,
|
||
|
MaterialConfig,
|
||
|
MaterialCollection,
|
||
|
this);
|
||
|
}
|
||
|
|
||
|
UVoxelMultiplayerInterface* AVoxelWorld::CreateMultiplayerInterfaceInstance()
|
||
|
{
|
||
|
FVoxelMessages::Info(FUNCTION_ERROR("Multiplayer with TCP require Voxel Plugin Pro"));
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
UVoxelMultiplayerInterface* AVoxelWorld::GetMultiplayerInterfaceInstance() const
|
||
|
{
|
||
|
FVoxelMessages::Info(FUNCTION_ERROR("Multiplayer with TCP require Voxel Plugin Pro"));
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::SetCollisionResponseToChannel(ECollisionChannel Channel, ECollisionResponse NewResponse)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
CollisionPresets.SetResponseToChannel(Channel, NewResponse);
|
||
|
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
GetWorldRoot().SetCollisionResponseToChannel(Channel, NewResponse);
|
||
|
|
||
|
Renderer->ApplyToAllMeshes([&](UVoxelProceduralMeshComponent& MeshComponent)
|
||
|
{
|
||
|
MeshComponent.SetCollisionResponseToChannel(Channel, NewResponse);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
void AVoxelWorld::BeginPlay()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
Super::BeginPlay();
|
||
|
|
||
|
UpdateCollisionProfile();
|
||
|
|
||
|
if (!IsCreated() && bCreateWorldAutomatically)
|
||
|
{
|
||
|
// Allow other actors begin play to run before creating the world, if they need to create the global pool
|
||
|
auto& TimerManager = GetWorld()->GetTimerManager();
|
||
|
TimerManager.SetTimerForNextTick(this, &AVoxelWorld::CreateWorld);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
Super::EndPlay(EndPlayReason);
|
||
|
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
DestroyWorld();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::Tick(float DeltaTime)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
if (GetWorld()->WorldType != EWorldType::Editor && GetWorld()->WorldType != EWorldType::EditorPreview) // We don't want to tick the BP in preview
|
||
|
{
|
||
|
Super::Tick(DeltaTime);
|
||
|
}
|
||
|
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
WorldRoot->TickWorldRoot();
|
||
|
GameThreadTasks->Flush();
|
||
|
#if WITH_EDITOR
|
||
|
if (PlayType == EVoxelPlayType::Preview && Data->IsDirty())
|
||
|
{
|
||
|
MarkPackageDirty();
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
if (!IsCreated() || !bEnableCustomWorldRebasing)
|
||
|
{
|
||
|
Super::ApplyWorldOffset(InOffset, bWorldShift);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
const FVector RelativeOffset = InOffset / VoxelSize;
|
||
|
const FIntVector IntegerOffset = FVoxelUtilities::RoundToInt(RelativeOffset);
|
||
|
const FVector GlobalIntegerOffset = FVector(IntegerOffset) * VoxelSize;
|
||
|
const FVector Diff = InOffset - GlobalIntegerOffset;
|
||
|
|
||
|
Super::ApplyWorldOffset(Diff, bWorldShift);
|
||
|
|
||
|
*WorldOffset += IntegerOffset;
|
||
|
|
||
|
Renderer->RecomputeMeshPositions();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::OnConstruction(const FTransform& Transform)
|
||
|
{
|
||
|
Super::OnConstruction(Transform);
|
||
|
|
||
|
#if WITH_EDITOR
|
||
|
if (bIsToggled &&
|
||
|
!IsCreated() &&
|
||
|
!GetDefault<UVoxelSettings>()->bDisableAutoPreview &&
|
||
|
(GetWorld()->WorldType == EWorldType::EditorPreview ||
|
||
|
GetWorld()->WorldType == EWorldType::Editor ||
|
||
|
GetWorld()->WorldType == EWorldType::GamePreview))
|
||
|
{
|
||
|
CreateInEditor();
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::UnregisterAllComponents(bool bForReregister)
|
||
|
{
|
||
|
#if WITH_EDITOR
|
||
|
if (bDisableComponentUnregister)
|
||
|
{
|
||
|
Super::UnregisterAllComponents(true); // Do as if it was a reregister so that proc mesh are ignored
|
||
|
}
|
||
|
else
|
||
|
#endif
|
||
|
{
|
||
|
Super::UnregisterAllComponents(bForReregister);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#if WITH_EDITOR
|
||
|
bool AVoxelWorld::ShouldTickIfViewportsOnly() const
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
void AVoxelWorld::BeginDestroy()
|
||
|
{
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
DestroyWorld();
|
||
|
}
|
||
|
|
||
|
// Forward to BeginDestroy AFTER destroying the world, else the components are invalid
|
||
|
// Note: when exiting the components are still invalid. This doesn't seem to be really useful, but might as well do it here still
|
||
|
Super::BeginDestroy();
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::Serialize(FArchive& Ar)
|
||
|
{
|
||
|
Super::Serialize(Ar);
|
||
|
|
||
|
// Temp fix
|
||
|
if (!IsTemplate())
|
||
|
{
|
||
|
CollisionPresets.FixupData(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::PostLoad()
|
||
|
{
|
||
|
Super::PostLoad();
|
||
|
|
||
|
if (!ProcMeshClass)
|
||
|
{
|
||
|
ProcMeshClass = UVoxelProceduralMeshComponent::StaticClass();
|
||
|
}
|
||
|
|
||
|
FVoxelDefaultPool::FixPriorityCategories(PriorityCategories);
|
||
|
FVoxelDefaultPool::FixPriorityOffsets(PriorityOffsets);
|
||
|
|
||
|
SetRenderOctreeDepth(RenderOctreeDepth);
|
||
|
|
||
|
if (int32(UVConfig) >= int32(EVoxelUVConfig::Max))
|
||
|
{
|
||
|
UVConfig = EVoxelUVConfig::GlobalUVs;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#if WITH_EDITOR
|
||
|
|
||
|
void AVoxelWorld::PreEditChange(FProperty* PropertyThatWillChange)
|
||
|
{
|
||
|
bDisableComponentUnregister = true;
|
||
|
Super::PreEditChange(PropertyThatWillChange);
|
||
|
bDisableComponentUnregister = false;
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||
|
{
|
||
|
if (!ProcMeshClass)
|
||
|
{
|
||
|
ProcMeshClass = UVoxelProceduralMeshComponent::StaticClass();
|
||
|
}
|
||
|
|
||
|
if (auto* Property = PropertyChangedEvent.Property)
|
||
|
{
|
||
|
const FName Name = Property->GetFName();
|
||
|
if (Name == GET_MEMBER_NAME_STATIC(FVoxelLODMaterialsBase, StartLOD))
|
||
|
{
|
||
|
for (auto& It : LODMaterials)
|
||
|
{
|
||
|
It.EndLOD = FMath::Max(It.StartLOD, It.EndLOD);
|
||
|
}
|
||
|
for (auto& It : LODMaterialCollections)
|
||
|
{
|
||
|
It.EndLOD = FMath::Max(It.StartLOD, It.EndLOD);
|
||
|
}
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(FVoxelLODMaterialsBase, EndLOD))
|
||
|
{
|
||
|
for (auto& It : LODMaterials)
|
||
|
{
|
||
|
It.StartLOD = FMath::Min(It.StartLOD, It.EndLOD);
|
||
|
}
|
||
|
for (auto& It : LODMaterialCollections)
|
||
|
{
|
||
|
It.StartLOD = FMath::Min(It.StartLOD, It.EndLOD);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (auto* Property = PropertyChangedEvent.MemberProperty)
|
||
|
{
|
||
|
const FName Name = Property->GetFName();
|
||
|
if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, RenderOctreeDepth))
|
||
|
{
|
||
|
SetRenderOctreeDepth(RenderOctreeDepth);
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, WorldSizeInVoxel))
|
||
|
{
|
||
|
SetWorldSize(WorldSizeInVoxel);
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, MaterialsHardness))
|
||
|
{
|
||
|
for (auto& It : MaterialsHardness)
|
||
|
{
|
||
|
if (!It.Key.IsEmpty())
|
||
|
{
|
||
|
It.Key = FString::FromInt(FMath::Clamp(TCString<TCHAR>::Atoi(*It.Key), 0, 255));
|
||
|
}
|
||
|
else if (It.Value == 0)
|
||
|
{
|
||
|
It.Value = 1;
|
||
|
}
|
||
|
It.Value = FMath::Max(It.Value, 0.f);
|
||
|
}
|
||
|
MaterialsHardness.KeySort([](const FString& A, const FString& B) { return TCString<TCHAR>::Atoi(*A) < TCString<TCHAR>::Atoi(*B); });
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, SpawnersCollisionDistanceInVoxel))
|
||
|
{
|
||
|
SpawnersCollisionDistanceInVoxel = FMath::CeilToInt(SpawnersCollisionDistanceInVoxel / 32.f) * 32;
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, ChunksClustersSize))
|
||
|
{
|
||
|
ChunksClustersSize = FMath::Max(ChunksClustersSize, RENDER_CHUNK_SIZE);
|
||
|
const int32 PowerOf2 = FMath::RoundToInt(FMath::Log2(float(ChunksClustersSize)));
|
||
|
ChunksClustersSize = 1 << PowerOf2;
|
||
|
ChunksClustersSize = FMath::Max(ChunksClustersSize, RENDER_CHUNK_SIZE);
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, PriorityCategories))
|
||
|
{
|
||
|
FVoxelDefaultPool::FixPriorityCategories(PriorityCategories);
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, PriorityOffsets))
|
||
|
{
|
||
|
FVoxelDefaultPool::FixPriorityOffsets(PriorityOffsets);
|
||
|
}
|
||
|
else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, MaterialConfig))
|
||
|
{
|
||
|
if (IsCreated() && UVoxelBlueprintLibrary::HasMaterialData(this))
|
||
|
{
|
||
|
const auto Result = FMessageDialog::Open(
|
||
|
EAppMsgType::YesNo,
|
||
|
VOXEL_LOCTEXT("Changing the material config with existing material data! Do you want to clear that data (recommended)?"));
|
||
|
|
||
|
if (Result == EAppReturnType::Yes)
|
||
|
{
|
||
|
UVoxelBlueprintLibrary::ClearMaterialData(this);
|
||
|
GetData().MarkAsDirty();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
OnPropertyChanged_Interactive.Broadcast();
|
||
|
|
||
|
if (IsCreated() && PropertyChangedEvent.ChangeType != EPropertyChangeType::Interactive)
|
||
|
{
|
||
|
OnPropertyChanged.Broadcast();
|
||
|
|
||
|
if (Property->HasMetaData("Recreate"))
|
||
|
{
|
||
|
UVoxelBlueprintLibrary::Recreate(this, true);
|
||
|
}
|
||
|
else if (Property->HasMetaData("RecreateRender"))
|
||
|
{
|
||
|
RecreateRender();
|
||
|
}
|
||
|
else if (Property->HasMetaData("RecreateSpawners"))
|
||
|
{
|
||
|
RecreateSpawners();
|
||
|
}
|
||
|
else if (Property->HasMetaData("UpdateAll"))
|
||
|
{
|
||
|
UpdateDynamicLODSettings();
|
||
|
GetLODManager().UpdateBounds(FVoxelIntBox::Infinite);
|
||
|
}
|
||
|
else if (Property->HasMetaData("UpdateLODs"))
|
||
|
{
|
||
|
UpdateDynamicLODSettings();
|
||
|
GetLODManager().ForceLODsUpdate();
|
||
|
}
|
||
|
else if (Property->HasMetaData("UpdateRenderer"))
|
||
|
{
|
||
|
UpdateDynamicRendererSettings();
|
||
|
GetRenderer().ApplyNewMaterials();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bUseCustomWorldBounds)
|
||
|
{
|
||
|
CustomWorldBounds = GetWorldBounds();
|
||
|
}
|
||
|
|
||
|
bDisableComponentUnregister = true;
|
||
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||
|
bDisableComponentUnregister = false;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
void AVoxelWorld::UpdateCollisionProfile()
|
||
|
{
|
||
|
#if WITH_EDITOR
|
||
|
// This is needed to restore transient collision profile data.
|
||
|
CollisionPresets.LoadProfileData(false);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
///////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
void AVoxelWorld::OnWorldLoadedCallback()
|
||
|
{
|
||
|
LOG_VOXEL(Log, TEXT("%s took %fs to generate"), *GetName(), FPlatformTime::Seconds() - TimeOfCreation);
|
||
|
bIsLoaded = true;
|
||
|
OnWorldLoaded.Broadcast();
|
||
|
}
|
||
|
|
||
|
TVoxelSharedRef<FVoxelDebugManager> AVoxelWorld::CreateDebugManager() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
return FVoxelDebugManager::Create(FVoxelDebugManagerSettings(this, PlayType, Pool.ToSharedRef(), Data.ToSharedRef()));
|
||
|
}
|
||
|
|
||
|
TVoxelSharedRef<FVoxelData> AVoxelWorld::CreateData() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
return FVoxelData::Create(FVoxelDataSettings(this, PlayType), DataOctreeInitialSubdivisionDepth);
|
||
|
}
|
||
|
|
||
|
TVoxelSharedRef<IVoxelPool> AVoxelWorld::CreatePool() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
const auto CreateOwnPool = [&](int32 InNumberOfThreads, bool bInConstantPriorities)
|
||
|
{
|
||
|
return FVoxelDefaultPool::Create(
|
||
|
FMath::Max(1, InNumberOfThreads),
|
||
|
bInConstantPriorities,
|
||
|
PriorityCategories,
|
||
|
PriorityOffsets);
|
||
|
};
|
||
|
|
||
|
if (PlayType == EVoxelPlayType::Preview)
|
||
|
{
|
||
|
return CreateOwnPool(NumberOfThreadsForPreview, true);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (bCreateGlobalPool)
|
||
|
{
|
||
|
const auto ExistingPool = IVoxelPool::GetPoolForWorld(GetWorld());
|
||
|
if (!ExistingPool.IsValid())
|
||
|
{
|
||
|
const auto NewPool = CreateOwnPool(NumberOfThreads, bConstantPriorities);
|
||
|
IVoxelPool::SetWorldPool(GetWorld(), NewPool, GetName());
|
||
|
return NewPool;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
FVoxelMessages::Warning(
|
||
|
"CreateGlobalPool = true but global or world pool is already created! Using existing one, NumberOfThreads will be ignored.\n"
|
||
|
"Consider setting CreateGlobalPool to false and calling CreateWorldVoxelThreadPool at BeginPlay (for instance in your level blueprint).",
|
||
|
this);
|
||
|
return ExistingPool.ToSharedRef();
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
const auto ExistingPool = IVoxelPool::GetPoolForWorld(GetWorld());
|
||
|
if (ExistingPool.IsValid())
|
||
|
{
|
||
|
return ExistingPool.ToSharedRef();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
FVoxelMessages::Warning(
|
||
|
"CreateGlobalPool = false but global pool isn't created! Creating it with default setting NumberOfThreads = 2. "
|
||
|
"You need to call CreateWorldVoxelThreadPool at BeginPlay (for instance in your level blueprint).",
|
||
|
this);
|
||
|
|
||
|
const auto NewPool = CreateOwnPool(NumberOfThreads, bConstantPriorities);
|
||
|
IVoxelPool::SetWorldPool(GetWorld(), NewPool, GetName());
|
||
|
return NewPool;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
TVoxelSharedRef<IVoxelRenderer> AVoxelWorld::CreateRenderer() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
return FVoxelDefaultRenderer::Create(FVoxelRendererSettings(
|
||
|
this,
|
||
|
PlayType,
|
||
|
WorldRoot,
|
||
|
Data.ToSharedRef(),
|
||
|
Pool.ToSharedRef(),
|
||
|
ToolRenderingManager.ToSharedRef(),
|
||
|
DebugManager.ToSharedRef(),
|
||
|
false));
|
||
|
}
|
||
|
|
||
|
TVoxelSharedRef<IVoxelLODManager> AVoxelWorld::CreateLODManager() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
return FVoxelDefaultLODManager::Create(
|
||
|
FVoxelLODSettings(this,
|
||
|
PlayType,
|
||
|
Renderer.ToSharedRef(),
|
||
|
Pool.ToSharedRef()),
|
||
|
this,
|
||
|
LODDynamicSettings);
|
||
|
}
|
||
|
|
||
|
TVoxelSharedPtr<FVoxelEventManager> AVoxelWorld::CreateEventManager() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
return FVoxelEventManager::Create(FVoxelEventManagerSettings(this, PlayType));
|
||
|
}
|
||
|
|
||
|
TVoxelSharedPtr<FVoxelToolRenderingManager> AVoxelWorld::CreateToolRenderingManager() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
return MakeVoxelShared<FVoxelToolRenderingManager>();
|
||
|
}
|
||
|
|
||
|
|
||
|
void AVoxelWorld::CreateWorldInternal(const FVoxelWorldCreateInfo& Info)
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
check(!IsCreated());
|
||
|
|
||
|
LOG_VOXEL(Log, TEXT("Loading world"));
|
||
|
|
||
|
bIsCreated = true;
|
||
|
bIsLoaded = false;
|
||
|
TimeOfCreation = FPlatformTime::Seconds();
|
||
|
|
||
|
if (!Generator.IsValid())
|
||
|
{
|
||
|
FVoxelMessages::Error("Invalid generator!", this);
|
||
|
}
|
||
|
|
||
|
// Setup root
|
||
|
ApplyCollisionSettingsToRoot();
|
||
|
|
||
|
if (PlayType == EVoxelPlayType::Game && WorldRoot->BodyInstance.bSimulatePhysics && CollisionTraceFlag == ECollisionTraceFlag::CTF_UseComplexAsSimple)
|
||
|
{
|
||
|
FVoxelMessages::Error("Simulate physics requires using Simple collisions (either 'Simple And Complex' or 'Use Simple Collision As Complex')", this);
|
||
|
}
|
||
|
|
||
|
GetLineBatchComponent().Flush();
|
||
|
|
||
|
*WorldOffset = FIntVector::ZeroValue;
|
||
|
UpdateDynamicLODSettings();
|
||
|
UpdateDynamicRendererSettings();
|
||
|
|
||
|
ensure(!GeneratorCache);
|
||
|
GeneratorCache = NewObject<UVoxelGeneratorCache>(this);
|
||
|
GeneratorCache->SetGeneratorInit(GetGeneratorInit());
|
||
|
|
||
|
GameThreadTasks = MakeVoxelShared<FGameThreadTasks>();
|
||
|
|
||
|
if (Info.bOverrideData)
|
||
|
{
|
||
|
ensure(!(Info.DataOverride && Info.DataOverride_Raw));
|
||
|
if (Info.DataOverride)
|
||
|
{
|
||
|
if (Info.DataOverride->IsCreated())
|
||
|
{
|
||
|
Data = Info.DataOverride->GetDataSharedPtr();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
FVoxelMessages::Warning(FUNCTION_ERROR("Info.DataOverride is not created! Can't copy data from it."), this);
|
||
|
}
|
||
|
}
|
||
|
else if (Info.DataOverride_Raw)
|
||
|
{
|
||
|
Data = Info.DataOverride_Raw;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
FVoxelMessages::Warning(FUNCTION_ERROR("Info.bOverrideData is true, but DataOverride is null!"), this);
|
||
|
}
|
||
|
}
|
||
|
if (!Data)
|
||
|
{
|
||
|
Data = CreateData();
|
||
|
}
|
||
|
Pool = CreatePool();
|
||
|
DebugManager = CreateDebugManager();
|
||
|
EventManager = CreateEventManager();
|
||
|
ToolRenderingManager = CreateToolRenderingManager();
|
||
|
Renderer = CreateRenderer();
|
||
|
Renderer->OnWorldLoaded.AddUObject(this, &AVoxelWorld::OnWorldLoadedCallback);
|
||
|
LODManager = CreateLODManager();
|
||
|
|
||
|
if (SpawnerConfig)
|
||
|
{
|
||
|
FVoxelMessages::Info("Spawners are only available in Voxel Plugin Pro", this);
|
||
|
}
|
||
|
|
||
|
if (bEnableMultiplayer)
|
||
|
{
|
||
|
FVoxelMessages::Info("TCP Multiplayer is only available in Voxel Plugin Pro", this);
|
||
|
}
|
||
|
|
||
|
if (Info.bOverrideData && Info.bOverrideSave)
|
||
|
{
|
||
|
FVoxelMessages::Warning(FUNCTION_ERROR("Cannot use Info.bOverrideSave if Info.bOverrideData is true!"), this);
|
||
|
}
|
||
|
|
||
|
if (!Info.bOverrideData)
|
||
|
{
|
||
|
// Load if possible
|
||
|
if (Info.bOverrideSave)
|
||
|
{
|
||
|
UVoxelDataTools::LoadFromSave(this, Info.SaveOverride);
|
||
|
}
|
||
|
else if (SaveObject)
|
||
|
{
|
||
|
LoadFromSaveObject();
|
||
|
if (!IsCreated()) // if LoadFromSaveObject destroyed the world
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add placeable items AFTER loading
|
||
|
if (!PlaceableItemManager)
|
||
|
{
|
||
|
// This lets objects adds new items without having to specify a custom class
|
||
|
PlaceableItemManager = NewObject<UVoxelPlaceableItemManager>();
|
||
|
}
|
||
|
PlaceableItemManager->SetExternalGeneratorCache(GeneratorCache);
|
||
|
PlaceableItemManager->Clear();
|
||
|
PlaceableItemManager->Generate();
|
||
|
PlaceableItemManager->ApplyToData(GetData());
|
||
|
PlaceableItemManager->DrawDebug(*this, GetLineBatchComponent());
|
||
|
|
||
|
// Do that after Clear/Generate
|
||
|
ApplyPlaceableItems();
|
||
|
|
||
|
// Let events add other items
|
||
|
OnGenerateWorld.Broadcast();
|
||
|
|
||
|
ensure(!PlaceableItemActorHelper);
|
||
|
PlaceableItemActorHelper = NewObject<UVoxelPlaceableItemActorHelper>(this);
|
||
|
PlaceableItemActorHelper->Initialize();
|
||
|
}
|
||
|
|
||
|
if (PlayType == EVoxelPlayType::Preview)
|
||
|
{
|
||
|
UVoxelProceduralMeshComponent::SetVoxelCollisionsFrozen(false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::DestroyWorldInternal()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
check(IsCreated());
|
||
|
|
||
|
#if WITH_EDITOR
|
||
|
if (PlayType == EVoxelPlayType::Preview)
|
||
|
{
|
||
|
SaveData();
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
LOG_VOXEL(Log, TEXT("Unloading world"));
|
||
|
|
||
|
bIsCreated = false;
|
||
|
bIsLoaded = false;
|
||
|
|
||
|
Data.Reset();
|
||
|
Pool.Reset();
|
||
|
|
||
|
DebugManager->Destroy();
|
||
|
FVoxelUtilities::DeleteTickable(GetWorld(), DebugManager);
|
||
|
|
||
|
EventManager->Destroy();
|
||
|
FVoxelUtilities::DeleteTickable(GetWorld(), EventManager);
|
||
|
|
||
|
Renderer->Destroy();
|
||
|
FVoxelUtilities::DeleteTickable(GetWorld(), Renderer);
|
||
|
|
||
|
LODManager->Destroy();
|
||
|
FVoxelUtilities::DeleteTickable(GetWorld(), LODManager);
|
||
|
|
||
|
|
||
|
WorldOffset = MakeVoxelShared<FIntVector>(FIntVector::ZeroValue);
|
||
|
|
||
|
GameThreadTasks->Flush();
|
||
|
GameThreadTasks.Reset();
|
||
|
|
||
|
// Clear generator cache to avoid keeping instances alive
|
||
|
if (ensure(GeneratorCache))
|
||
|
{
|
||
|
GeneratorCache->ClearCache();
|
||
|
GeneratorCache->MarkAsGarbage();
|
||
|
GeneratorCache = nullptr;
|
||
|
}
|
||
|
|
||
|
if (PlaceableItemActorHelper)
|
||
|
{
|
||
|
PlaceableItemActorHelper->MarkAsGarbage();
|
||
|
PlaceableItemActorHelper = nullptr;
|
||
|
}
|
||
|
|
||
|
DestroyVoxelComponents();
|
||
|
|
||
|
if (LineBatchComponent)
|
||
|
{
|
||
|
// Note: components might be destroyed if called in BeginDestroy (eg if actor deleted in the editor)
|
||
|
LineBatchComponent->Flush();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::DestroyVoxelComponents()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
auto Components = GetComponents(); // need a copy as we are modifying it when destroying comps
|
||
|
for (auto& Component : Components)
|
||
|
{
|
||
|
if (IsValid(Component) && Component->HasAnyFlags(RF_Transient) && (Component->IsA<UVoxelProceduralMeshComponent>() || Component->IsA<UHierarchicalInstancedStaticMeshComponent>()))
|
||
|
{
|
||
|
Component->DestroyComponent();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::LoadFromSaveObject()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
check(SaveObject);
|
||
|
|
||
|
if (SaveObject->Depth == -1)
|
||
|
{
|
||
|
// Not saved yet
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
FVoxelUncompressedWorldSave Save;
|
||
|
UVoxelSaveUtilities::DecompressVoxelSave(SaveObject->Save, Save);
|
||
|
|
||
|
if (Save.Const().GetDepth() == -1)
|
||
|
{
|
||
|
FVoxelMessages::Error("Invalid Save Object!", this);
|
||
|
return;
|
||
|
}
|
||
|
if (Save.Const().GetDepth() > Data->Depth)
|
||
|
{
|
||
|
LOG_VOXEL(Warning, TEXT("Save Object depth is bigger than world depth, the save data outside world bounds will be ignored"));
|
||
|
}
|
||
|
|
||
|
if (!UVoxelDataTools::LoadFromSave(this, Save))
|
||
|
{
|
||
|
const auto Result = FMessageDialog::Open(
|
||
|
EAppMsgType::YesNoCancel,
|
||
|
VOXEL_LOCTEXT("Some errors occured when loading from the save object. Do you want to continue? This might corrupt the save object\n"
|
||
|
"Note: always keep your voxel world toggled when renaming assets referenced by it, else the references won't be updated"));
|
||
|
|
||
|
if (Result != EAppReturnType::Yes)
|
||
|
{
|
||
|
Data->ClearData();
|
||
|
PlayType = EVoxelPlayType::Game; // Hack
|
||
|
DestroyWorldInternal();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::ApplyPlaceableItems()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
TArray<AVoxelPlaceableItemActor*> PlaceableItemActors;
|
||
|
for (TActorIterator<AVoxelPlaceableItemActor> ActorItr(GetWorld()); ActorItr; ++ActorItr)
|
||
|
{
|
||
|
auto* Actor = *ActorItr;
|
||
|
if (Actor->IsA<AVoxelAssetActor>() && !bMergeAssetActors)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
if (Actor->IsA<AVoxelDisableEditsBox>() && !bMergeDisableEditsBoxes)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
if (Actor->bOnlyImportIntoPreviewWorld && Actor->PreviewWorld != this)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
PlaceableItemActors.Add(Actor);
|
||
|
}
|
||
|
|
||
|
TGuardValue<bool> AllowScriptsInEditor(GAllowActorScriptExecutionInEditor, true);
|
||
|
|
||
|
TMap<AVoxelPlaceableItemActor*, int32> Priorities;
|
||
|
for (auto* It : PlaceableItemActors)
|
||
|
{
|
||
|
Priorities.Add(It, It->K2_GetPriority());
|
||
|
}
|
||
|
PlaceableItemActors.Sort([&](auto& ActorA, auto& ActorB) { return Priorities[&ActorA] < Priorities[&ActorB]; });
|
||
|
|
||
|
for (auto* PlaceableItemActor : PlaceableItemActors)
|
||
|
{
|
||
|
PlaceableItemActor->K2_AddItemToWorld(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::UpdateDynamicLODSettings() const
|
||
|
{
|
||
|
LODDynamicSettings->MinLOD = FVoxelUtilities::ClampMesherDepth(MinLOD);
|
||
|
LODDynamicSettings->MaxLOD = FVoxelUtilities::ClampMesherDepth(MaxLOD);
|
||
|
|
||
|
LODDynamicSettings->InvokerDistanceThreshold = InvokerDistanceThreshold;
|
||
|
|
||
|
LODDynamicSettings->ChunksCullingLOD = FVoxelUtilities::ClampDepth<RENDER_CHUNK_SIZE>(ChunksCullingLOD);
|
||
|
|
||
|
LODDynamicSettings->bEnableRender = bRenderWorld;
|
||
|
|
||
|
LODDynamicSettings->bEnableCollisions = PlayType == EVoxelPlayType::Game ? bEnableCollisions : true;
|
||
|
LODDynamicSettings->bComputeVisibleChunksCollisions = PlayType == EVoxelPlayType::Game ? bComputeVisibleChunksCollisions : true;
|
||
|
LODDynamicSettings->VisibleChunksCollisionsMaxLOD = FVoxelUtilities::ClampDepth<RENDER_CHUNK_SIZE>(PlayType == EVoxelPlayType::Game ? VisibleChunksCollisionsMaxLOD : 32);
|
||
|
|
||
|
LODDynamicSettings->bEnableNavmesh = bEnableNavmesh; // bEnableNavmesh is needed for path previews in editor
|
||
|
|
||
|
LODDynamicSettings->bComputeVisibleChunksNavmesh = bComputeVisibleChunksNavmesh;
|
||
|
LODDynamicSettings->VisibleChunksNavmeshMaxLOD = FVoxelUtilities::ClampDepth<RENDER_CHUNK_SIZE>(VisibleChunksNavmeshMaxLOD);
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::UpdateDynamicRendererSettings() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
TArray<UVoxelMaterialCollectionBase*> MaterialCollectionsToInitialize;
|
||
|
for (int32 LOD = 0; LOD < 32; LOD++)
|
||
|
{
|
||
|
auto& LODData = RendererDynamicSettings->LODData[LOD];
|
||
|
|
||
|
// Copy materials
|
||
|
LODData.Material = nullptr;
|
||
|
for (auto& It : LODMaterials)
|
||
|
{
|
||
|
if (It.StartLOD <= LOD && LOD <= It.EndLOD)
|
||
|
{
|
||
|
if (LODData.Material.IsValid())
|
||
|
{
|
||
|
FVoxelMessages::Warning(FString::Printf(TEXT("Multiple materials are assigned to LOD %d!"), LOD), this);
|
||
|
}
|
||
|
LODData.Material = It.Material;
|
||
|
}
|
||
|
}
|
||
|
if (!LODData.Material.IsValid())
|
||
|
{
|
||
|
LODData.Material = VoxelMaterial;
|
||
|
}
|
||
|
|
||
|
// Copy material collection
|
||
|
LODData.MaterialCollection = nullptr;
|
||
|
for (auto& It : LODMaterialCollections)
|
||
|
{
|
||
|
if (It.StartLOD <= LOD && LOD <= It.EndLOD)
|
||
|
{
|
||
|
if (LODData.MaterialCollection.IsValid())
|
||
|
{
|
||
|
FVoxelMessages::Warning(FString::Printf(TEXT("Multiple material collections are assigned to LOD %d!"), LOD), this);
|
||
|
}
|
||
|
LODData.MaterialCollection = It.MaterialCollection;
|
||
|
}
|
||
|
}
|
||
|
if (!LODData.MaterialCollection.IsValid())
|
||
|
{
|
||
|
LODData.MaterialCollection = MaterialCollection;
|
||
|
}
|
||
|
|
||
|
// Set MaxMaterialIndices
|
||
|
if (auto* Collection = LODData.MaterialCollection.Get())
|
||
|
{
|
||
|
LODData.MaxMaterialIndices.Set(FMath::Max(Collection->GetMaxMaterialIndices(), 1));
|
||
|
MaterialCollectionsToInitialize.AddUnique(Collection);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
LODData.MaxMaterialIndices.Set(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Initialize all used collections
|
||
|
for (auto* Collection : MaterialCollectionsToInitialize)
|
||
|
{
|
||
|
Collection->InitializeCollection();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::ApplyCollisionSettingsToRoot() const
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
|
||
|
WorldRoot->CollisionTraceFlag = CollisionTraceFlag;
|
||
|
WorldRoot->CanCharacterStepUpOn = CanCharacterStepUpOn;
|
||
|
WorldRoot->SetGenerateOverlapEvents(bGenerateOverlapEvents);
|
||
|
// Only copy collision data - we don't want to override the physics settings
|
||
|
WorldRoot->BodyInstance.CopyRuntimeBodyInstancePropertiesFrom(&CollisionPresets);
|
||
|
WorldRoot->BodyInstance.SetObjectType(CollisionPresets.GetObjectType());
|
||
|
WorldRoot->BodyInstance.SetPhysMaterialOverride(PhysMaterialOverride);
|
||
|
WorldRoot->BodyInstance.bNotifyRigidBodyCollision = bNotifyRigidBodyCollision;
|
||
|
WorldRoot->BodyInstance.bUseCCD = bUseCCD;
|
||
|
WorldRoot->RecreatePhysicsState();
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::RecreateRender()
|
||
|
{
|
||
|
VOXEL_FUNCTION_COUNTER();
|
||
|
check(IsCreated());
|
||
|
|
||
|
UpdateDynamicLODSettings();
|
||
|
UpdateDynamicRendererSettings();
|
||
|
|
||
|
|
||
|
LODManager->Destroy();
|
||
|
FVoxelUtilities::DeleteTickable(GetWorld(), LODManager);
|
||
|
|
||
|
Renderer->Destroy();
|
||
|
FVoxelUtilities::DeleteTickable(GetWorld(), Renderer);
|
||
|
|
||
|
DestroyVoxelComponents();
|
||
|
|
||
|
Renderer = CreateRenderer();
|
||
|
LODManager = CreateLODManager();
|
||
|
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::RecreateSpawners()
|
||
|
{
|
||
|
if (SpawnerConfig)
|
||
|
{
|
||
|
FVoxelMessages::Info("Spawners are only available in Voxel Plugin Pro", this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::RecreateAll(const FVoxelWorldCreateInfo& Info)
|
||
|
{
|
||
|
check(IsCreated());
|
||
|
|
||
|
DestroyWorldInternal();
|
||
|
CreateWorldInternal(Info);
|
||
|
}
|
||
|
|
||
|
#if WITH_EDITOR
|
||
|
void AVoxelWorld::Toggle()
|
||
|
{
|
||
|
MarkPackageDirty();
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
// ensure(bIsToggled);
|
||
|
bIsToggled = false;
|
||
|
DestroyWorldInternal();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// ensure(!bIsToggled);
|
||
|
bIsToggled = true;
|
||
|
CreateInEditor();
|
||
|
if (!IsCreated()) // Load failed
|
||
|
{
|
||
|
bIsToggled = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::CreateInEditor(const FVoxelWorldCreateInfo& Info)
|
||
|
{
|
||
|
if (!IVoxelWorldEditor::GetVoxelWorldEditor())
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
if (!VoxelWorldEditor)
|
||
|
{
|
||
|
UClass* VoxelWorldEditorClass = IVoxelWorldEditor::GetVoxelWorldEditor()->GetVoxelWorldEditorClass();
|
||
|
|
||
|
if (VoxelWorldEditorClass)
|
||
|
{
|
||
|
// Create/Find VoxelWorldEditor
|
||
|
for (TActorIterator<AActor> It(GetWorld(), VoxelWorldEditorClass); It; ++It)
|
||
|
{
|
||
|
VoxelWorldEditor = *It;
|
||
|
break;
|
||
|
}
|
||
|
if (!VoxelWorldEditor)
|
||
|
{
|
||
|
FActorSpawnParameters Transient;
|
||
|
Transient.ObjectFlags = RF_Transient;
|
||
|
VoxelWorldEditor = GetWorld()->SpawnActor<AActor>(VoxelWorldEditorClass, Transient);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
BindEditorDelegates(this);
|
||
|
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
DestroyWorldInternal();
|
||
|
}
|
||
|
PlayType = EVoxelPlayType::Preview;
|
||
|
CreateWorldInternal(Info);
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::SaveData()
|
||
|
{
|
||
|
if (IsGarbageCollecting())
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
check(IsCreated());
|
||
|
if (Data->IsDirty() && GetTransientPackage() != nullptr)
|
||
|
{
|
||
|
if (!SaveObject && ensure(IVoxelWorldEditor::GetVoxelWorldEditor()))
|
||
|
{
|
||
|
const EAppReturnType::Type Result = FMessageDialog::Open(
|
||
|
EAppMsgType::YesNo,
|
||
|
VOXEL_LOCTEXT("The voxel world Save Object is null. You need to create one now if you don't want to lose your changes.\n\nSave changes?"));
|
||
|
|
||
|
if (Result == EAppReturnType::No)
|
||
|
{
|
||
|
// Clear dirty flag so we don't get more annoying popups
|
||
|
Data->ClearDirtyFlag();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Modify();
|
||
|
SaveObject = IVoxelWorldEditor::GetVoxelWorldEditor()->CreateSaveObject();
|
||
|
}
|
||
|
if (SaveObject)
|
||
|
{
|
||
|
{
|
||
|
FVoxelScopedSlowTask Progress(3);
|
||
|
Progress.EnterProgressFrame(1.f, VOXEL_LOCTEXT("Rounding voxels"));
|
||
|
if (GetDefault<UVoxelSettings>()->bRoundBeforeSaving)
|
||
|
{
|
||
|
UVoxelDataTools::RoundVoxels(this, FVoxelIntBox::Infinite);
|
||
|
}
|
||
|
|
||
|
FVoxelUncompressedWorldSave Save;
|
||
|
|
||
|
Progress.EnterProgressFrame(1.f, VOXEL_LOCTEXT("Creating save"));
|
||
|
UVoxelDataTools::GetSave(this, Save);
|
||
|
|
||
|
Progress.EnterProgressFrame(1.f, VOXEL_LOCTEXT("Compressing save"));
|
||
|
UVoxelSaveUtilities::CompressVoxelSave(Save, SaveObject->Save);
|
||
|
}
|
||
|
|
||
|
SaveObject->CopyDepthFromSave();
|
||
|
SaveObject->MarkPackageDirty();
|
||
|
|
||
|
FNotificationInfo Info = FNotificationInfo(VOXEL_LOCTEXT("Voxel world saved!"));
|
||
|
Info.CheckBoxState = ECheckBoxState::Checked;
|
||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||
|
|
||
|
Data->ClearDirtyFlag();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
FNotificationInfo Info = FNotificationInfo(VOXEL_LOCTEXT("Voxel world not saved: no Save Object!"));
|
||
|
Info.CheckBoxState = ECheckBoxState::Unchecked;
|
||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||
|
}
|
||
|
|
||
|
if (bAutomaticallySaveToFile)
|
||
|
{
|
||
|
FText Error;
|
||
|
if (!SaveToFile(GetDefaultFilePath(), Error))
|
||
|
{
|
||
|
FMessageDialog::Open(EAppMsgType::Ok, Error);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
inline bool CanLoad(const TVoxelSharedPtr<FVoxelData>& Data)
|
||
|
{
|
||
|
if (Data->IsDirty())
|
||
|
{
|
||
|
const auto Result = FMessageDialog::Open(EAppMsgType::YesNoCancel, VOXEL_LOCTEXT("There are unsaved changes. Loading will overwrite them. Confirm?"));
|
||
|
if (Result != EAppReturnType::Yes)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::LoadFromSaveObjectEditor()
|
||
|
{
|
||
|
check(SaveObject);
|
||
|
if (!CanLoad(Data))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
LoadFromSaveObject();
|
||
|
|
||
|
if (IsCreated())
|
||
|
{
|
||
|
const FNotificationInfo Info(VOXEL_LOCTEXT("Loaded!"));
|
||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool AVoxelWorld::SaveToFile(const FString& Path, FText& Error)
|
||
|
{
|
||
|
check(IsCreated());
|
||
|
|
||
|
if (Path.IsEmpty())
|
||
|
{
|
||
|
Error = VOXEL_LOCTEXT("Empty Save File Path");
|
||
|
return false;
|
||
|
}
|
||
|
else if (!FPaths::ValidatePath(Path, &Error))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||
|
|
||
|
PlatformFile.CreateDirectoryTree(*FPaths::GetPath(Path));
|
||
|
|
||
|
FBufferArchive Archive(true);
|
||
|
|
||
|
FVoxelCompressedWorldSave CompressedSave;
|
||
|
UVoxelDataTools::GetCompressedSave(this, CompressedSave);
|
||
|
CompressedSave.Serialize(Archive);
|
||
|
|
||
|
if (FFileHelper::SaveArrayToFile(Archive, *Path))
|
||
|
{
|
||
|
const FNotificationInfo Info(FText::Format(VOXEL_LOCTEXT("{0} was successfully created"), FText::FromString(Path)));
|
||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||
|
|
||
|
if (CompressedSave.Objects.Num() > 0)
|
||
|
{
|
||
|
const FNotificationInfo OtherInfo(FText::Format(VOXEL_LOCTEXT("The voxel data has {0} placeable items (eg, data assets)! These will not be saved"), CompressedSave.Objects.Num()));
|
||
|
FSlateNotificationManager::Get().AddNotification(OtherInfo);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Error = FText::Format(VOXEL_LOCTEXT("Error when creating {0}"), FText::FromString(Path));
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool AVoxelWorld::LoadFromFile(const FString& Path, FText& Error)
|
||
|
{
|
||
|
if (!CanLoad(Data))
|
||
|
{
|
||
|
Error = VOXEL_LOCTEXT("Canceled");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
TArray<uint8> Array;
|
||
|
if (!FFileHelper::LoadFileToArray(Array, *Path))
|
||
|
{
|
||
|
Error = FText::Format(VOXEL_LOCTEXT("Error when reading {0}"), FText::FromString(Path));
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
FMemoryReader Reader(Array);
|
||
|
FVoxelCompressedWorldSave CompressedSave;
|
||
|
CompressedSave.Serialize(Reader);
|
||
|
UVoxelDataTools::LoadFromCompressedSave(this, CompressedSave);
|
||
|
|
||
|
const FNotificationInfo Info(FText::Format(VOXEL_LOCTEXT("{0} was successfully loaded"), FText::FromString(Path)));
|
||
|
FSlateNotificationManager::Get().AddNotification(Info);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
FString AVoxelWorld::GetDefaultFilePath() const
|
||
|
{
|
||
|
FString Path = SaveFilePath;
|
||
|
if (Path.IsEmpty())
|
||
|
{
|
||
|
return Path;
|
||
|
}
|
||
|
Path.RemoveFromEnd(".voxelsave");
|
||
|
int32 Year = 0, Month = 0, DayOfWeek = 0, Day = 0, Hour = 0, Min = 0, Sec = 0, MSec = 0;
|
||
|
FPlatformTime::SystemTime(Year, Month, DayOfWeek, Day, Hour, Min, Sec, MSec);
|
||
|
const FString Sep = TEXT("_");
|
||
|
const FString Date = FString::FromInt(Year) +
|
||
|
Sep + FString::FromInt(Month) +
|
||
|
Sep + FString::FromInt(Day) +
|
||
|
Sep + FString::FromInt(Hour) +
|
||
|
Sep + FString::FromInt(Min) +
|
||
|
Sep + FString::FromInt(Sec) +
|
||
|
Sep + FString::FromInt(MSec);
|
||
|
Path += Date;
|
||
|
Path += ".voxelsave";
|
||
|
return Path;
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::OnPreSaveWorld(uint32 SaveFlags, UWorld* World)
|
||
|
{
|
||
|
if (IsCreated() && PlayType == EVoxelPlayType::Preview)
|
||
|
{
|
||
|
SaveData();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::OnPreBeginPIE(bool bIsSimulating)
|
||
|
{
|
||
|
if (PlayType == EVoxelPlayType::Preview && IsCreated())
|
||
|
{
|
||
|
DestroyWorldInternal();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::OnEndPIE(bool bIsSimulating)
|
||
|
{
|
||
|
if (PlayType == EVoxelPlayType::Preview && ensure(!IsCreated()) && bIsToggled)
|
||
|
{
|
||
|
CreateInEditor();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::OnPrepareToCleanseEditorObject(UObject* Object)
|
||
|
{
|
||
|
if (PlayType == EVoxelPlayType::Preview && IsCreated())
|
||
|
{
|
||
|
DestroyWorldInternal();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::OnPreExit()
|
||
|
{
|
||
|
bIsToggled = false; // Disable saving data
|
||
|
}
|
||
|
|
||
|
void AVoxelWorld::OnApplyObjectToActor(UObject* Object, AActor* Actor)
|
||
|
{
|
||
|
if (Actor != this)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (auto* CastedObject = Cast<UMaterialInterface>(Object))
|
||
|
{
|
||
|
MarkPackageDirty();
|
||
|
|
||
|
VoxelMaterial = CastedObject;
|
||
|
MaterialConfig = EVoxelMaterialConfig::RGB;
|
||
|
RecreateRender();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void IVoxelWorldEditor::SetVoxelWorldEditor(TSharedPtr<IVoxelWorldEditor> InVoxelWorldEditor)
|
||
|
{
|
||
|
check(!VoxelWorldEditor.IsValid());
|
||
|
VoxelWorldEditor = InVoxelWorldEditor;
|
||
|
}
|
||
|
|
||
|
TSharedPtr<IVoxelWorldEditor> IVoxelWorldEditor::VoxelWorldEditor;
|
||
|
IVoxelEditorDelegatesInterface::FBindEditorDelegates IVoxelEditorDelegatesInterface::BindEditorDelegatesDelegate;
|
||
|
#endif
|