Triangle Factory is a fast-growing Belgian gaming company that uses Unity to create high-quality multiplayer VR titles like Hyper Dash and their latest game, Breachers. Triangle Factory leverages tools like Cinemachine, Unity Profiler, Game Server Hosting, Matchmaker, Voice Chat (Vivox), and Friends in creating an immersive experience for players.
In this blog, Jel Sadones, lead level design/tech art, and lead developer Pieter Vantorre walk us through their Blender-to-Unity pipeline and how they brought their VR tactical FPS title Breachers to life.
Unity has been our go-to engine and development environment for over a decade, and we have gone through many workflows over the years for environment modeling and design. That includes using in-engine modeling tools like ProBuilder (which we still use and love for rapid prototyping) and assembling scenes from Prefabs created in other modeling packages. For our current projects though, we’ve landed on a workflow where we model and organize our levels in Blender, and rely on Unity’s AssetPostprocessor to integrate them into our Unity project.
In this article, we’ll share with you how we ended up with this workflow and how it supports the kind of rapid design iteration we need for our games.
In 2021, we released our first big VR title, Hyper Dash, a fast-paced 5v5 arena shooter. When we started development on the game in 2019, we had a basic Blender-to-Unity workflow that probably looks familiar to many: We simply modeled geometry in Blender, exported our assets as FBX files and manually integrated them into Unity. The manual integration involved several steps:
This process can work well for smaller projects, but quickly becomes cumbersome as a project scales and evolves. When we started planning the development of our next title, we knew we were going to need a drastically improved workflow.
Breachers is a competitive shooter with complex level layouts, subtler gameplay mechanics, more technical systems at play, and a higher level of graphical polish targeting the newest generation of standalone VR hardware. In terms of complexity, it goes several steps further than Hyper Dash, and we quickly felt the effects of this on our workflow.
In the prototyping phase, we still relied heavily on Prefabs for dynamic objects, like window barricades for example. These are objects that we place inside window frames to block line of sight between interiors and the outside to prevent teams from seeing each other during the warm-up phase of the game.
While testing our prototype, we were constantly moving around windows to improve gameplay, which meant changing geometry in Blender and re-exporting to Unity and then manually moving the barricade objects to match our changes. Many hours were spent flying around Unity’s Scene view, manually checking and fixing these kinds of things. Still, we had more than one playtest where we only noticed during gameplay that something had been overlooked.
Obviously, this workflow was not going to give us the ability to quickly iterate on our map designs as we playtested, both internally and as part of our open alpha, where we planned to make one map available for free to get feedback from the community. We were looking forward to all that feedback, but not looking forward at all to the manual effort involved in applying it to our maps.
Another potential downside to a Prefab-based design workflow is performance. We mainly target mobile, standalone VR headsets for our games. We want to push the visuals as far as we can, so we need to squeeze every last drop of performance out of our workflow.
Assembling levels from Prefabs can be less efficient than creating a watertight mesh in a modeling program. If you snap two modular wall pieces together, you always have an unmerged loop of geometry in between them. With Prefabs, it’s also easy to end up placing a lot of geometry in your scene that isn’t visible (because it’s on the underside of an object, or placed against a wall) but still taking up valuable lightmap space. Over an entire level, those small inefficiencies can add up to wasted performance and diminished visuals.
The final issue with Prefabs we want to mention is that it can be easy to break things by applying seemingly innocent changes to the source model in Blender, like renaming an object. As a game or level evolves, you often want to reorganize your assets and give them improved or more consistent names. But renaming an object in Blender and re-exporting it can easily (and without warning) break the overrides and additions made to the object in Unity, leading to regressions.
In this simplified example, we have a ventilation grate Prefab and want smoke coming out of it. After importing the mesh into Unity, our artist has added the smoke particle system as a child object and added a surface type component to the Prefab to mark it as being a metal object.
Here you can see what happens if we rename our mesh in Blender:
When reimporting the mesh with the updated name, Unity can no longer find the old mesh by name, so it removes the object from the model Prefab. Children of this removed object are moved to the root of the Prefab and existing scripts are removed, again leading to manual cleanup work we’d rather avoid.
As the prototyping phase for Breachers wrapped up and we prepared to go into full production mode in early 2022, our art and dev teams sat down together and investigated what we could do to remedy these problems. We defined clear goals for our ideal asset pipeline, one that would support the rapid and flexible iteration required for Breachers:
As mentioned above, our main goal was to have an accurate visualization of the game in Blender – not only properly reflecting how the end result will look in Unity but also how the gameplay mechanics are set up. Gameplay in Breachers not only depends on a level’s layout, but also on dynamic objects (like breachable walls) and invisible elements (like sound volumes and colliders). We want to have all this information visible at the design stage and carried over precisely to Unity.
Custom properties are critical to our workflow, and we assign these to objects in Blender. These are then carried over in Unity by the FBX format, so we can read them and run custom logic when our assets are imported into Unity.
This gives us a great amount of flexibility, as well as stability. These properties stay connected to objects throughout the pipeline, so we can reorganize and rename things in our levels as much as we want without worrying about things breaking or getting out of sync.
Unity has a powerful class called AssetPostprocessor, which allows modifications of assets while they are being imported. This is what we use at import time to parse those custom properties and act on them.
We have a custom property named PrefabLink, which tells Unity the object imported from Blender should be replaced by a Prefab already in the Unity project, while preserving the imported model’s transform. This allows us to place these dynamic objects in Blender while retaining the advantages of Prefabs once they are imported into Unity. The window barricades in the Blender scene above are a good example of this.
Surface definition is extremely important in Breachers. Walking on a metal staircase sounds different from walking on a concrete floor. Bullet penetration through wood is a lot different than through steel. And each surface type has its own impact effects. Going over each prop in Unity and tagging it as the correct surface type would be extremely time consuming, so we also tackle this at the design stage in Blender by setting custom properties on our geometry colliders.
Another important setting for optimization are Unity’s static flags. Setting these correctly can have a profound impact on things like visibility culling, light baking, and batching. Using custom properties in Blender, we can set these on any part of the level, including reusable props, and have that information carry over into Unity across our levels.
Lastly, we’d like to share how we set up colliders. Unity has a simple but effective system that automatically detects level-of-detail variants for models when you postfix a model asset name with _LOD0, _LOD1, etc. We were inspired by this and created a similar system for colliders: By simply having geometry with _BoxCollider or _NoCollision in the name, we replace the meshes from Blender with colliders in Unity.
As a concrete example, here is a snippet of our LevelSetupPostprocessor that reads custom properties and assigns the right static flags on each imported object:
public class LevelSetupPostprocessor : AssetPostprocessor
{
// Dictionary of each object that is using a custom property.
private readonly Dictionary<string, (string[], object[])> _userPropertyMap = new ();
// List of all the custom properties we support
private static readonly string[] SupportedPropNames = new []
{
"Surface",
"Layer",
"PrefabLink",
"Collision",
"StaticFlags",
"LightmapScale",
"LightMeshPreset"
};
// Unity Event from AssetPostprocessor
// Called for each object in the model
private void OnPostprocessGameObjectWithUserProperties(GameObject go, string[] propNames, object[] values)
{
// Check if the custom properties contain any that we are interested in and add them to the dictionary.
if (SupportedPropNames.Select(x => x.ToLowerInvariant()).Intersect(propNames.Select(x => x.ToLowerInvariant())).Any())
{
_userPropertyMap.Add(go.name, (propNames, values));
}
}
// Unity Event from AssetPostprocessor
private void OnPostprocessModel(GameObject model)
{
// For each of the discovered custom properties,
// find the corresponding gameobject in the Model Prefab Variant
// and apply the appropriate logic
for(int i = _userPropertyMap.Count -1; i >= 0; i--)
{
var kvp = _userPropertyMap.ElementAt(i);
GameObject go = FindGameObjectInHierarchy(model, kvp.Key); // searches the model's children by name
string[] propNames = kvp.Value.Item1;
object[] values = kvp.Value.Item2;
for(int j = 0; j < propNames.Length; j++)
{
object value = values[j];
switch (propNames[j])
{
case "staticflags":
HandleStaticFlags(go, value);
break;
// ...
}
}
}
}
// Applies StaticFlags on the object based on custom properties from Blender
private void HandleStaticFlags(GameObject go, object value)
{
string[] staticFlags = value.ToString().Split(',');
StaticEditorFlags activeFlags = 0;
for(int i = 0; i < staticFlags.Length; ++i)
{
string flag = staticFlags[i].ToLower().Trim();
switch (flag)
{
case "batching static":
activeFlags |= StaticEditorFlags.BatchingStatic;
break;
// ...
default:
LogWarning($"Unknown static flag {flag} detected when importing {go.name}", go);
break;
}
}
GameObjectUtility.SetStaticEditorFlags(go, activeFlags);
}
}
For all of this to work smoothly, we did have to do some work on the Blender side as well.
Custom properties are a bit hidden in Blender’s UI and would require artists to manually type out the custom properties each time, which is not a great user experience. Relying on manual text entry would also be very error-prone, undoing much of the advantage of setting things up in Blender in the first place. Moving from a Prefab-based workflow into Blender also made us miss some of the advantages of Prefabs, like having a nice library of objects to browse through and pick from. Luckily, Blender, like Unity, is very flexible and easily extensible.
The answer to the Prefab organization problem came in Blender 3.2 with Asset Libraries. This system acts a bit like the Prefab system in Unity: It allows you to create assets in a separate file and then import those into your Blender scene, while changes in the asset file reflect automatically in the Blender scene. Additionally, it ensures that any custom properties or colliders are correctly applied to each instance of this asset in Blender.
For Blender, we wrote an in-house add-on to help set up the custom properties in a more clear user interface. This simplifies setting custom properties by just selecting the relevant Blender objects and hitting a button, instead of typing out each property manually.
The Bundle Exporter add-on is an open source add-on that we’re using to export all of our FBX files in one click. We modified it to also work with custom properties and updated the UI to have faster exports for our specific needs.
Setting up our level design workflow for Breachers took a large time investment initially, but we believe it was the right choice for the project. Also, it was kind of fun!
As we’ve built out the game from initial blockouts through alpha testing and the months leading up to the final release, iterating on our levels has been quick and painless. We’ve been able to eliminate overhead and busywork for our designers and artists, while also transferring responsibilities to them that they previously would have needed a developer for.
We have been impressed at both Unity and Blender for their ability to integrate with each other this smoothly, and we strongly believe this integration was critical to making Breachers a game we’re happy with and proud to share with the world.
Thanks for reading, and enjoy the game!
Triangle Factory’s Breachers is now available. Check out more blogs from Made with Unity developers here.
Is this article helpful for you?
Thank you for your feedback!