Skip to content

Example Portfolio template - Sprint X

Adding a new enemy to the game using the Unity editor and the CombineMesh functionality.

Author: Mike Hofstede

Introduction

In this task, the goal is to implement breakable enemies in the game. This feature not only enhances player engagement by allowing them to blast away parts of enemies but also contributes to performance optimization. By merging multiple meshes into one mesh, the number of draw calls is reduced, resulting in improved rendering performance, particularly in scenes with a large number of objects. For this purpose, a custom script named CombineMesh has been written, which can be placed on a GameObject in the Unity editor, consisting of various meshes.

Implementation/Realisation of enemies

Methods

The methods used for the analysis, design, and implementation of the breakable enemies are described below. Since the emphasis of this proof lies on implementation and realization, method 3 and 4 are particularly detailed.

1. Define: Requirements of the feature

  • Type: Requirements prioritization/analysis
  • Description: Gathering and describing the feature.
  • Source: Game Design documentation

2. Analysis: Research into Unity’s Mesh Combine Feature

  • Type: Literature study/review
  • Description: Analyzing Unity’s Mesh Combine feature to understand how multiple meshes can be merged into one for performance optimization.
  • Source: Unity documentation (Mesh.CombineMeshes)

3. Design: Unity Editor for custom enemies

  • Type: Prototyping design
  • Description: Utilizing the Unity editor as a design tool to create enemies by assembling basic shapes such as cubes, thus avoiding the need for external 3D modeling tools.
  • Source: Unity documentation (Unity Mesh filter)

4. Implementation: Mesh Combining

  • Type: Prototyping implementation / proof of concept (POC)
  • Description: Implementing the Mesh Combine feature in Unity to generate a single mesh from multiple meshes such as cubes, enabling the creation of breakable parts for enemies.

Execution

The implementation of the method involves the following three steps, outlining and addressing the process:

1. Requirements of the feature:

  • Breakable Enemy: The game’s key feature involves enemies with breakable parts that players can blast off. This enables players to demolish enemies by shooting at different parts.

  • Unity Editor as Design Tool: Rather than using external 3D modeling software like Maya or Blender, enemies are assembled within Unity using basic cube shapes. The Unity editor functions as the primary design tool for creating enemies.

  • Grouping Cubes: Players have the option to create larger groups of cubes, allowing for the creation of complex enemy structures and facilitating reuse of grouped cubes.

  • Prefab Utility Class: The Prefab Utility class in Unity is utilized to save enemy objects as prefabs, enabling easy spawning and management within the game.

  • Optimization Techniques:

  • Shared Materials: Utilize Unity’s shared material feature to reduce draw calls by assigning the same material to cubes with similar appearance.
  • Mesh Combiners: Use Unity’s Mesh combine feature to generate single meshes from multiple cubes, reducing the complexity of larger parts and optimizing rendering.
  • Vertices Optimization: Optionally optimize meshes by removing redundant or hidden vertices, enhancing performance by refining mesh geometry. This involves searching for and removing overlapping or unnecessary vertices within the mesh.

2. Research into the Unity’s Mesh Combine Feature:

Exploring Unity’s Combine Meshes feature reveals its potential as a valuable tool for performance optimization within game development. While it may be considered a somewhat hidden gem within Unity, its functionality warrants attention and understanding.

