In Unreal 5, what's the definitive way of supporting user-generated content?

I finally arrived at a way to do it!
For anyone who comes across this, here’s how I did it. This approach works for both standalone content and Game Feature plugin content.

PAK Is The Way

Title is TL;DR. Here’s why:

  1. Unreal’s docs about patching and DLC talk about PAK files.
  2. A bunch of guides talk about PAK files.
  3. Unreal’s own UGC plugin boils down to PAK files.

How to PAK

PAK files are archive files (like .zip) that contain the assets in your game. You can very well generate a single PAK file for your entire game and share it around with the executable. For our purposes, we can use PAK files to pack new content in them, that we can runtime-load in the game.

There are two similar ways to create new content for your game:

  1. Make a new project with the new content and package it in a PAK. This is covered by this guide.
  2. Make a plugin in the same project. I like this approach, because I wanted tap into Modular Game Features from UE5.

The first method works well, but it needs two different projects so you can’t quickly try out the new content with the base game in-editor. But, it also means that you don’t need the base game project to make content, which is good as long as the interface between the two is standardized (via best practices, fixed spawn points, data assets etc.) However, it’s difficult to change things in the base game via this method, for example, granting extra abilities to the player, which is why I really wanted to make the Game Features plugin method work. I based my code on the first method’s code and went from there.

Plugin

It’s easy to package just the plugin content in a PAK. (Just make sure the plugin is turned off when packaging the base game, otherwise the plugin will also be packaged with it.) The hard part is loading it, owing to a couple differences in the way standalone and plugin PAKs behave.

The first difference is the way their mount points and content paths vary. For example, inspecting the PAKs with UnrealPak <pak_path> -list shows:

  • Standalone PAK
    Mount point ../../../NewContentBuild/Content/
    "Blueprints/Actors/BP_Main.uasset"
    "Levels/TestPad.uexp"
    
  • Plugin PAK
    Mount point ../../../BaseGameProject/Plugins/GameFeatures/MyGameFeature/
    "Content/Blueprints/BP_Main.uasset"
    "Content/Levels/TestPad.uexp"
    

Notice the Content folder: In the standalone PAK, it appears right in the mount point, but in the plugin PAK, it appears in every content file’s path. Because of this, the original code doesn’t work directly, because it appends /Content/ to the mount point of the plugin PAK, leading to the content file paths being read as ../../../BaseGameProject/Plugins/GameFeatures/MyGameFeature/Content/Content/Blueprints/BP_Main.uasset, which obviously do not exist. So we need to change the way mount points are processed for plugins.

Another key difference is the way mount points are converted to relative mount points for both:

  • Standalone PAK

    ../../../NewContentBuild/Content/ => ../../../NewContentBuild/Content/
    
  • Plugin PAK

    ../../../BaseGameProject/Plugins/GameFeatures/MyGameFeature/ => ../../Plugins/GameFeatures/MyGameFeature/
    

For the plugin PAK, which is built in the same project because it’s a game feature plugin, the mount point is relative to the project itself. This doesn’t cause any issue on its own, but for plugin PAKs, the mount point needs to map to /plugin_name/ (as mentioned here) as opposed to /Game/ for a standalone PAK.

So I modified the original PAK loading code to include these changes, but how do we know if a supplied PAK is standalone or a plugin? The method I used (which may not be correct) is inspecting the final relative mount point of the PAK:

  1. If the third directory in the path is "Plugins", then it’s a plugin.
    1. Now, if the fourth directory is "GameFeatures", then the mount point needs to map to the name of the fifth directory (the plugin name).
    2. If not, then map it to the fourth directory itself (which is hopefully the plugin name).
  2. If not, then map it to /Game/.

With these modifications in place (and converting the pak loader into a subsystem), I was finally able to load a plugin PAK into the game! The final project is here: tanmayband/RuntimeContentLoading-UE5-Public (github.com)

2 Likes