Level streaming with C++

Hi everyone :D,

I have a short question (or long).

I have now searched for hours and tested some code but I can’t find information about correct level streaming with C++.

What I want to do is:

I have 2 maps (“map1”, “map2”, “this” is a class derived from"AActor").



....
//sample code for used class
UClass()
class Test : public AActor {
  .....
  public:
     void test();  
  ....
}


At first I call:



//FLatentActionInfo isn't document well what it does and when, or I can't find enough information about it
UGameplayStatics::LoadStreamLevel(this, "map1", true, true, FLatentActionInfo()) 


Later ingame “ENTER” is pressed and I want to stream another level, as I had done it with blueprints before for testing.



UGameplayStatics::LoadStreamLevel(this, "map2", true, true, FLatentActionInfo()) 
UGameplayStatics::UnloadStreamLevel(this, "map1", FLatentActionInfo()) 


The first call loads the map correctly. The second call for “map2” loads also correctly but then the “map1” isn’t unloaded. The documentation says that the next call isn’t processed until the first call is finished (“Stream the level with the LevelName ; Calling again before it finishes has no effect.”).

The should be the point why “map1” isn’t unloaded because “map2” is currently loaded and not finished at this point.

I also tried to fill the FLatentActionInfo struct with



FLatentActionInfo info;
info.CallbackTarget=this;
info.ExecutionFunction="test";
info.UUID=12345;


and called



UGameplayStatics::LoadStreamLevel(this, "map1", true, true, info)


I expected that the callback is called if the loading operation was done but the method “test” is never called.

So I wondering about how I can achive it.

If anyone could explain this for me it would be really really great or have a short information which points in the correct direction.

Thank you in advance and have a nice day so far.

Regards

-freakxnet

Hi,

It’s much easier to manage level streaming in blueprints instead of C++.
Take a look at GetStreamingLevel BP node. It gives you full control over level streaming.

!!!solved!!!

Thank you for your post. With blueprints the streaming isn’t a problem. So but I want to have it in Code ;-).

So for all who wants to integrate level streaming within C++ code here’s the solution:

I thought about the callback function of the FLatentActionInfo structure.
You have to define:



FLatenActionInfo info;
info.CallbackTarget = *myObjectWithDerivesFromUObject;
info.ExecutionFunction = "myFunctionToCall";
info.UUID = 1;
info.Linkage = 0;


Normally you would give a function pointer to got access to a callback method. But here you give a method name String. Now for Java, when I does reflection related stuff I use often strings for method / member access. Also for this problem. A string to use is only possible if the method name is resolved by the reflection system.

Then the solution is easy:

You have to define the method within the UE4 reflection system. After I had added the UFUNCTION macro the method was instantly called.




UCLASS()
class TestClass : public AActor {
    public:
       UFUNCTION(BlueprintCallable, Category = Game)
       void myFunctionToCall();

};


Now this is possible:

load “map1” => callback1 => unload “map1” => callback => load “map2” and voila it’s done. The content of “map1” is disappeared and only “map2” stays loaded.

Wrapped into a new controller class its now a simple task to stream levels. But don’t forget to add them within UE4 => MainMenu:Window => Levels.



FLatenActionInfo info;
info.CallbackTarget = *myObjectWithDerivesFromUObject;
info.ExecutionFunction = "myFunctionToCall";
info.UUID = 1;
info.Linkage = 0;

UGameplayStatics::LoadStreamLevel(this, "map1", true, true, info)


Regards

-freakxnet

6 Likes

If you want to use C++ for level streaming you should look at how ALevelStreamingVolume manages this.
Look at UWorld::ProcessLevelStreamingVolumes function.
LevelStreamingVolume directly manipulates bShouldBeLoaded and bShouldBeVisible properties in a ULevelStreaming objects.

I’m so glad freakxnet has asked this question and remained true to C++. For me it’s about being able to allow programmers to do the part that makes sense for them to do, adding level loading with nodes is crazy mad man terrain… Rambo styley. You want to be able to leave the artists to do artsy stuff and level designers to plop things around and get the level working schweeet… and let the programmers do the underlying engine stuff… amirite!

Actually finding that you have to be very careful not to slip into a blueprint coma. now that I have a grip on things, I tend to start with a C++ class and try and aim for properties that the level and artist designers would want to play around with…

Thanks for ddvlost actually helping out there… cool stories all around people.

A small addendum to this topic. If you’re loading many streaming levels at once (e.g. inside a for loop), you have assign different UUIDs for each call. Otherwise it’ll load only the first request. Example:



for (int32 i=0; blablabla)
{
					FLatentActionInfo info;
					info.UUID = i;
					UGameplayStatics::LoadStreamLevel(this, FName(*MapName*), true, true, info);
}


3 Likes

