// Assets/_Project/Scripts/Visuals/ConeMesh.cs using UnityEngine; namespace TD.Visuals { /// /// Procedurally generates a cone mesh and assigns it to the GameObject's /// . Unity ships no built-in cone primitive, so this /// fills that gap for tower visuals (e.g. the Basic Arrow Tower). /// /// /// Visual only. No networking, no collision — the mesh is purely cosmetic. /// The existing MeshRenderer + material (and any owner/paint tinting via /// TowerInstance.tintedRenderers) keep working unchanged. /// /// Pivot. The cone is centred on the local origin: its base ring sits at /// y = -height/2 and its apex at y = +height/2, matching the vertical /// extents of a unit cube so it sits on a build tile the same way the old cube did. /// /// Edit-time. [ExecuteAlways] rebuilds the mesh in the editor so the /// cone is visible without entering play mode. The generated mesh is flagged /// HideFlags.DontSave so it is never written out as a project asset. /// [ExecuteAlways] [RequireComponent(typeof(MeshFilter))] public class ConeMesh : MonoBehaviour { [Tooltip("Number of radial segments around the base. Higher = smoother circle.")] [Range(3, 64)] [SerializeField] private int sides = 16; [Tooltip("Radius of the cone's base in local units.")] [Min(0.001f)] [SerializeField] private float radius = 0.5f; [Tooltip("Height of the cone from base to apex in local units.")] [Min(0.001f)] [SerializeField] private float height = 1f; private Mesh mesh; private void OnEnable() => Rebuild(); private void OnValidate() { // OnValidate can fire before OnEnable in the editor; guard the component. if (isActiveAndEnabled) Rebuild(); } private void OnDisable() { // Clean up the generated mesh so it doesn't linger when the object is destroyed. if (mesh != null) { if (Application.isPlaying) Destroy(mesh); else DestroyImmediate(mesh); mesh = null; } } private void Rebuild() { var filter = GetComponent(); if (filter == null) return; if (mesh == null) { mesh = new Mesh { name = "ProceduralCone", hideFlags = HideFlags.DontSave }; } BuildCone(mesh); filter.sharedMesh = mesh; } private void BuildCone(Mesh target) { target.Clear(); float halfHeight = height * 0.5f; // Vertex layout: // [0 .. sides-1] base ring // [sides] apex // [sides + 1] base centre (for the bottom cap) int baseRingStart = 0; int apexIndex = sides; int baseCentreIndex = sides + 1; var vertices = new Vector3[sides + 2]; for (int i = 0; i < sides; i++) { float angle = (i / (float)sides) * Mathf.PI * 2f; vertices[baseRingStart + i] = new Vector3( Mathf.Cos(angle) * radius, -halfHeight, Mathf.Sin(angle) * radius); } vertices[apexIndex] = new Vector3(0f, halfHeight, 0f); vertices[baseCentreIndex] = new Vector3(0f, -halfHeight, 0f); // Side faces (base ring -> apex) + base cap (fan from centre). // Wind both so they face outward / downward respectively. var triangles = new int[sides * 3 * 2]; int t = 0; for (int i = 0; i < sides; i++) { int next = (i + 1) % sides; // Side triangle, outward-facing. triangles[t++] = baseRingStart + i; triangles[t++] = apexIndex; triangles[t++] = baseRingStart + next; // Base cap triangle, downward-facing. triangles[t++] = baseCentreIndex; triangles[t++] = baseRingStart + next; triangles[t++] = baseRingStart + i; } target.vertices = vertices; target.triangles = triangles; target.RecalculateNormals(); target.RecalculateBounds(); } } }