/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Objects.ItemAssist { using Opsive.Shared.Game; using System.Collections.Generic; using UnityEngine; /// /// Builds a mesh which can show a trail following a melee weapon. This is typically used when the melee weapon is slashed. /// public class Trail : MonoBehaviour { [Tooltip("The minimum distance between the position of the last slice and the current position.")] [SerializeField] protected float m_MinDistance = 0.01f; [Tooltip("The vertical length of the trail.")] [SerializeField] protected float m_Length = 1f; [Tooltip("The maximum number of slices within the trail. A larger value will cause the trail to be longer.")] [SerializeField] protected int m_MaxSliceCount = 50; [Tooltip("The smoothing value of the curve. A larger value will have a smoother curve compared to a smaller value.")] [SerializeField] protected int m_CurveSmoothness = 10; [Tooltip("The steepness of the curve. A value closer to 1 will increase the steepness.")] [Range(0, 1)] [SerializeField] protected float m_CurveSteepness = 0.5f; [Tooltip("The start color of the trail, near the melee weapon object.")] [SerializeField] protected Color m_StartColor = Color.white; [Tooltip("The end color of the trail.")] [SerializeField] protected Color m_EndColor = Color.white; [Tooltip("The amount of time the slice should be visible.")] [SerializeField] protected float m_VisibilityTime = 1; private GameObject m_GameObject; private Transform m_Transform; private Mesh m_Mesh; private float m_MinDistanceSquared; private TrailSlice[] m_TrailSlices = new TrailSlice[4]; private int m_TrailSlicesIndex = -1; private int m_TrailSlicesCount; private TrailSlice[] m_SmoothedTrailSlices; private int m_SmoothedTrailSlicesIndex = -1; private int m_SmoothedTrailSlicesCount; private int m_SmoothedTrailSlicesPrevCount; private int m_SmoothedTrailSlicesPrevIndex; private List m_Vertices; private List m_UVs; private List m_Colors; private List m_Triangles; private bool m_GenerateSlices; /// /// A small container class for each object that represents a slice from the melee trail. /// private struct TrailSlice { private Vector3 m_Point; private Vector3 m_Up; private float m_Time; public Vector3 Point { get { return m_Point; } } public Vector3 Up { get { return m_Up; } } public float Time { get { return m_Time; } } /// /// Initializes the slice. /// /// The position of the slice. /// The up direction of the slice. public void Initialize(Vector3 point, Vector3 up) { m_Point = point; m_Up = up; m_Time = UnityEngine.Time.time; } } /// /// Initializes the trail. /// private void Awake() { m_GameObject = gameObject; m_Transform = transform; var meshFilter = GetComponent(); if (meshFilter == null) { Debug.LogError("Error: Unable to find the MeshFilter component. Enssure the Trail object has been created through the Object Manager."); return; } m_Mesh = meshFilter.mesh; m_MinDistanceSquared = m_MinDistance * m_MinDistance; var count = m_MaxSliceCount * m_CurveSmoothness; m_SmoothedTrailSlices = new TrailSlice[count]; m_Vertices = new List(count * 2); m_UVs = new List(count * 2); m_Colors = new List(count * 2); m_Triangles = new List((count - 1) * 6); // 3 indices per triangle, 2 triangles per slice. } /// /// Start to generate the trail slices. /// private void OnEnable() { m_GenerateSlices = true; } /// /// Samples the position of the melee object. /// private void FixedUpdate() { if (m_GenerateSlices) { SampleTrail(); } } /// /// Displays the trail. /// private void LateUpdate() { RemoveOldSlices(); BuildMesh(); } /// /// Stores a new sample of the trail slice. /// private void SampleTrail() { // Add a new slice if the last sample position is too far away. if (m_TrailSlicesCount == 0 || (m_TrailSlices[m_TrailSlicesIndex].Point - m_Transform.position).sqrMagnitude > m_MinDistanceSquared) { // Revert the trail slice index and smoothed index/count values so the extra slice can be removed. A more accurate slice value will replace the prediction. if (m_TrailSlicesCount > 3) { m_TrailSlicesIndex = m_TrailSlicesIndex - 1; if (m_TrailSlicesIndex < 0) { m_TrailSlicesIndex += m_TrailSlicesCount; } m_SmoothedTrailSlicesIndex = m_SmoothedTrailSlicesPrevIndex; m_SmoothedTrailSlicesCount = m_SmoothedTrailSlicesPrevCount; } // Add the new slice at the current position. AddTrailSlice(m_Transform.position, m_Transform.up); // A catmull-rom curve smooths the middle two verticies rather then all four vertices. Add one more slice near the beginning of the trail so the start of the // curve will intersect with the melee object. if (m_TrailSlicesCount > 3) { var prevIndex = m_TrailSlicesIndex - 1; if (prevIndex < 0) { prevIndex = m_TrailSlicesCount - 1; } var prevTrailSlice = m_TrailSlices[prevIndex]; var trailSlice = m_TrailSlices[m_TrailSlicesIndex]; // Remember the previous smoothed values so the extra slice can be removed. m_SmoothedTrailSlicesPrevIndex = m_SmoothedTrailSlicesIndex; m_SmoothedTrailSlicesPrevCount = m_SmoothedTrailSlicesCount; // The new slice should be in the previous to the current slice position. This value probably won't be correct the next frame unless // the object is moving in a linear path but it is a good prediction. AddTrailSlice(trailSlice.Point + (trailSlice.Point - prevTrailSlice.Point).normalized, (trailSlice.Up + prevTrailSlice.Up) / 2); } } } /// /// Adds the point and up vertex to the trail slices array. The values will also be smoothed. /// /// The point of the slice. /// The up direction of the slice. private void AddTrailSlice(Vector3 point, Vector3 up) { // Catmull-rom curves do not like repeated points. if (m_TrailSlicesCount > 0 && m_TrailSlices[m_TrailSlicesIndex].Point == point) { return; } m_TrailSlicesIndex = (m_TrailSlicesIndex + 1) % m_TrailSlices.Length; m_TrailSlices[m_TrailSlicesIndex].Initialize(point, up); if (m_TrailSlicesIndex + 1 > m_TrailSlicesCount) { m_TrailSlicesCount++; } SmoothTrailSlice(); } /// /// Smooths the trail slices with a catmull-rom curve. /// private void SmoothTrailSlice() { // A catmull-rom curve requires at least four points. if (m_TrailSlicesCount < 4) { return; } // A fixed size array is used to store the vertex values. The starting index may not be at the beginning of the array. var startIndex = m_TrailSlicesIndex - m_TrailSlicesCount + 1; if (startIndex < 0) { startIndex = m_TrailSlices.Length + startIndex; } // Determine a smoothed value for both the point and up vertex. var p0 = m_TrailSlices[startIndex].Point; var p1 = m_TrailSlices[(startIndex + 1) % 4].Point; var p2 = m_TrailSlices[(startIndex + 2) % 4].Point; var p3 = m_TrailSlices[(startIndex + 3) % 4].Point; var u0 = m_TrailSlices[startIndex].Up; var u1 = m_TrailSlices[(startIndex + 1) % 4].Up; var u2 = m_TrailSlices[(startIndex + 2) % 4].Up; var u3 = m_TrailSlices[(startIndex + 3) % 4].Up; var t1 = CentripetralCatmullRomTime(0, p0, p1); var t2 = CentripetralCatmullRomTime(t1, p1, p2); var t3 = CentripetralCatmullRomTime(t2, p2, p3); // Iterate based on the number of sample values. var iterAmount = ((t2 - t1) / m_CurveSmoothness); for (float t = t1; t < t2; t += iterAmount) { var point = CentripetralCatmullRomValue(p0, p1, p2, p3, 0, t1, t2, t3, t); var up = CentripetralCatmullRomValue(u0, u1, u2, u3, 0, t1, t2, t3, t); // The value has been determined. Add it to the smoothed array. m_SmoothedTrailSlicesIndex = (m_SmoothedTrailSlicesIndex + 1) % m_SmoothedTrailSlices.Length; m_SmoothedTrailSlices[m_SmoothedTrailSlicesIndex].Initialize(point, up); if (m_SmoothedTrailSlicesIndex + 1 > m_SmoothedTrailSlicesCount) { m_SmoothedTrailSlicesCount++; } } } /// /// Returns the time of the centripetral catmull-rom curve, defined in https://en.wikipedia.org/wiki/Centripetal_Catmull–Rom_spline. /// /// The sample time. /// The first vertex. /// The second vertex. /// The time of the centripetral catmull-rom curve. private float CentripetralCatmullRomTime(float t, Vector3 v0, Vector3 v1) { var a = Mathf.Pow((v1.x - v0.x), 2f) + Mathf.Pow((v1.y - v0.y), 2f) + Mathf.Pow((v1.z - v0.z), 2f); var b = Mathf.Pow(a, 0.5f); var c = Mathf.Pow(b, m_CurveSteepness); return c + t; } /// /// Returns the vertex of the centripetral catmull-rom curve, defined in https://en.wikipedia.org/wiki/Centripetal_Catmull–Rom_spline. /// /// The vertex of the centripetral catmull-rom curve. private Vector3 CentripetralCatmullRomValue(Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, float t0, float t1, float t2, float t3, float t) { var a1 = (t1 - t) / (t1 - t0) * v0 + (t - t0) / (t1 - t0) * v1; var a2 = (t2 - t) / (t2 - t1) * v1 + (t - t1) / (t2 - t1) * v2; var a3 = (t3 - t) / (t3 - t2) * v2 + (t - t2) / (t3 - t2) * v3; var b1 = (t2 - t) / (t2 - t0) * a1 + (t - t0) / (t2 - t0) * a2; var b2 = (t3 - t) / (t3 - t1) * a2 + (t - t1) / (t3 - t1) * a3; return (t2 - t) / (t2 - t1) * b1 + (t - t1) / (t2 - t1) * b2; } /// /// Removes any slices which have existed for more than the visible time. /// private void RemoveOldSlices() { if (m_TrailSlicesCount == 0) { if (!m_GenerateSlices) { if (ObjectPool.InstantiatedWithPool(m_GameObject)) { ObjectPool.Destroy(m_GameObject); } else { m_GameObject.SetActive(false); } } return; } var startIndex = m_TrailSlicesIndex - m_TrailSlicesCount + 1; if (startIndex < 0) { startIndex = m_TrailSlices.Length + startIndex; } var count = m_TrailSlicesCount; for (int i = 0; i < count; ++i) { var trailSlice = m_TrailSlices[(startIndex + i) % m_TrailSlices.Length]; if (trailSlice.Time + m_VisibilityTime > Time.time) { break; } // The slice has existed for more than the visiblity time - remove it by decreasing the count. m_TrailSlicesCount--; } if (m_SmoothedTrailSlicesCount == 0) { return; } startIndex = m_SmoothedTrailSlicesIndex - m_SmoothedTrailSlicesCount + 1; if (startIndex < 0) { startIndex = m_SmoothedTrailSlices.Length + startIndex; } count = m_SmoothedTrailSlicesCount; for (int i = 0; i < count; ++i) { var trailSlice = m_SmoothedTrailSlices[(startIndex + i) % m_SmoothedTrailSlices.Length]; if (trailSlice.Time + m_VisibilityTime > Time.time) { break; } // The slice has existed for more than the visiblity time - remove it by decreasing the count. m_SmoothedTrailSlicesCount--; m_SmoothedTrailSlicesPrevCount--; } if (m_TrailSlicesCount == 0 && m_SmoothedTrailSlicesCount == 0) { m_GenerateSlices = false; } } /// /// Creates the mesh from the catmull rom verticies. /// private void BuildMesh() { m_Mesh.Clear(); if (m_TrailSlicesCount < 4) { return; } // A fixed size array is used to store the vertex values. The starting index may not be at the beginning of the array. var startIndex = m_SmoothedTrailSlicesIndex - m_SmoothedTrailSlicesCount + 1; if (startIndex < 0) { startIndex = m_SmoothedTrailSlices.Length + startIndex; } m_Vertices.Clear(); m_UVs.Clear(); m_Colors.Clear(); m_Triangles.Clear(); for (int i = 0; i < m_SmoothedTrailSlicesCount; ++i) { var trailSlice = m_SmoothedTrailSlices[(startIndex + i) % m_SmoothedTrailSlices.Length]; // The vertex position is the local position of the slice point. This will allow the trail to stay in the same position while the melee object is moving. m_Vertices.Add(m_Transform.InverseTransformPoint(trailSlice.Point)); m_Vertices.Add(m_Transform.InverseTransformPoint(trailSlice.Point + trailSlice.Up * m_Length)); // Set the UV value so a texture can be applied to the material. var u = Mathf.Max(i / (float)(m_SmoothedTrailSlicesCount - 1), 0.01f); m_UVs.Add(new Vector2(u, 0)); m_UVs.Add(new Vector2(u, 1)); // Optionally lerp between the start and end color. m_Colors.Add(Color.Lerp(m_EndColor, m_StartColor, u)); // A clear color will fade the trail at the bottom. m_Colors.Add(Color.clear); // Map the triangle indices to the vertex element. if (i < m_SmoothedTrailSlicesCount - 1) { // First triangle. m_Triangles.Add((i * 2)); m_Triangles.Add((i * 2) + 1); m_Triangles.Add((i * 2) + 2); // Second triangle. m_Triangles.Add((i * 2) + 2); m_Triangles.Add((i * 2) + 1); m_Triangles.Add((i * 2) + 3); } } // Assign the values so the mesh will be displayed on the screen. The list version is used to prevent allocations when the mesh changes size. m_Mesh.SetVertices(m_Vertices); m_Mesh.SetUVs(0, m_UVs); m_Mesh.SetColors(m_Colors); m_Mesh.SetTriangles(m_Triangles, 0); } /// /// Stops generating the trail. /// public void StopGeneration() { m_Transform.parent = null; m_GenerateSlices = false; } } }