Skip to content

Added GBFGameFeatureAction_AddLevelInstances #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#include "GameFeatures/GBFGameFeatureAction_AddLevelInstances.h"

#include "Engine/LevelStreamingDynamic.h"
#include "Misc/DataValidation.h"

#define LOCTEXT_NAMESPACE "AncientGameFeatures"

void UGBFGameFeatureAction_AddLevelInstances::OnGameFeatureActivating( FGameFeatureActivatingContext & context )
{
FWorldDelegates::OnWorldCleanup.AddUObject( this, &UGBFGameFeatureAction_AddLevelInstances::OnWorldCleanup );

if ( !ensureAlways( AddedLevels.Num() == 0 ) )
{
DestroyAddedLevels();
}

bIsActivated = true;
Super::OnGameFeatureActivating( context );
}

void UGBFGameFeatureAction_AddLevelInstances::OnGameFeatureDeactivating( FGameFeatureDeactivatingContext & context )
{
DestroyAddedLevels();
bIsActivated = false;

FWorldDelegates::OnWorldCleanup.RemoveAll( this );
Super::OnGameFeatureDeactivating( context );
}

#if WITH_EDITOR
EDataValidationResult UGBFGameFeatureAction_AddLevelInstances::IsDataValid( FDataValidationContext & context ) const
{
auto result = CombineDataValidationResults( Super::IsDataValid( context ), EDataValidationResult::Valid );

auto entry_index = 0;
for ( const auto & [ level, target_world, location, rotation ] : LevelInstanceList )
{
if ( level.IsNull() )
{
result = EDataValidationResult::Invalid;
context.AddError( FText::Format( LOCTEXT( "LevelEntryNull", "Null level reference at index {0} in LevelInstanceList" ), FText::AsNumber( entry_index ) ) );
}

++entry_index;
}

return result;
}
#endif

void UGBFGameFeatureAction_AddLevelInstances::AddToWorld( const FWorldContext & world_context, const FGameFeatureStateChangeContext & change_context )
{
auto * world = world_context.World();

if ( const auto game_instance = world_context.OwningGameInstance;
ensureAlways( bIsActivated ) && ( game_instance != nullptr ) && ( world != nullptr ) && world->IsGameWorld() )
{
AddedLevels.Reserve( AddedLevels.Num() + LevelInstanceList.Num() );

for ( const auto & entry : LevelInstanceList )
{
if ( entry.Level.IsNull() )
{
continue;
}
if ( !entry.TargetWorld.IsNull() )
{
if ( const auto * target_world = entry.TargetWorld.Get();
target_world != world )
{
// This level is intended for a specific world (not this one)
continue;
}
}

LoadDynamicLevelForEntry( entry, world );
}
}

GEngine->BlockTillLevelStreamingCompleted( world );
}

void UGBFGameFeatureAction_AddLevelInstances::OnWorldCleanup( UWorld * world, bool /*session_ended*/, bool /*cleanup_resources*/ )
{
const auto found_index = AddedLevels.IndexOfByPredicate( [ world ]( const ULevelStreamingDynamic * streaming_level ) {
return streaming_level && streaming_level->GetWorld() == world;
} );

if ( found_index != INDEX_NONE )
{
CleanUpAddedLevel( AddedLevels[ found_index ] );
AddedLevels.RemoveAtSwap( found_index );
}
}

ULevelStreamingDynamic * UGBFGameFeatureAction_AddLevelInstances::LoadDynamicLevelForEntry( const FGBFGameFeatureLevelInstanceEntry & entry, UWorld * target_world )
{
auto success = false;
auto * streaming_level_ref = ULevelStreamingDynamic::LoadLevelInstanceBySoftObjectPtr( target_world, entry.Level, entry.Location, entry.Rotation, success );

if ( !success )
{
UE_LOG( LogGameFeatures, Error, TEXT( "[GameFeatureData %s]: Failed to load level instance `%s`." ), *GetPathNameSafe( this ), *entry.Level.ToString() );
}
else if ( streaming_level_ref )
{
AddedLevels.Add( streaming_level_ref );
}

return streaming_level_ref;
}

