Improving the MegaAssemblies Workflow

Intro

Prefabs are an incredibly common, extremely powerful tool in any world-builder's kit. If you want to place a campfire, you can combine a firepit, VFX, lights, and sound effects into one asset to be placed and updated together.

Unreal Engine 5 doesn’t have a true prefab system. I know myself and many others were ecstatic when Epic first teased the Packed Level Instance Blueprints (aka MegaAssemblies) system for Unreal 5. When I first got my hands on the early access version of UE5 I found building out MegaAssemblies to be a tedious, confusing process, even after doing it a few times. To make matters worse, the system only supports static meshes. This means a major feature of traditional prefabs – the ability to package multiple asset types together – remains missing.

In an effort to implement a pseudo-prefab system in UE5, I spent countless hours digging through docs, attempting to build them manually through various combinations of features. I landed on a happy marriage between Packed Level Instance Blueprints, Level Instances, and the default in-level editing features in UE5. This results in a more modern feel to building out “prefabs” that is as simple as selecting your assets and using the right-click context menu.

The Overview

Demos

The following demo videos demonstrate using this workflow to build out everyone’s favorite crafting location – a campfire. I tried to use a number of various object classes in this demonstration, but I have yet to find an object class that doesn’t work with this system. If you can place it in a level, it should work properly.

Making the Prefab

In the clip above, we see a scene containing a number of objects. The logs are a Packed Level Instance Blueprint (aka. MegaAssembly), and there is a point light, fire FX, audio emitter, and a static mesh boulder. We select these and then convert to a “prefab” using the right-click context menu which runs a Python script. We then use the level saving menu to name the level we will i nstance, and choose the location in which to save it. The video is in real time, but bigger and more complex prefabs may take longer to process.

Editing the Prefab

In the clip above, we see two instances of our prefab placed in the world. We then demonstrate the default Level Instance/Packed Level Instance editing functionality working with our prefab, and can see the duplicate prefab updating. This is all done in real time, and can be done from any instance, or within the prefab’s generated level.

The Breakdown

The vast majority of this tool was written in Python using Unreal’s API, only using Blueprint scripting to generate the right-click functionality, and to run a feedback message if something goes wrong or the process is cancelled. As such, the majority of the focus will be on the Python API.

Understanding The Problem

The Packed Level Instance workflow for static meshes is an incredible advancement paired with Nanite technology. The only real drawback I have found is that the workflow requires a strong understanding of what is being created and why it is being created. This causes problems for artists who are less technically minded, and inevitably leads to naming and directory issues when assets are saved inappropriately, which means someone must be policing their use constantly to avoid a pile up of mistakes, and eventually worse – a pile-up of redirectors.

Secondly, this workflow simply does not support non-static-mesh assets and as far as I am aware Epic has no intent to include other assets into this workflow. It is strictly reserved for the instancing of static meshes. Level instances however, do support other asset types, and offer much of the same functionality as Packed Level Instances, with the exception that they do not automatically create a Blueprint which can then be used to place additional instances of your prefab. This requires artists to take the additional step after creating a level instance, of creating an appropriately named and placed Blueprint of the LevelInstance Class, then manually assign their new level instance to this Blueprint before continuing.

Building The Custom Python Node

As a preface, earlier in development we had integrated Python into our Unreal workflow, and started the development of our own Blueprint nodes in Python using the @unreal.uclass and @unreal.ufunction wrappers discussed in this post. Since this post does a much better job explaining it, I suggest reading this if you are unfamiliar with that process.

Note: According to the update on the same post that I mention above, this appears to not be the preferred way to integrate Python into blueprints anymore, so I will have to look into updates at a future time. None the less, the following Python code should still work, though the way I am integrating it into my Blueprint may need rework.

Building Factories

The first thing we will find when creating our assets is we need two specific Unreal Factories to generate the types of assets we are trying to build, a Blueprint factory and a World factory. For expandability and ease of reuse, I developed these in their own module called Ue_Factories.

The Blueprint Factory

The code here is relatively straight forward. First we are instantiating our factory, then assigning the class of Blueprint we are making, which is passed through the asset_type arg.

From there, we create the Blueprint itself with the name we passed, at the path we passed, with the newly instantiated factory. We then save the newly loaded Blueprint.