According to Unity’s documentation (https://docs.unity3d.com/ScriptReference/Mesh.CombineMeshes.html), the Combine Meshes feature allows developers to merge multiple Meshes into a single Mesh. This consolidation of Meshes is specifically noted for its performance benefits, indicating its usefulness in optimizing game performance.

An important consideration lies in how this feature interacts with materials. Unity emphasizes the distinction between applying Combine Meshes to a mesh with a single material versus one with multiple materials. The performance gains are notably greater when utilizing Combine Meshes on meshes with a single material.

The method signature for Combine Meshes is defined as follows:

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

Notably, there are three optional parameters within this method, underscoring its flexibility. Essentially, developers need only provide an array of CombineInstances as input. By default, the method assumes a single material on the meshes being combined. However, if multiple materials are present, it’s imperative to set the mergeSubMeshes parameter to false to ensure proper handling.

3. Unity Editor for custom enemies:

Step 1: Assets Overview

Step 1

This is an image showing the required/used assets located in the Unity editor asset overview.

  • Script: Utilize the “CombineScript” (see part 4 of methods and execution) for executing the mesh optimization. This script facilitates the application of Unity’s Mesh.CombineMeshes functionality on a GameObject housing multiple children with individual meshes.
  • Scene: Navigate to the “ExampleMesh” scene within Unity, serving as the environment for the demonstration. This scene can be any scene within the Unity project.
  • Materials: Prepare materials including in this example “red,” “white,” and “yellow.” These materials are employed in the example to showcase the capability of combining meshes with multiple materials.

Step 2: Preparing the Parent Object

Step 2

This is an image showing an enemy configuration using a single material.

  • Begin by creating an empty GameObject within the scene, designated as the parent object for mesh combination.
  • Attach a Mesh Filter component to the parent GameObject. This component will facilitate mesh manipulation and rendering.
  • Attach a Mesh Renderer component to the parent GameObject, enabling visualization of the combined meshes.
  • Populate the parent GameObject with multiple children, each representing individual cubes of various shapes. Continue adding children until the collective shape resembles the desired design of the primary enemy in the game.

Step 3: Scene Hierarchy Setup

Step 3

This is an image showing a setup of a parent gameobject with a subset of children in the scene hierachy.

Upon completion of the sub-steps outlined in Step 2, the scene hierarchy will resemble the structure illustrated above.

The parent object, named “Sketch_Cubicon_Single_Material,” will serve as the container for numerous smaller child objects. These child objects, representing individual cubes of varying shapes, will be attached to the parent object.

The subsequent action involves merging all the child objects into a single mesh. This unified mesh will then be assigned to the Mesh Filter component of the parent object, “Sketch_Cubicon_Single_Material.” Consequently, the parent object will encapsulate the combined mesh, streamlining the rendering process and enhancing performance.

Step 4: Configuring Child Objects

Step 4

This is an image showing the childrens Game Object configuration in the inpsector.

Referencing the image provided, a typical Child object configuration is depicted, showcasing its constituent components. It is crucial to ensure that each Child object possesses both a Mesh Filter and a Mesh Renderer component.

Note: The flexibility exists to employ various shapes or meshes for these Child objects, catering to diverse design requirements and preferences.

Step 5: Adding the CombineMesh Script

Step 5

This is an image showing the CombineMesh script attached to the parent Game Object.

With the scene structured to include a parent object housing numerous child objects, the next step involves integrating the CombineMesh script (refer to CombineMesh.cs at the end of this article) into the parent object. This script orchestrates the amalgamation of all child object meshes into a unified mesh, consolidating it within the parent object.

The image provided illustrates the configuration of the parent object, “Sketch_Cubicon_Single_Material.” Here, the CombineMesh script component is attached to the parent GameObject, alongside existing components such as Transform, Mesh Filter, and Mesh Renderer.

Upon addition, the parent GameObject will contain the following components:

  • Transform
  • CombineMesh
  • Mesh Filter
  • Mesh Renderer

This configuration ensures that the parent object encapsulates the combined mesh, facilitating efficient rendering and performance optimization.

Note: The properties of the CombineMesh script are configured to optimize for a single material, disregarding settings for multiple materials. Subsequent steps (#6 and #7) will elucidate how to modify the CombineMesh script configuration to accommodate multiple materials.

Step 6: Configuring Multiple Materials

Step 6

This is an image showing a enemy configuration using multiple materials.

Following the previous step, where a parent object was configured with a single material, the focus now shifts to an object utilizing multiple materials. The image provided depicts an example of such an object.

Here’s how to proceed:

  1. Create Parent Object: Begin by generating a new parent object within the scene.

  2. Add Child Objects: Populate the parent object with multiple children, as done previously. However, this time, assign various materials to these children. For instance, in the provided example, both red and yellow materials are utilized.

  3. Material Assignment: Ensure that each child object’s Mesh Renderer component is configured to incorporate the respective materials. This entails adding the desired materials to the Materials property of the Mesh Renderer component. Specifically, navigate to the property Game Object > Mesh Renderer > Materials > Element 0 to assign materials to individual child objects.

By incorporating multiple materials into the child objects, the subsequent steps will demonstrate how to adjust the configuration of the CombineMesh script to accommodate and optimize for these multiple materials.

Step 7: Optimizing for Multiple Materials

The resulting configuration from Step 6 showcases a parent object containing multiple child objects, each adorned with different materials. This contrasts with the configuration optimized for a single material as depicted in Step 5.

To optimize for multiple materials, follow these steps:

  1. Enable “B Multiple Materials” Flag: Within the CombineMesh script, activate or set the flag labeled “B Multiple Materials.” This adjustment ensures that the script appropriately accounts for multiple materials present among the child objects.

  2. Add Materials to Shared Material Multiple List: Populate the Shared Material Multiple list within the CombineMesh script with all the different materials utilized among the child objects. This step enables the script to effectively manage and incorporate the various materials during mesh combination.

Optional: - Mesh Collider Configuration: If the final mesh is intended for collision purposes, incorporate a Mesh Collider component into the parent object. Additionally, ensure that the “B Use Mesh Collider” flag within the CombineMesh script is activated. This configuration ensures that collision detection accurately reflects the combined mesh structure.

By implementing these adjustments, the mesh combination process can effectively accommodate multiple materials, enhancing the visual diversity and realism within the parent object while maintaining performance optimization.

4. Mesh Combining:

With the CombineMesh.cs script, both meshes using only one material and meshes using multiple materials can be merged. Below, both examples are presented.

Combine mesh based on one material:

Step 1: Gather all meshFilters This step involves collecting all mesh filters present in the object and its children.

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

Step 2: Create a list of CombineInstances Here, CombineInstance structures are created and filled with mesh data from each MeshFilter.

// 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); 
    }

Step 3: Combine into a single final mesh and set the unique material This step involves merging all collected meshes into one final mesh and applying a material.

// Use the CombineInstance list from #2, And combine them into one final mesh and set the unique material.  
MeshFilter meshFilter = objectWithMeshes.GetComponent<MeshFilter>();  
meshFilter.sharedMesh = new Mesh();  
meshFilter.sharedMesh.CombineMeshes(meshCombineInstance, true, true);  

Step 4: Move object back to its original position in the world After combining, the object is moved back to its original position in the world.

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

Combine meshes based on multiple materials:

Step 1: Gather all meshFilters This step involves collecting all mesh filters present in the object and its children.

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

Step 2: Iterate through the used materials and create lists of CombineInstances In this step, lists of CombineInstances are created for each used material and filled with mesh data from each MeshFilter that utilizes that specific material.

// 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)
{
    // 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<CombineInstance> combiners = new List<CombineInstance>();
    foreach (MeshFilter m in meshFilterWorld)
    {
        MeshRenderer renderer = m.GetComponent<MeshRenderer>();
        Material[] localMaterials = renderer.sharedMaterials;
        for (int indexMatLocal = 0; indexMatLocal < localMaterials.Length; indexMatLocal++)
        {
            if (localMaterials[indexMatLocal] == mat)
            {
                // add material to optimized list if it doesn't 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);
            }
        }
    }
}

Step 3: Combine mesh for each material This step involves combining the meshes for each material into separate submeshes.

// 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);

Step 4: Iterate through all generated submeshes and gather them This step involves iterating through all generated submeshes per material and collecting them into a new list of CombineInstances.

// Step through all the submeshes generated per material from the previous steps #1 – #4,
// And collect them into a new CombineInstance list.
List<CombineInstance> combinersFinal = new List<CombineInstance>();
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);
}