void UGBFGameFeatureAction_AddLevelInstances::OnLevelLoaded()
{
if ( ensureAlways( bIsActivated ) )
{
// We don't have a way of knowing which instance this was triggered for, so we have to look through them all...
for ( auto * level : AddedLevels )
{
if ( level && level->GetLevelStreamingState() == ELevelStreamingState::LoadedNotVisible )
{
level->SetShouldBeVisible( true );
}
}
}
}

void UGBFGameFeatureAction_AddLevelInstances::DestroyAddedLevels()
{
for ( auto * level : AddedLevels )
{
CleanUpAddedLevel( level );
}
AddedLevels.Empty();
}

void UGBFGameFeatureAction_AddLevelInstances::CleanUpAddedLevel( ULevelStreamingDynamic * level )
{
if ( level != nullptr )
{
level->OnLevelLoaded.RemoveAll( this );
level->SetIsRequestingUnloadAndRemoval( true );
}
}

//////////////////////////////////////////////////////////////////////

#undef LOCTEXT_NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#pragma once

#include "GBFGameFeatureAction_WorldActionBase.h"

#include "GBFGameFeatureAction_AddLevelInstances.generated.h"

class ULevelStreamingDynamic;

// Description of a level to add to the world when this game feature is enabled
USTRUCT()
struct FGBFGameFeatureLevelInstanceEntry
{
GENERATED_BODY()

// The level instance to dynamically load at runtime.
UPROPERTY( EditAnywhere, Category = "Instance Info" )
TSoftObjectPtr< UWorld > Level;

// Specific world to load into. If left null, this level will be loaded for all worlds.
UPROPERTY( EditAnywhere, Category = "Instance Info" )
TSoftObjectPtr< UWorld > TargetWorld;

// The translational offset for this level instance.
UPROPERTY( EditAnywhere, Category = "Instance Info" )
FVector Location = FVector( 0.f );

// The rotational tranform for this level instance.
UPROPERTY( EditAnywhere, Category = "Instance Info" )
FRotator Rotation = FRotator( 0.f );
};

//////////////////////////////////////////////////////////////////////
// UGameFeatureAction_AddLevelInstances

/**
* Loads specified level instances at runtime.
*/
UCLASS( MinimalAPI, meta = ( DisplayName = "Add Level Instances" ) )
class UGBFGameFeatureAction_AddLevelInstances final : public UGBFGameFeatureAction_WorldActionBase
{
GENERATED_BODY()

public:
//~ Begin UGameFeatureAction interface
void OnGameFeatureActivating( FGameFeatureActivatingContext & context ) override;
void OnGameFeatureDeactivating( FGameFeatureDeactivatingContext & context ) override;
//~ End UGameFeatureAction interface

//~ Begin UObject interface
#if WITH_EDITOR
EDataValidationResult IsDataValid( FDataValidationContext & context ) const override;
#endif
//~ End UObject interface

private:
//~ Begin UGameFeatureAction_WorldActionBase interface
void AddToWorld( const FWorldContext & world_context, const FGameFeatureStateChangeContext & change_context ) override;
//~ End UGameFeatureAction_WorldActionBase interface

void OnWorldCleanup( UWorld * world, bool session_ended, bool cleanup_resources );

ULevelStreamingDynamic * LoadDynamicLevelForEntry( const FGBFGameFeatureLevelInstanceEntry & entry, UWorld * target_world );

UFUNCTION() // UFunction so we can bind to a dynamic delegate
void OnLevelLoaded();

void DestroyAddedLevels();
void CleanUpAddedLevel( ULevelStreamingDynamic * level );

/** List of levels to dynamically load when this game feature is enabled */
UPROPERTY( EditAnywhere, Category = "Level Instances", meta = ( TitleProperty = "Level", ShowOnlyInnerProperties ) )
TArray< FGBFGameFeatureLevelInstanceEntry > LevelInstanceList;

UPROPERTY( transient )
TArray< ULevelStreamingDynamic * > AddedLevels;

bool bIsActivated = false;
bool bLayerStateReentrantGuard = false;
};