Last week we covered soft vs hard references and why hard refs are the main reason your project loads slowly. Demoting big assets to soft references cuts your level load time dramatically.
But it comes with a catch. A soft reference is just a path — the asset isn't in memory yet. The first time you need it, somebody has to actually load it from disk. If you do that the wrong way, your game hitches. Hard.
Let's walk through the pattern that does it right.
The Tick disaster
Here's the trap almost everyone falls into. You have a Soft Object Reference to an inventory icon. You want to display it. You drag the pin into a Resolve Soft Object Reference node — but the icon isn't loaded yet, so it returns None. The icon disappears. Confused, you wire up Load Asset Blocking instead. The icon appears. Ship it.
What you just did: every time that node runs, the game thread stalls until the file is read from disk. On an SSD that's a 20ms stutter you might not notice. On an HDD or a slow USB it's half a second of frozen frames. And if you put it inside a Tick event — which beginners do all the time — your game is now hitching every single frame.
Load Asset Blocking is the wrong tool 95% of the time. Use it only when you have no choice but to wait (e.g. during a deliberate loading screen).
The right pattern
The node you actually want is Async Load Asset (for Soft Object Reference) or Async Load Class Asset (for Soft Class Reference). It loads on a background thread and fires an execution pin when ready.
CB0
Three things to know:
- It's a latent node (the little clock icon). It only works in event graphs and timelines, not in pure functions.
- The
Completedexecution pin fires after the load finishes. The rest of your graph runs immediately, in parallel — don't expect the loaded object to exist beforeCompletedtriggers. - The output is a generic
Object. You'll need to cast it to the type you want before using its properties.
That's the whole pattern. Three nodes, no frozen game.
Don't lose the reference
Async loading has a sneaky failure mode. You load an asset, use it once, and the load callback finishes. The instant your local variable goes out of scope, Unreal garbage-collects the asset because nothing is holding a hard reference to it anymore. Next time you need it, you'll load it again.
The fix: store the loaded object in a member variable on something that's going to stay alive — your PlayerController, a GameInstance subsystem, or a manager actor.
CB1
Now the icon stays in memory as long as the holder exists. When you no longer need it (e.g. the inventory closes), clear the variable so GC can clean up.
Showing a loading spinner
For bigger assets — a new area, a cinematic, a heavy weapon — you'll want to show the user that something is happening. The pattern:
- Show the spinner widget.
- Trigger
Async Load Asset(or several in parallel — see below). - On
Completed, hide the spinner and switch to the new content.
The benefit over a blocking load: the game keeps responding. Animations, music, the spinner itself — all keep ticking. Even the input keeps working. It's the difference between "the game froze" and "the game is loading".
Loading several things at once
Often you don't need one asset — you need five. The naïve approach is to chain them: load A, then B, then C. That works, but they load sequentially. Total time = A + B + C.
A faster pattern is to kick all of them off in parallel and wait for all to finish. In Blueprints you can do this with the Asset Manager:
Load Asset List (Async)— give it an array ofSoft Object References. It returns when all are loaded.Load Class Asset List (Async)— same, for classes.
Or, more flexibly, fire several Async Load Asset nodes in parallel and use a counter: increment on each kick-off, decrement in each Completed callback, when the counter hits zero you're done.
Total time = max(A, B, C). On an SSD with three medium assets, that often goes from 90ms sequential to 35ms parallel.
Common pitfalls
- Calling
Async Load Assetin Tick. Even the async version is wasteful if you re-trigger it every frame. Load once, store the result, reuse. - Casting before
Completedfires. The output is null until the load finishes. Always cast after theCompletedpin. - Forgetting to handle failure. Async Load Asset can return
Noneif the asset is missing or moved. Branch onIs Validbefore using the result. - Async loading tiny assets. If the asset is 2 KB and used every frame, just make it a hard reference. Async loading adds frame-level overhead that's only worth it for big or rare assets.
- Not unloading. Loaded soft references stay in memory until GC runs and nothing references them. If you cache aggressively without clearing, you've reinvented hard references with extra steps.
How to verify it's working
Two commands you should know:
stat LevelStreaming— shows what's currently streaming in and out.stat MemoryPlatform— current memory footprint. Run before and after an async load to confirm the asset actually loaded.
For a deeper view: Window → Developer Tools → Memory opens the live memory profiler. Filter by your asset path and you'll see exactly when it enters and leaves memory.
The 30-second recap
Load Asset Blocking= game thread stall. Avoid except during deliberate loading screens.Async Load Asset= background load, latent, firesCompletedwhen done.- Store the loaded result in a member variable, or GC eats it.
- Multiple assets? Load them in parallel with
Asset Manageror your own counter. - Cast and check
Is ValidafterCompleted, never before.
If you applied the soft-reference rules from last week and now your game hitches at the moment of use, this is the fix. The result: a project that loads fast and runs smooth.
Next week, a step back to basics. I've been writing fairly advanced posts so far — and got messages from a few readers asking "great, but where do I even start?" Fair point. Next Sunday: Your first Unreal Engine project — the right way. The 30 minutes of setup that save you weeks. Sometimes the boring foundations are the most useful thing I can write.
— Marco
Comments
Leave a comment