Step 5: Combine all submeshes into one final mesh and set the unique used materials In this step, all submeshes are merged into one final mesh, and the unique used materials are set on the object.

// 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<MeshFilter>().sharedMesh = meshFinal;
objectWithMeshes.GetComponent<MeshFilter>().sharedMaterials = usedMaterials.ToArray();

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

Closing Statement with Alternatives

In conclusion, while the case above has demonstrated effective utilization of combining meshes with a single or multiple materials within our Unity project, there are alternative approaches worth exploring to achieve similar or even better performance.

Some alternative ideas include:

  • Voxel Technology: Consider leveraging voxel engine technology, which offers a unique approach to rendering and manipulating three-dimensional environments. Voxel-based systems can provide efficient rendering and flexibility in creating diverse and dynamic worlds.

  • Custom 3D Modeling: Explore the option of creating separate shapes using traditional 3D modeling tools such as Maya or Blender. This approach allows for precise control over individual components and may result in optimized meshes tailored to specific design requirements.

  • Mesh Generation Techniques: Experiment with generating meshes directly from triangles or implementing procedural content generation (PCG) algorithms. These techniques offer dynamic and scalable solutions for generating complex geometry at runtime, potentially leading to more efficient rendering pipelines.

These alternatives represent just a few of the many possibilities available for optimizing performance and achieving desired visual outcomes in Unity projects. It’s essential to assess project requirements to determine the most suitable approach.

Sources

Unity Documentatie: Mesh.CombineMeshes
Unity Documentatie: CombineInstance
Unity Documentatie: MeshFilter
Unity Documentatie: Mesh


Last update: April 11, 2024