I have to tell you, at least for me, it seems that the best way to make an ue4 game is simply this:

  1. Anything that happens on every tick for lots of objects, make in c++ if you can. Optimizing something that happens every tick gives visible results. The difference between c++ and blueprints is pretty large in execution time. So every object, every tick adds up pretty fast.

  2. Everything else use blueprints. It just doesn’t justify your time IMHO to make a level streaming c++ function. It happens once every few minutes. And you simply wont get any optimization out of c++. Time much better spent elsewhere.

Hi. The above is true if your only concern is investigation time vs execution time. However, the reasons to use C++ are more numerous. To mention some of them, it gives you more control of what is really happening inside, it allows you to use c++ more complex coding, and my favorite, it gives you full control of historic changes. Besides, once you know how to do something in c++, it gives you more development options and even allows you to code more faster and efficiently in terms of organization.

In conclusion, asking how to do something in C++ is never a waisted time. Greetings =)

This is old, but I figured I would clarify something. The Linkage value is an int32 that can be used to “link” the callback to the request. This linkage is useful when you have more than one of these requests currently running.

To use this feature, the function arguments should look like this:

UFUNCTION()
void myFunctionToCall( int32 Linkage );

Hi NinjaCatFail,

this is exaclty the information I am looking for. Unfortunately I do not understand your explanation completely, for what exactly the linkage value is for. What do you mean by request? Could you elaboarte on it maybe with a small example? would be very helpful.

Thanks in advance.

Here is a simple subsystem that manages Streaming Levels. (Note: some of this is not fully tested.)

SimpleLevelManagerSubsystem.h

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "SimpleLevelManagerSubsystem.generated.h"

// Delegates
DECLARE_DELEGATE_OneParam( FOnLoadStreamingLevelFinished, FName );
DECLARE_DELEGATE_OneParam( FOnUnloadStreamingLevelFinished, FName );

// USimpleLevelManagerSubsystem
UCLASS()
class USimpleLevelManagerSubsystem : public UGameInstanceSubsystem
{
   GENERATED_BODY()

public:

   //~UGameInstanceSubsystem interfaces
   virtual void Initialize( FSubsystemCollectionBase &Collection ) override;
   virtual void Deinitialize() override;
   //~End of UGameInstanceSubsystem interfaces

   // Load or unload levels
   bool LoadStreamingLevel( FName LevelName, bool bMakeVisible, FOnLoadStreamingLevelFinished &&Callback );
   void UnloadStreamingLevel( FName LevelName );
   void UnloadStreamingLevel( FName LevelName, FOnUnloadStreamingLevelFinished &&Callback );

private:

   void OnWorldDestroyed( UWorld *World );

   UFUNCTION()
   void OnLoadStreamLevelFinished( int32 Linkage );

   UFUNCTION()
   void OnUnloadStreamLevelFinished( int32 Linkage );

   // Arrays of pending levels
   struct FLevelRefCounter
   {
      FName                                LevelName;
      int32                                Linkage;
      int32                                RefCount = 0;
      bool                                 bIsActive;
      bool                                 bWaitingForLevel;
      bool                                 bVisible;
      FOnLoadStreamingLevelFinished        LoadDelegate;
      FOnUnloadStreamingLevelFinished      UnloadDelegate;
   };

   // Active Levels and the NextUUID
   TArray< FLevelRefCounter > ActiveLevels;
   int32 NextUUID = 1;
};

SimpleLevelManagerSubsystem.cpp

#include "LevelManagerSubsystem.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/World.h"
#include "Engine/Engine.h"

// ULevelManagerSubsystem

// Initialize
void
USimpleLevelManagerSubsystem::Initialize( FSubsystemCollectionBase &Collection )
{
   Super::Initialize( Collection );

   GEngine->OnWorldDestroyed().AddUObject( this, &ThisClass::OnWorldDestroyed );
}

// World Destroyed
void
USimpleLevelManagerSubsystem::OnWorldDestroyed( UWorld *World )
{
   ActiveLevels.Empty();
}

// Initialize -- remove the ticker
void
USimpleLevelManagerSubsystem::Deinitialize()
{
   // Disallow level streaming while shutting down
   UGameplayStatics::FlushLevelStreaming( GetWorld() );

   GEngine->OnWorldDestroyed().RemoveAll( this );

   // All active levels should be unloaded here...
   for ( int32 idx = 0; idx < ActiveLevels.Num(); ++idx )
   {
      // Active levels need to be unloaded
      if ( ActiveLevels[ idx ].bIsActive )
      {
         // Ensure all levels are removed
         ActiveLevels[ idx ].RefCount = 1;
         UnloadStreamingLevel( ActiveLevels[ idx ].LevelName );
      }
   }

   Super::Deinitialize();
}

