From b2b018815c0108967dec43bc7f1eb62d63494d62 Mon Sep 17 00:00:00 2001 From: Michael Delva Date: Mon, 24 Feb 2025 16:31:20 +0100 Subject: [PATCH 1/2] Added GBFGameFeatureAction_AddLevelInstances --- ...GBFGameFeatureAction_AddLevelInstances.cpp | 148 ++++++++++++++++++ .../GBFGameFeatureAction_AddLevelInstances.h | 79 ++++++++++ 2 files changed, 227 insertions(+) create mode 100644 Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp create mode 100644 Source/GameBaseFramework/Public/GameFeatures/GBFGameFeatureAction_AddLevelInstances.h diff --git a/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp b/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp new file mode 100644 index 00000000..c98b7cd1 --- /dev/null +++ b/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp @@ -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 ); + + int32 EntryIndex = 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( EntryIndex ) ) ); + } + + ++EntryIndex; + } + + 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 \ No newline at end of file diff --git a/Source/GameBaseFramework/Public/GameFeatures/GBFGameFeatureAction_AddLevelInstances.h b/Source/GameBaseFramework/Public/GameFeatures/GBFGameFeatureAction_AddLevelInstances.h new file mode 100644 index 00000000..6decaf2a --- /dev/null +++ b/Source/GameBaseFramework/Public/GameFeatures/GBFGameFeatureAction_AddLevelInstances.h @@ -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; +}; From 9a57fe85f0f4c27212938a525c2dfa859a491af2 Mon Sep 17 00:00:00 2001 From: Michael Delva Date: Tue, 25 Feb 2025 10:08:21 +0100 Subject: [PATCH 2/2] coding standard --- .../GBFGameFeatureAction_AddLevelInstances.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp b/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp index c98b7cd1..5c124add 100644 --- a/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp +++ b/Source/GameBaseFramework/Private/GameFeatures/GBFGameFeatureAction_AddLevelInstances.cpp @@ -32,16 +32,16 @@ EDataValidationResult UGBFGameFeatureAction_AddLevelInstances::IsDataValid( FDat { auto result = CombineDataValidationResults( Super::IsDataValid( context ), EDataValidationResult::Valid ); - int32 EntryIndex = 0; + 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( EntryIndex ) ) ); + context.AddError( FText::Format( LOCTEXT( "LevelEntryNull", "Null level reference at index {0} in LevelInstanceList" ), FText::AsNumber( entry_index ) ) ); } - ++EntryIndex; + ++entry_index; } return result; @@ -53,7 +53,7 @@ void UGBFGameFeatureAction_AddLevelInstances::AddToWorld( const FWorldContext & auto * world = world_context.World(); if ( const auto game_instance = world_context.OwningGameInstance; - ensureAlways( bIsActivated ) && ( game_instance != nullptr ) && ( world != nullptr ) && world->IsGameWorld() ) + ensureAlways( bIsActivated ) && ( game_instance != nullptr ) && ( world != nullptr ) && world->IsGameWorld() ) { AddedLevels.Reserve( AddedLevels.Num() + LevelInstanceList.Num() ); @@ -66,7 +66,7 @@ void UGBFGameFeatureAction_AddLevelInstances::AddToWorld( const FWorldContext & if ( !entry.TargetWorld.IsNull() ) { if ( const auto * target_world = entry.TargetWorld.Get(); - target_world != world ) + target_world != world ) { // This level is intended for a specific world (not this one) continue;