fbpx
#Devlogs#Gamedev Tutorials

How Anti Air makes use of the Unity – Combine Meshes functionality for optimization

Anti Air: The Cubicon Conjunction

Share
FavoriteLoadingAdd to favorites

Where did the idea for the game come from?

Having grown up with a lot of the (retro) games in the 80s, these games are a big inspiration. For some reason my childhood experiences left a strong ingrained impression and the essence of these revive at the surface of my thoughts regularly, especially when it comes to video game development.

Some of the games that are a big inspiration for Anti Air have been Missile Command where you have to shoot down missiles from a central location, another game is Space Invaders for its variety of enemies and strong icon/artistic characters. Centipede and Asteroids have also been a great sources of inspiration since they show breakable enemies/obstacles that intensify the player its situation and when engaging them they increasingly intensify the situation.

Discontinue has been the first VR title I developed. It was like tipping my toe into Virtual Reality(VR) game development waters. After that experience, this idea originated of bringing retro gameplay features to VR, and develop a homage to retro gaming on this new technology/platform. Hence the birth of Anti Air.

Key Feature: Breakable enemy Cubicons

One of the key features in the game is that enemies have breakable parts that the player can blast off.
So the player can play around and completely demolish the enemies by shooting all over it.

It started with a simple experiment/prototype in which things were shot apart, these objects were put together in the Unity editor using the basic shapes.
So instead of using a 3D modellling tool such as Maya or Blender to create the 3D objects, the Cubicon enemies were put together inside the Unity editor using simple cube shapes and in turn making the Editor function as design tool for the Cubicons.

After having the shape of an enemy there is the option to make larger groups of cubes. This is useful as you might want a combination of cubes (of an object/enemy) to act as one part and have the option to reuse them.

When you have the complete object you can use the Prefab Utility class in Unity to save your editor/game object as a prefab. It's handy to have a prefab for each obstacle/enemy so you can eventually also easily spawn them.
Unity link to: PrefabUtility

Now for some optimization the following techniques were applied.

Shared materials
Cubes that look the same can use the same material, so for this we used the shared material feature Unity provides. Which helps a lot to reduce the draw calls. 
(Note: Also look into enabling GPU instancing on your materials)

Mesh combiners
The Mesh combine feature in Unity was used to generate one mesh out of several cubes, so these larger parts become one mesh. This also works for multiple materials, shapes with the same material will become one group of objects and the various groups are again combined.
This is done using the mesh combine feature on Meshes in Unity.
Unity Link to: Mesh.CombineMeshes

Vertices
One more optional feature is to optimize some meshes by removing and wielding vertices of a mesh that was just put together from multiple smaller cubes. The list of vertices can be searched through to see if any overlap or are hidden because they are in the middle, and if so remove it and/or connect some.

 

Let’s look into the Unity Combine Meshes feature

