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)
///
/// Mesh based progress bar that can be also used as an health bar.
///
It creates a mesh with 2 submeshes to have a foreground[0] and a background[1]
///
The UV's of the ProgressBar are mapped between 0-1, per material. This means that it always fits the texture inside.
///
///
///
///
[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,
}
///
/// Generates a quad for the bar meshes.
///
The given 1, 3, 4 and 6'th vertex of the mesh control the center divider.
///
The foreground submesh is the 0'th index and the background submesh is in the 1'st index.
///
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;
///
/// Mesh filter attached to this progress bar.
///
public MeshFilter MeshFilter
{
get
{
if (m_MeshFilter == null)
{
m_MeshFilter = GetComponent();
}
return m_MeshFilter;
}
}
private Renderer m_Renderer;
///
/// Renderer attached to this progress bar.
///
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
};
}
}
///
/// The progress bar mesh.
///
Depending on the current runtime state, this is the
/// or the with the quad always assigned.
///
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;
///
/// The camera to billboard towards.
///
public Camera BillboardCamera
{
get
{
if (m_BillboardCamera == null)
{
m_BillboardCamera = Camera.main;
}
return m_BillboardCamera;
}
}
///
/// Foreground material applied to this progress bar.
///
Depending on which state of the runtime this material is accessed on, it can be the sharedMaterial or the material.
///
public Material ForegroundMaterial
{
get
{
#if UNITY_EDITOR
return Application.isPlaying ? Renderer.materials[0] : Renderer.sharedMaterials[0];
#else
return Renderer.materials[0];
#endif
}
}
///
/// Background material applied to this progress bar.
///
Depending on which state of the runtime this material is accessed on, it can be the sharedMaterial or the material.
///
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 m_VertsCache = new List(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);
}
///
/// Translates the mesh's vertices / rendering matrix to always face towards camera.
///
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;
///
/// The progress elapsed for this progress bar.
///
This value goes between 0->1. Setting this value updates the vertices of the mesh to it's progressed state.
///
public float Progress
{
get
{
return m_Progress;
}
set
{
m_Progress = Mathf.Clamp01(value);
SetProgressVertices(m_Progress);
}
}
///
/// If this is set to any value other than ,
/// the meshes are transformed to look at the given primary scene camera.
///
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;
}
}