Lastly we perform some path cleanup before returning our new Blueprint actor and the cleaned up path.

The World Factory

Much of the World Factory will look familiar to the Blueprint factory. Again we start by instantiating our factory, though this time we do not need to pass a class. Instead, we check for our prefixes, and use the Unreal Asset Tools to call an Asset Creation dialogue. This will allow our users to input a name (note we have prefaced this name with the established prefix), and select a path for the level that will be created.

From here we simply get the outermost and tidy up some paths to remove information that is unnecessary for our purposes, ensure the name is appropriately formatted, and lastly perform a rename on the newly created world which will automatically save the asset.

Creating The Prefab

With the necessary factories built, we can finally take a look at compiling selected actors in the editor into a Prefab. This first starts with the @unreal.ufunction wrapper:

In the wrapper we first identify that this is a static function, assign the Category the blueprint node will be listed under, and define the return type as a bool.

Next we get all the selected actors using unreal.EditorLevelLibrary.get_selected_level_actors(), and we ensure that something was selected with a length check on our selection. If our selection is greater than 0 we continue, else we return False.

From here on out we know we have a selection of actors and can continue, so we now generate the new world using our world factory. We do some housekeeping by setting a metadata tag for possible use later in development, and collect the world name that our users gave the new world.

Next we need to handle the transformations. When we copy these objects into the new world, they will be copied with their current transforms. While that is ideal for scale and rotation, it is problematic for the location of the assets. If they are made in a live level, they can be thousands of units off the origin, which will become problematic if we then try to open up the newly created level and cannot find the actors in the level. To combat this, we first will get the full transform of each actor, then collect the translation value from those.

From there we add all of these values together in the X and the Y, then divide them by the number of objects in the selection to get an approximated center on both the X and the Y.

Lastly, we simply collect the lowest Z transform value in the selection to serve as the bottom value.

From here we compile the final location, and generate a new unreal.Transform at the new origin for our prefab.

Next we handle creating a relative transform for each of the objects, so we know where to place them in relation to the newly created prefab origin. We then move all the actors to the world origin, then using the relative transform we reassemble them in the correct locations.

At this point we are ready to copy our actors to the new world, and we save the world to ensure it is ready to be added to our blueprint.

Next we generate our Blueprint path and name from the pre-created name and path for the world we just created. From there we use our Blueprint Factory to generate a new blueprint with the given name at the given location, of the class LevelInstance, then we again set a metadata tag for use later in development.

After that we need to load the generated class of the blueprint, so we assemble the full class name and use unreal.load_object to load it. From there we get the default object, which will let us set the WorldAsset for our LevelInstance blueprint, which we then set to the previously created world.

Lastly, we compile and save the blueprint.

All that is left from here is cleaning up the old actors, and replacing them with our newly created prefab at the previously determined location.

Building The Bluetility

The last thing left is to expose this to the user. We do this with a dead simple Bluetility node.

The entire Bluetility Blueprint using our custom Create Prefab Python node.

We have our custom function Make Prefab under the Actor Actions category. When this is called, we simply call our Create Prefab Python node. We know this script will return a bool, so we run this through a Branch node. If the process failed, we run a canned message which is hard coded and simply needs to return a notification to the user. Because we know this script will return False if cancelled, or if the user has nothing selected, we simply notify them that they must select objects before creating a Prefab. The only other failure state I encountered developing this was intermittent engine crashes in our development branch of the Early Access version of UE5, which were also present when using the default MegaAssembly process.

Looking Forward

For the most part, this tool has worked great for the team. The reduction of user input drastically improved the workflow for artists and reduced the opportunity for mistakes exponentially. This is not without it's drawbacks, as LevelInstancing does not play nice with negative scale values for example, but on the whole when we play within the rules it provides a much more modern prefab workflow than is available by default in UE5.

I still see room for improvement in this tool, and I hope to take this further in time. One of the features I think would drastically improve this tool is automating the creation of Packed Level Instance Blueprints alongside the creation of our Prefab Blueprint. As it stands, if users wish to take advantage of the power of MegaAssemblies, they must manually create them using the default process either before the creation of the Prefab, or after within the Prefab. While the flexability is nice, this still remains a sticking point for user error and can be refined.