Maybe the Mesh combine feature is a more hidden feature/gem that is available on Meshes in Unity, but worth to be aware of.
Amongst other information Unity lets us know the following on their website
(https://docs.unity3d.com/ScriptReference/Mesh.CombineMeshes.html):
“Combines several Meshes into this Mesh.

Combining Meshes is useful for performance optimization.”

An important distinction to be made is;
- using this feature on a mesh with a single material
- or using this material on a mesh with multiple materials.

More performance benefit is to be gained when using it in combination with only 1 material.

The method signature is the following:

public void CombineMeshes(CombineInstance[] combine, bool mergeSubMeshes = true, bool useMatrices = true, bool hasLightmapData = false);

Here you can see there are 3 optional parameters, so in essence you only need to pass it an array of CombineInstance’s. And by default it expects you to only have a single material on the meshes but if you have multiple make sure to set mergeSubMeshes to false.

 

Step by step guide

With the above information now look at a series of images that demonstrate the usage.
These show 14 illustration to put provide an overview of the different elements involved in putting it all together. Step 1-7 show how to implement this in a Unity project and 8-14 show more details about the CombineMesh script that is used to turn multiple meshes into a single one.

Step #1 Assets used, these are the core elements used in this example.
Script: CombineScript

This script is available below the article and is described in step 8-14. This is used to optimize your mesh by executing the Unity Mesh.CombineMeshes functionality on your Game Object containing multiple children with individual meshes.

Scene: ExampleMesh
This is a Unity scene in which the demonstration is held, this can be any scene.

Materials: red, White and Yellow
Since you can combine meshes having multiple materials, these three are used in the example to demonstrate this.


Step #2 An example parent object consisting out of many child game objects. This Game Object is an initial design of the primary enemy in the game.


Undertake the following sub steps to make an object ready for the Combine Meshes functionality.
a. Create an empty Game Object in the scene which acts as the parent object.
b. Add a Mesh Filter component to it.
c. Add a Mesh Renderer component to it.
d. Add many children having cubes of different shapes, until the larger shape matches your design.

Step #3 When undertaken the sub steps of step #2 it should result in a scene hierarchy similar to the one shown above.

Having a parent object (in this case called "Sketch_Cubicon_Single_Material") with lots of smaller child objects attached to it. What is going to happen is that all the child objects will be merged into a single mesh which will be put into the Mesh Filter component of the parent object ("Sketch_Cubicon_Single_Material").

Step #4 In the image above a random Child object is shown with how its components is configured. Important is that they have a Mesh Filter and Mesh Renderer component.

Note: you can use all kinds of shapes/meshes here.

Step #5 Now that there is a scene with a parent object containing many child objects it’s time to add the CombineMesh script (see CombineMesh.cs at the bottom of this article) to the parent object. Cause this script will combine all the child object meshes into a single one and place it in the parent object.
The image above shows how the parent object ("Sketch_Cubicon_Single_Material") is configured.
Here the component CombineMesh script is add to the parent game object.
Resulting in having a parent game object containing a Transform, CombineMesh, Mesh Filter and Mesh renderer component. The object above is an example of this configuration.

Note: the CombineMesh properties are set so the script will optimize for making use of a single material. The multiple material settings are ignored in this case.
Steps #6 and #7 will show how the configuration of the CombineMesh script can be changed to make use of multiple materials.

Step #6 In the previous step there was an object using only one material, in the image above there is an example of an object using multiple materials. Again create a parent object, add many children to it. To these children various material can be add, in this case the red and yellow materials are used.
Note: These materials are add to the Mesh Renderer component of the child objects (see property Game Object > Mesh Renderer > Materials > Element 0).

Step #7 The above image illustrates the result of  what has been done in step #6. Resulting in a parent object having many child objects with different materials.

The difference between optimizing for single or multiple materials can be viewed by comparing the image from step #5 to this image of step #7.

To optimize for multiple materials create the following configuration:
a. Enable/set the flag “B Multiple Materials” in CombineMesh so the script takes multiple materials into account.
b. Add all the different materials used in the children to the Shared Material Multiple list.
Note: The following is optional, but if you would like to use the final mesh for collision then make sure to add a Mesh Collider to the parent object and turn the “B Use Mesh Collider” flag in the Combine Mesh script on.

The next steps (8-14) show an overview of the CombineMesh script, but are not necessary to get it to work.

Step #8 The next images show the layout of the CombineMesh script. In this first image we see the configurable member variables.

Step #9 Next is the Awake method which check if a parent root object is set and if the script should run or not.

Step #10 When the script should run it checks whether to run it for a single material or multiple.
*Depends on configuration by member variables.

Step #11 Here is an overview of the method called when Combining meshes for a single material.

Step #12 To combine meshes based on multiple materials it takes a bit more code, that's why the next three images show this in all details.

Step #13 Search through the child objects attached to the root for Mesh Filters and combine them based on a specific material. This way we first create submeshes based on material type. This will result in multiple combine mesh instances based on different materials.

Step #14 Finally combine the different meshes into a single mesh and assign a list of unique materials so it know what the different materials that were used are.

 

Video

If besides the series of images above you would like to see a video demonstration of the Unity mesh combine in action then head over to:
Video here

Alternatives

Other ideas to achieve something similar and even better performance might be:
- Using Voxel (engine) technology
- Create separate shapes using a 3D tool.
- Generating Meshes from triangles, even possibly using procedural content generation (PCG)
- Most likely more ...

 

Hope this overview provides a nice and quick insight into how the enemies were made for Anti Air and hope you get a chance to experience the destruction of Cubicons yourself 🙂

Useful links

https://docs.unity3d.com/ScriptReference/Mesh.CombineMeshes.html
https://docs.unity3d.com/ScriptReference/CombineInstance.html
https://docs.unity3d.com/ScriptReference/MeshFilter.html
https://docs.unity3d.com/ScriptReference/Mesh.html

 

 

Class CombineMesh.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Anti Air - The Cubicon Conjunction
// http://www.postbeta.com/antiair
// INFO:
// Add this class to your object in the scene of which you would like the different meshes to be combined in to a single mesh.
// Reference material:
// https://docs.unity3d.com/ScriptReference/Mesh.CombineMeshes.html
// https://docs.unity3d.com/ScriptReference/CombineInstance.html
// https://docs.unity3d.com/ScriptReference/MeshFilter.html
// https://docs.unity3d.com/ScriptReference/Mesh.html
public class CombineMesh : MonoBehaviour
{
public GameObject staticRoot; // If no specific static root has been set use the game objects it is attached to.
public Material sharedMaterialSingle; // The material used on all shapes in the mesh/combined mesh
public bool bMultipleMaterials = true; // Can be set to false to optimize even further, when there is only one material.
public List sharedMaterialMultiple; // Support for mutiple materials(list of materials)
public bool bUseMeshCollider = false; // Set this to true if you would like to have the newly generated mesh also be used as the (Mesh) collider.
public bool bOn = true; // set to false if you dont want to obejct to execure the mesh combine functionality
private bool bMeshCombined = false; // set to true when the mesh has been combined.

void Awake()
{
// if no specific static root has been set use the game objects it is attached to.
if (staticRoot == null)
{
staticRoot = this.transform.gameObject;
}

if (bOn && staticRoot)
{
// run the correct mesh combine script.
InitiateCombineMesh(sharedMaterialSingle, sharedMaterialMultiple);

// and remove all children parts used for mesh combining from staticPart
foreach (Transform child in staticRoot.transform)
{
GameObject.Destroy(child.gameObject);
}

//whether the mesh should also be used as a meshCollider shape in the collision component.
if (bUseMeshCollider)
{
staticRoot.GetComponent().sharedMesh = staticRoot.GetComponent().mesh;
}
}
}

void InitiateCombineMesh(Material materialSingle, List materialMultiple)
{
// Create one mesh based on all the static parts
if (bMultipleMaterials)
{
CombineMeshesBasedOnMultipleMaterials(staticRoot, materialMultiple);
}
else
{
CombineMeshToOnePartOneMaterial(staticRoot, materialSingle);
}

bMeshCombined = true;
}

public static void CombineMeshToOnePartOneMaterial(GameObject objectWithMeshes, Material objectMaterial)
{
// Move Object to zero position in world for les conflict with matrix calculation mistakes
Vector3 position_original = objectWithMeshes.transform.position;
objectWithMeshes.transform.position = Vector3.zero;
Quaternion rotation_original = objectWithMeshes.transform.rotation;
objectWithMeshes.transform.rotation = Quaternion.identity;

// #1: Collect all meshFilters inside the staticRoot/objectWithMeshes its children, these are the ones we are going to combine.
MeshFilter[] meshFilterWorld = objectWithMeshes.GetComponentsInChildren(false);

// #2: Next create a list of CombineInstance's and step through each MeshFilter to add it to the CombineInstance list
CombineInstance[] meshCombineInstance = new CombineInstance[meshFilterWorld.Length];
for (int i = 0; i < meshFilterWorld.Length; i++) { meshCombineInstance[i].mesh = meshFilterWorld[i].sharedMesh; meshCombineInstance[i].transform = meshFilterWorld[i].transform.localToWorldMatrix; //meshFilterWorld[i].gameObject.SetActive(false); } // #3: use the CombineInstance list from #2, And combine them into one final mesh and set the unique material. // Note: When using a single material here we have the advantage to use CombineMeshes->mergeSubMeshes for Unity performance benefits.
objectWithMeshes.transform.GetComponent().sharedMaterial = objectMaterial;
objectWithMeshes.transform.GetComponent().mesh = new Mesh();
objectWithMeshes.transform.GetComponent().mesh.CombineMeshes(meshCombineInstance, true, true); // When only using 1 material set to true for optimization reasons

// Move object back to original position in world
objectWithMeshes.transform.position = position_original;
objectWithMeshes.transform.rotation = rotation_original;
//objectWithMeshes.transform.gameObject.SetActive(true);
}

public static void CombineMeshesBasedOnMultipleMaterials(GameObject objectWithMeshes, List objectMaterials)
{
// Move Object to zero position in world for les conflict with matrix calculation mistakes
Vector3 position_original = objectWithMeshes.transform.position;
objectWithMeshes.transform.position = Vector3.zero;

// #1: Collect all meshFilters inside the staticRoot/objectWithMeshes its children,
// these are the ones we are going to combine.
MeshFilter[] meshFilterWorld = objectWithMeshes.GetComponentsInChildren(false);
List submeshes = new List();
List usedMaterials = new List();

// #2: Step through the materials used on the object, these were set manually in the list sharedMaterialMultiple
// NOTE: these can also be collected dynamically
foreach (Material mat in objectMaterials)
{
// #3: Next create a list of CombineInstance's and step through each MeshFilter to match it on material type
// and add it to the CombineInstance of the same material.
// But also store each unique material used in the usedMaterial list.
List combiners = new List();
foreach (MeshFilter m in meshFilterWorld)
{
MeshRenderer renderer = m.GetComponent();
Material[] localMaterials = renderer.sharedMaterials;
for (int indexMatLocal = 0; indexMatLocal < localMaterials.Length; indexMatLocal++)
{
if (localMaterials[indexMatLocal] == mat)
{
// add material to optimized list if it doesn have it yet
if (!usedMaterials.Contains(mat))
{
usedMaterials.Add(mat);
}
CombineInstance ci = new CombineInstance();
ci.mesh = m.sharedMesh;
ci.subMeshIndex = indexMatLocal;
ci.transform = m.transform.localToWorldMatrix; // copy its transform such as position/rotation from the original as world location.
combiners.Add(ci);
}
}
}

// #4: Now call the highly anticipated CombineMeshes method on the new Mesh object for the current Material in the list.
// This way we have one Mesh per Material
Mesh mesh = new Mesh();
mesh.CombineMeshes(combiners.ToArray(), true); // NOTE: if true it will fail when there are too many vertices it will create multiple submeshes, would require optmizing then!
submeshes.Add(mesh);
}

// #5: Step through all the submeshes generated per material from the previous steps #1 - #4,
// And collect them into a new CombineInstance list.
List combinersFinal = new List();
foreach (Mesh mesh in submeshes)
{
CombineInstance ci = new CombineInstance();
ci.mesh = mesh;
ci.subMeshIndex = 0;
ci.transform = Matrix4x4.identity; // use default identity matrix to give it a default transfrom
combinersFinal.Add(ci);
}

// #6: use the CombineInstance list from #5, And combine them into one final mesh and set the unique used materials.
Mesh meshFinal = new Mesh();
meshFinal.CombineMeshes(combinersFinal.ToArray(), false); // NOTE: When true and merging submeshes it will only use a single material
objectWithMeshes.GetComponent().sharedMesh = meshFinal;
objectWithMeshes.GetComponent().sharedMaterials = usedMaterials.ToArray();

// Move object back to original position in world
objectWithMeshes.transform.position = position_original;

Debug.Log("Mesh material count is: " + submeshes.Count);
}

}

How many Cubicons will you take down?

Steam link: Anti_Air

 

 

 

Join us!


How about writing your own piece for IndieWatch?


Mike

Game developer since 2007, having developed for titles on Xbox, PC, Mobile and VR.

Leave a Reply

Your email address will not be published.

Back to top button