// Load a streaming level
bool
USimpleLevelManagerSubsystem::LoadStreamingLevel( FName LevelName, bool bMakeVisible, FOnLoadStreamingLevelFinished &&Callback )
{
   // Possible that this is the World that we are on now... if so, we aren't streaming...
   if ( *UGameplayStatics::GetCurrentLevelName( this ) == LevelName )
   {
      Callback.Execute( LevelName );
      return true;
   }

   // Handle incrementing the ref count
   for ( int32 idx = 0; idx < ActiveLevels.Num(); ++idx )
   {
      if ( ActiveLevels[ idx ].LevelName == LevelName )
      {
         ActiveLevels[ idx ].RefCount++;
         Callback.Execute( LevelName );
         return true;
      }
   }

   // Make sure this level exists
   ULevelStreaming *LevelStreaming = UGameplayStatics::GetStreamingLevel( GetWorld(), LevelName );
   if ( !LevelStreaming )
   {
      return false;
   }

   FLatentActionInfo LatentInfo;
   LatentInfo.CallbackTarget = this;
   LatentInfo.UUID = NextUUID++;
   LatentInfo.Linkage = LatentInfo.UUID;
   LatentInfo.ExecutionFunction = "OnLoadStreamLevelFinished";

   int32 idx = ActiveLevels.Add( FLevelRefCounter { LevelName, LatentInfo.UUID, 1, false, true, bMakeVisible, MoveTemp( Callback ) } );

   // Load and activate this level
   UGameplayStatics::LoadStreamLevel( GetWorld(), LevelName, bMakeVisible, false, LatentInfo );

   return true;
}

// Unload the Streaming Level (No Callback)
void
USimpleLevelManagerSubsystem::UnloadStreamingLevel( FName LevelName )
{
   ULevelStreaming *LevelStreaming = UGameplayStatics::GetStreamingLevel( GetWorld(), LevelName );
   if ( !LevelStreaming )
   {
      return;
   }

   // Check active levels and remove this level
   for ( int32 idx = 0; idx < ActiveLevels.Num(); ++idx )
   {
      if ( ActiveLevels[ idx ].LevelName == LevelName )
      {
         ActiveLevels[ idx ].RefCount--;
         if ( ActiveLevels[ idx ].RefCount <= 0 )
         {
            FLatentActionInfo LatentInfo;
            UGameplayStatics::UnloadStreamLevel( GetWorld(), LevelName, LatentInfo, false );
            ActiveLevels.RemoveAt( idx );
         }
         break;
      }
   }
}

// Unload the Streaming Level
void
USimpleLevelManagerSubsystem::UnloadStreamingLevel( FName LevelName, FOnUnloadStreamingLevelFinished &&Callback )
{
   ULevelStreaming *LevelStreaming = UGameplayStatics::GetStreamingLevel( GetWorld(), LevelName );
   if ( !LevelStreaming )
   {
      Callback.Execute( LevelName );
      return;
   }

   // Check active levels and remove this level
   for ( int32 idx = 0; idx < ActiveLevels.Num(); ++idx )
   {
      if ( ActiveLevels[ idx ].LevelName == LevelName )
      {
         ActiveLevels[ idx ].RefCount--;
         if ( ActiveLevels[ idx ].RefCount <= 0 )
         {
            FLatentActionInfo LatentInfo;
            LatentInfo.CallbackTarget = this;
            LatentInfo.UUID = ActiveLevels[ idx ].Linkage;
            LatentInfo.Linkage = LatentInfo.UUID;
            LatentInfo.ExecutionFunction = "OnUnloadStreamLevelFinished";

            ActiveLevels[ idx ].UnloadDelegate = MoveTemp( Callback );

            UGameplayStatics::UnloadStreamLevel( GetWorld(), LevelName, LatentInfo, false );
         }
         else
         {
            Callback.Execute( LevelName );
         }
         break;
      }
   }
}

// Load Streaming Level Callback
void
USimpleLevelManagerSubsystem::OnLoadStreamLevelFinished( int32 Linkage )
{
   for ( int32 idx = 0; idx < ActiveLevels.Num(); ++idx )
   {
      // Matching?
      if ( ActiveLevels[ idx ].Linkage == Linkage )
      {
         ActiveLevels[ idx ].bIsActive = true;
         ActiveLevels[ idx ].bWaitingForLevel = false;
         ActiveLevels[ idx ].LoadDelegate.Execute( ActiveLevels[ idx ].LevelName );
      }
   }
}


// Unload Streaming Level Callback
void
USimpleLevelManagerSubsystem::OnUnloadStreamLevelFinished( int32 Linkage )
{
   for ( int32 idx = 0; idx < ActiveLevels.Num(); ++idx )
   {
      // Matching?
      if ( ActiveLevels[ idx ].Linkage == Linkage )
      {
         ActiveLevels[ idx ].UnloadDelegate.Execute( ActiveLevels[ idx ].LevelName );
         ActiveLevels.RemoveAt( idx );
         return;
      }
   }
}

In this case, we are using the UUID as the Linkage value, and thus when we finish loading, the Linkage can be used to identify which active level to fire the callback on. Hope this clears things up.

2 Likes