Skip to content

Instantly share code, notes, and snippets.

@b3x206
Last active October 21, 2025 17:25
Show Gist options
  • Select an option

  • Save b3x206/ccb501fc54c2d2bd50ad0e1cb376d360 to your computer and use it in GitHub Desktop.

Select an option

Save b3x206/ccb501fc54c2d2bd50ad0e1cb376d360 to your computer and use it in GitHub Desktop.
A mesh based progress/health bar, for Unity games
using UnityEngine;
using System;
using System.Collections.Generic;
// This is free and unencumbered software released into the public domain.
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
// (see https://unlicense.org)
/// <summary>
/// Mesh based progress bar that can be also used as an health bar.
/// <br>It creates a mesh with 2 submeshes to have a foreground[0] and a background[1]</br>
/// <br>The UV's of the ProgressBar are mapped between 0-1, per material. This means that it always fits the texture inside.</br>
/// </summary>
/// <example>
/// <![CDATA[
/// // Setup 'MeshProgressBar' by adding it into an object and then assigning materials to the 0 and 1 index of the 'MeshRenderer'.
/// using UnityEngine;
///
/// public class SampleScript : MonoBehaviour
/// {
/// public int startingHealth = 100;
/// public MeshProgressBar healthBar;
/// public int Health { get; private set; }
///
/// private void Start()
/// {
/// Health = startingHealth;
/// }
///
/// private void Update()
/// {
/// if (Input.GetKeyDown(KeyCode.Space))
/// {
/// Damage(Random.Range(5, 15));
/// }
/// }
///
/// public void Damage(int damage)
/// {
/// Health -= damage;
///
/// // 'Progress' is clamped between 0-1, but is a floating point number.
/// // Casting to (float) is required to not do integer division. (which floors the number)
/// healthBar.Progress = (float)Health / startingHealth;
/// }
/// }
/// ]]>
/// </example>
[ExecuteAlways, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class MeshProgressBar : MonoBehaviour
{
// taken from BXFW to make this script standalone
public enum TransformAxis
{
None = 0,
XAxis = 1 << 0,
YAxis = 1 << 1,
ZAxis = 1 << 2,
XYZAxis = XAxis | YAxis | ZAxis,
}
/// <summary>
/// Generates a quad for the bar meshes.
/// <br>The given 1, 3, 4 and 6'th vertex of the mesh control the center divider.</br>
/// <br>The foreground submesh is the 0'th index and the background submesh is in the 1'st index.</br>
/// </summary>
protected static Mesh GenerateSplitQuad()
{
// Don't combine the center into a single vertex
// That combination will cause the UV's being split weirdly
Mesh mesh = new Mesh();
float width = 1f;
float height = 1f;
float halfWidth = width / 2f;
float halfHeight = height / 2f;
// 2 - 3, 6 - 7
// | | | |
// 0 - 1, 4 - 5
Vector3[] vertices = new Vector3[8]
{
// left
new Vector3(-halfWidth, -halfHeight, 0f),
new Vector3(0f, -halfHeight, 0f),
new Vector3(-halfWidth, height - halfHeight, 0f),
new Vector3(0f, height - halfHeight, 0f),
// right
new Vector3(0f, -halfHeight, 0f),
new Vector3(width - halfWidth, -halfHeight, 0f),
new Vector3(0f, height - halfHeight, 0f),
new Vector3(width - halfWidth, height - halfHeight, 0f),
};
mesh.vertices = vertices;
mesh.subMeshCount = 2;
mesh.SetTriangles(new int[6]
{
// lower left triangle
0, 2, 1,
// upper right triangle
2, 3, 1
}, 0);
mesh.SetTriangles(new int[6]
{
// lower left
4, 6, 5,
// upper right
6, 7, 5
}, 1);
Vector3[] normals = new Vector3[8]
{
-Vector3.forward,
-Vector3.forward,
-Vector3.forward,
-Vector3.forward,
-Vector3.forward,
-Vector3.forward,
-Vector3.forward,
-Vector3.forward
};
mesh.normals = normals;
Vector2[] uv = new Vector2[8]
{
// left
new Vector2(0f, 0f),
new Vector2(1f, 0f),
new Vector2(0f, 1f),
new Vector2(1f, 1f),
// right
new Vector2(0f, 0f),
new Vector2(1f, 0f),
new Vector2(0f, 1f),
new Vector2(1f, 1f)
};
mesh.uv = uv;
mesh.name = "PBarMesh";
return mesh;
}
private MeshFilter m_MeshFilter;
/// <summary>
/// Mesh filter attached to this progress bar.
/// </summary>
public MeshFilter MeshFilter
{
get
{
if (m_MeshFilter == null)
{
m_MeshFilter = GetComponent<MeshFilter>();
}
return m_MeshFilter;
}
}
private Renderer m_Renderer;
/// <summary>
/// Renderer attached to this progress bar.
/// </summary>
public Renderer Renderer
{
get
{
if (m_Renderer == null)
{
if (!TryGetComponent(out m_Renderer))
{
return null;
}
}
CheckRendererMaterialsSize(m_Renderer);
return m_Renderer;
}
}
private void CheckRendererMaterialsSize(Renderer r)
{
bool hasAtleastTwoMaterials;
bool hasAtleastOneMaterial;
#if UNITY_EDITOR
// 'sharedMaterials'
if (!Application.isPlaying)
{
hasAtleastTwoMaterials = r.sharedMaterials.Length >= 2;
hasAtleastOneMaterial = hasAtleastTwoMaterials || r.sharedMaterials.Length >= 1;
if (r.sharedMaterials.Length != 2)
{
Material mat1 = null, mat2 = null;
if (hasAtleastTwoMaterials)
{
mat1 = r.sharedMaterials[0];
mat2 = r.sharedMaterials[1];
}
else if (hasAtleastOneMaterial)
{
mat1 = r.sharedMaterials[0];
}
// nullify the 'r.sharedMaterials' first
// to avoid damaging the materials itself
// This is done because unity's Renderer material management is footgun
for (int i = 2; i < r.sharedMaterials.Length; i++)
{
r.sharedMaterials[i] = null;
}
// finally set the shared materials array after no materials were verified to be not damagable irreversibily
// (it is irreversible if the user wasn't using version control)
r.sharedMaterials = new Material[2]
{
mat1,
mat2
};
}
return;
}
#endif
hasAtleastTwoMaterials = r.materials.Length >= 2;
hasAtleastOneMaterial = hasAtleastTwoMaterials || r.materials.Length >= 1;
// This one can be done as is, the materials will be instanced and be not damaged
if (r.materials.Length != 2)
{
r.materials = new Material[2]
{
hasAtleastOneMaterial ? r.materials[0] : null,
hasAtleastTwoMaterials ? r.materials[1] : null
};
}
}
/// <summary>
/// The progress bar mesh.
/// <br>Depending on the current runtime state, this is the <see cref="MeshFilter.sharedMesh"/>
/// or the <see cref="MeshFilter.mesh"/> with the quad always assigned.</br>
/// </summary>
public Mesh ProgressMesh
{
get
{
if (MeshFilter.sharedMesh == null)
{
MeshFilter.sharedMesh = GenerateSplitQuad();
}
#if UNITY_EDITOR
return Application.isPlaying ? MeshFilter.mesh : MeshFilter.sharedMesh;
#else
return MeshFilter.mesh;
#endif
}
}
private Camera m_BillboardCamera;
/// <summary>
/// The camera to billboard towards.
/// </summary>
public Camera BillboardCamera
{
get
{
if (m_BillboardCamera == null)
{
m_BillboardCamera = Camera.main;
}
return m_BillboardCamera;
}
}
/// <summary>
/// Foreground material applied to this progress bar.
/// <br>Depending on which state of the runtime this material is accessed on, it can be the sharedMaterial or the material.</br>
/// </summary>
public Material ForegroundMaterial
{
get
{
#if UNITY_EDITOR
return Application.isPlaying ? Renderer.materials[0] : Renderer.sharedMaterials[0];
#else
return Renderer.materials[0];
#endif
}
}
/// <summary>
/// Background material applied to this progress bar.
/// <br>Depending on which state of the runtime this material is accessed on, it can be the sharedMaterial or the material.</br>
/// </summary>
public Material BackgroundMaterial
{
get
{
#if UNITY_EDITOR
return Application.isPlaying ? Renderer.materials[1] : Renderer.sharedMaterials[1];
#else
return Renderer.materials[1];
#endif
}
}
private readonly List<Vector3> m_VertsCache = new List<Vector3>(8);
protected void SetProgressVertices(float progress)
{
// The given vertices, 1, 3, 4 and 6 has to be moved.
ProgressMesh.GetVertices(m_VertsCache);
float initialLeftX = m_VertsCache[0].x;
m_VertsCache[1] = new Vector3(initialLeftX + progress, m_VertsCache[1].y, m_VertsCache[1].z);
m_VertsCache[3] = new Vector3(initialLeftX + progress, m_VertsCache[3].y, m_VertsCache[3].z);
m_VertsCache[4] = new Vector3(initialLeftX + progress, m_VertsCache[4].y, m_VertsCache[4].z);
m_VertsCache[6] = new Vector3(initialLeftX + progress, m_VertsCache[6].y, m_VertsCache[6].z);
ProgressMesh.SetVertices(m_VertsCache);
}
/// <summary>
/// Translates the mesh's vertices / rendering matrix to always face towards camera.
/// </summary>
protected void UpdateBillboard()
{
Camera targetCamera;
#if UNITY_EDITOR
// do nothing in prefab scene, as prefab scenes when changed, it needs to be set dirty in 73 different ways
if (UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage() != null)
{
return;
}
if (!Application.isPlaying)
{
targetCamera = UnityEditor.SceneView.lastActiveSceneView.camera;
}
else
#endif
{
targetCamera = BillboardCamera;
}
if (targetCamera == null)
{
return;
}
// Since the mesh vertex positions are not transformed, we can always rotate it
// However this will mutate the mostly immutable mesh, which is bad.
// So update this transform as unity renderer is a black box and using Graphics.DrawMesh usually doesn't work as intended.
// --
// Note : we could serialize the 'm_VertsCache' and transform the mesh directly, but this will do for now.
// Even though it is changing a serialized value, we are going to change a serialized value anyways (the sharedMesh, that is)
Vector3 targetRotationEuler = Quaternion.LookRotation(transform.position - targetCamera.transform.position, Vector3.forward).eulerAngles;
if ((billboardAxis & TransformAxis.XAxis) != TransformAxis.XAxis)
{
targetRotationEuler.x = 0f;
}
if ((billboardAxis & TransformAxis.YAxis) != TransformAxis.YAxis)
{
targetRotationEuler.y = 0f;
}
if ((billboardAxis & TransformAxis.ZAxis) != TransformAxis.ZAxis)
{
targetRotationEuler.z = 0f;
}
transform.rotation = Quaternion.Euler(targetRotationEuler);
}
[SerializeField, Range(0f, 1f)] private float m_Progress = 0f;
/// <summary>
/// The progress elapsed for this progress bar.
/// <br>This value goes between 0-&gt;1. Setting this value updates the vertices of the mesh to it's progressed state.</br>
/// </summary>
public float Progress
{
get
{
return m_Progress;
}
set
{
m_Progress = Mathf.Clamp01(value);
SetProgressVertices(m_Progress);
}
}
/// <summary>
/// If this is set to any value other than <see cref="TransformAxis.None"/>,
/// the meshes are transformed to look at the given primary scene camera.
/// </summary>
public TransformAxis billboardAxis = TransformAxis.YAxis;
protected virtual void Awake()
{
// Prefab instances delete the mesh because unity serializer
if (MeshFilter.sharedMesh == null)
{
MeshFilter.sharedMesh = GenerateSplitQuad();
}
}
protected virtual void Update()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
// Doing this on the 'OnValidate' outputs 190237813 warnings to the console
// because unity wants to do that.
SetProgressVertices(m_Progress);
}
#endif
// Do billboarding
UpdateBillboard();
}
protected virtual void Reset()
{
MeshFilter.sharedMesh = null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment