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