Limited by hardware, when the project needs to create a large world, the entire large map cannot and does not need to be loaded into the memory. Like all engines that support the big world, UE adopts a chunked loading method: except for the loading of a persistent level (PersistentLevel), it uses a runtime dynamic loading method. We call these levels sub-levels or streaming levels (StreamingLevel). They only start loading when the player reaches the sublevel boundary.
Based on UE4.27, this article briefly describes the process and timing of streaming level loading. Unless necessary to explain the principle, how to use will not be covered. For specific usage methods, please refer to: YakSue's "Learning UE4's WorldComposition Basics"
Article Directory
Sublevel Editing in Unreal 4
Let’s first experience the editing interface of sub-levels intuitively
Window->Level opens the "Level" tab.
I created three new levels here as sub-levels. It can be seen that except for the default level as a persistent level, other levels are sub-levels. The game will not be loaded into the memory at the beginning, but will be dynamically loaded at runtime according to the player's position and the distance from the checkpoint
Click to call up the world scene composition.
In the composition of the world scene, you can see a default layer: "Uncategorized". Here I created a new layer called "My Layer" to
briefly explain the concept of layer: you can see that when editing sub-levels in the editor, each sub-level belongs to a layer, and the layer can set the streaming distance. This distance is an important attribute for judging the visibility of the level. We will mention it later
If you want to load the level closer to the player, create a new layer, put the target level into the new layer and reduce the streaming distance of the new layer.
Classes involved in streaming level loading
A large world (World) consists of multiple levels (Levels), in which the key classes and their properties related to streaming level loading are as follows: ULevelStreaming: stores
tags and methods related to streaming level loading, such as current state (loading, loaded, loaded but invisible, loaded and visible...), target state, etc.
UWorld: StreamingLevelsToConsider stores the current streaming level to be loaded, and StreamingLevels stores the loaded streaming level. Two containers that are dynamically updated at runtime
UWorldComposition: The main class responsible for loading stream levels, where Tiles stores the summary information of the sub-levels of the World to which it belongs, which is the smallest unit for loading stream levels; TilesStreaming stores its current state. Two TArrays correspond to each other through subscripts. These two data structures are basically only initialized once at the beginning, and store all the stream level information under the World to which they belong.
The Tiles of UWorldComposition can be regarded as one TArray<FWorldCompisitionTile>
, and its key properties are as follows:
// WorldComposition.h
// Helper structure which holds information about level package which participates in world composition
struct FWorldCompositionTile
{
FName PackageName;
FWorldTileInfo Info;
}
And FWorldTileInfo includes the Tile's position (Position), absolute position (AbsolutePosition), boundary (Bounds) and ZOrder and other information
loading process
The loading process is divided into three parts:
- Collect level resources
- Mark flow level state based on player position
- Perform level loading or unloading
Collect stream level resources
When the construction of UWorldComposition is about to be completed, it will be called UWorldComposition::Rescan
to collect flow level resources. This function will search the root directory of the current World, and deserialize the Summary information of the streaming level into memory.
Note that here only a little necessary information of each level is deserialized, such as the position of the level, for subsequent judgment when the level should be loaded. The entire flow level is not deserialized.
After collecting the information into the Tiles structure, call to UWorldComposition::PopulateStreamingLevels
assign an initial state to each flow level.
The result of this step is to complete the collection of stream level information, and the specific structure is the initialization of Tiles and TilesStreaming.
This step is basically only performed once towards the end of construction. The next two steps are repeated as the game progresses.
Compute Tiles visibility based on player position
UWorld::Tick()
It will be called UWorldComposition::UpdateStreamingState
here to calculate the levels that should be loaded and unloaded based on some positions that must be loaded, such as the player's current position and the position of PersistentLevel.
This step is just to calculate the state of the level, mark the TargetState of the level, and put the level to be loaded into the StreamingLevelsToConsider container of UWorld. doesn't actually load the level
Calculation method of level visibility :
It is written in the source code comments:
Check if tile bounding box intersects with a sphere with origin at provided location and with radius equal to tile layer distance settings
That is to judge whether the bounding box of the level (Tile) intersects with the sphere at the specified position as the center of the circle, where the radius of the sphere is the streaming distance set by the layer where the level (Tile) is located .
The key part of the source code is as follows:
void UWorldComposition::GetDistanceVisibleLevels(
const FVector* InLocations,
int32 NumLocations,
TArray<FDistanceVisibleLevel>& OutVisibleLevels,
TArray<FDistanceVisibleLevel>& OutHiddenLevels) const
{
……
for (int32 TileIdx = 0; TileIdx < Tiles.Num(); TileIdx++)
{
……
for (int32 LocationIdx = 0; LocationIdx < NumLocations; ++LocationIdx)
{
FSphere QuerySphere(InLocations[LocationIdx], TileStreamingDistance);
if (FMath::SphereAABBIntersection(QuerySphere, LevelBounds))
{
VisibleLevel.LODIndex = LODIdx;
bIsVisible = true;
break;
}
}
}
}
The function passes in some keypoint positions InLocations
, such as the player's position.
FSphere QuerySphere(InLocations[LocationIdx], TileStreamingDistance)
: Construct a sphere with the key point as InLocations[LocationIdx]
the center and radius, and judge whether the sphere intersects with the border of the level. TileStreamingDistance
If it intersects, it is considered that the level is very close to the player and needs to be loaded into memory.
Perform streaming level loading
Finally got to the real loading place. When the client draws to the screen, the level is loaded into memory and displayed according to the StreamingLevelsToConsider collected in the previous step.
The actual loading
ULevelStreaming::RequestLevel
happens in:
bool ULevelStreaming::RequestLevel(UWorld* PersistentWorld, bool bAllowLevelLoadRequests, EReqLevelBlock BlockPolicy)
{
... ...
const FName DesiredPackageName = bIsGameWorld ? GetLODPackageName() : GetWorldAssetPackageFName();
if (bAllowLevelLoadRequests)
{
const FName DesiredPackageNameToLoad = bIsGameWorld ? GetLODPackageNameToLoad() : PackageNameToLoad;
const FString PackageNameToLoadFrom = DesiredPackageNameToLoad != NAME_None ? DesiredPackageNameToLoad.ToString() : DesiredPackageName.ToString();
if (FPackageName::DoesPackageExist(PackageNameToLoadFrom))
{
... ...
LoadPackageAsync(DesiredPackageName.ToString(), nullptr, *PackageNameToLoadFrom, FLoadPackageAsyncDelegate::CreateUObject(this, &ULevelStreaming::AsyncLevelLoadComplete), PackageFlags, PIEInstanceID, GetPriority());
... ...
}
... ...
}
Application: Block the loading of some sub-levels
If there are sub-level resources locally, but you do not want to load them at runtime, you need to shield the sub-levels. UWorldComposition::Rescan
Just block the specified level when collecting .
Note that since the information collection and loading of sub-levels are carried out on the Server and Client respectively, it is not sent from the Server to the Client through network synchronization. If the shielding operation is to be performed, both the server and the client need to perform it.
Otherwise, the server will block the surface but the client will still display it, and because the collision judgment is on the server, this surface will have no collision: