Search Unity

Rapid design iteration in Breachers using AssetPostprocessor and Blender

April 18, 2023 in Games | 17 min. read
Rapid design iteration in Breachers using AssetPostprocessor and Blender | Hero image
Rapid design iteration in Breachers using AssetPostprocessor and Blender | Hero image
Share

Is this article helpful for you?

Thank you for your feedback!

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.

This content is hosted by a third party provider that does not allow video views without acceptance of Targeting Cookies. Please set your cookie preferences for Targeting Cookies to yes if you wish to view videos from these providers.

Finding the right workflow

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:

  • Setting up dynamic objects in the scene such as weapon pickups, spawn doors, capture points
  • Placing colliders to prevent players from walking or teleporting in certain areas
  • Setting up invisible guides to allow bots to behave properly
  • Etc.
Hyper Dash (2021)
Hyper Dash (2021)

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.

Using the prototype to identify pain points

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.

Breachers: Skyscraper rappel in 4K

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.

Figure 2: Breachers image provided by Triangle Factory (Prototyping the Hideout level)
Prototyping our Hideout level
Figure 3.5: Breachers image provided by Triangle Factory (The final version of Hideout)
The final version of Hideout

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.

Figure 3: Breachers image provided by Triangle Factory (Breakable windows in a sandbox level during the prototype)
Breakable windows in a sandbox level during the prototype

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.

Setting clear goals for the production pipeline

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:

  • All creation and modification of level geometry should happen in Blender.
  • WYSIWYG: What a designer creates in Blender should match the result in Unity as closely as possible.
  • When something is updated in Blender, importing the changes into Unity should happen automatically and not require any manual effort.
Figure 4: Breachers image provided by Triangle Factory

Making Blender love Unity

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.

Figure 5: Breachers image provided by Triangle Factory [Part of our Skyscraper level in Blender (static level geometry and props only)]
Part of our Skyscraper level in Blender (static level geometry and props only)
Figure 6: Breachers image provided by Triangle Factory (The same scene with dynamic objects added)
The same scene with dynamic objects added
Figure 7: Breachers image provided by Triangle Factory (The same scene imported into Unity, before light baking)
The same scene imported into Unity, before light baking
Figure 8: Breachers image provided by Triangle Factory (The final scene in Unity)
The final scene in Unity

Custom properties and Unity’s AssetPostprocessor

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.

Figure 9: Breachers image provided by Triangle Factory (An example of custom properties assigned to an object in Blender)
An example of custom properties assigned to an object in Blender

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.

Use cases

Prefab links

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.

Figure 10: Breachers image provided by Triangle Factory

Surface types

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.

Static flags

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.

Colliders

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.

Figure 11: Breachers image provided by Triangle Factory (Observe the _BoxCollider and _NoCollision names in Blender)
Observe the _BoxCollider and _NoCollision names in Blender
Figure 12: Breachers image provided by Triangle Factory (Object marked as _BoxCollider gets translated to an actual BoxCollider in Unity)
Object marked as _BoxCollider gets translated to an actual BoxCollider in Unity

Code example

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
  private readonly Dictionary<string, (string[], object[])> _userPropertyMap = new ();

  // サポートされているすべてのカスタムプロパティ
  private static readonly string[] SupportedPropNames = new []
  {
      "Surface",
      "Layer",
      "PrefabLink",
      "Collision",
      "StaticFlags",
      "LightmapScale",
      "LightMeshPreset"
  };

  // AssetPostprocessor の Unity イベント
  // モデルの各オブジェクトに対して呼び出される
  private void OnPostprocessGameObjectWithUserProperties(GameObject go, string[] propNames, object[] values)
  {
  // 対象のカスタムプロパティが含まれているか確認し、Dictionary に追加する
  if (SupportedPropNames.Select(x => x.ToLowerInvariant()).Intersect(propNames.Select(x => x.ToLowerInvariant())).Any())
      {
          _userPropertyMap.Add(go.name, (propNames, values));
      }
  }

  // AssetPostprocessor の Unity イベント
  private void OnPostprocessModel(GameObject model)
  {
      // 見つかった各カスタムプロパティに対して
      // Model Prefab Variant 内の対応する Gameobject を取得し
      // 適切なロジックを適用する
      for(int i = _userPropertyMap.Count -1; i >= 0; i--)
      {
          var kvp = _userPropertyMap.ElementAt(i);
          GameObject go = FindGameObjectInHierarchy(model, kvp.Key);	// モデルの子要素を名前で検索
          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;
                  // ...
              }
          }
      }
  }

  // Blender のカスタムプロパティに基づいてオブジェクトに StaticFlags を適用
  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);
  }
}

Customizing Blender to work better with Unity

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.

Blender asset library

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.

Figure 13: Breachers image provided by Triangle Factory (Part of our props library in Blender)
Part of our props library in Blender
Figure 14: Breachers image provided by Triangle Factory (All of the dynamic object types that use the PrefabLink custom property)
All of the dynamic object types that use the PrefabLink custom property

Custom Blender Addons

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.

Figure 15: Breachers image provided by Triangle Factory

Conclusion

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!

4K still from Triangle Factory's VR FPS Breachers

Triangle Factory’s Breachers is now available. Check out more blogs from Made with Unity developers here.

April 18, 2023 in Games | 17 min. read

Is this article helpful for you?

Thank you for your feedback!

Related Posts