using System.Collections; using System.Collections.Generic; using UnityEngine; public class RoadBuilder : MonoBehaviour, IWaitCarPath { public PathManager pathManager; [Header("Road building params")] public bool doBuildRoad = false; public bool doAddBarriers = true; public bool closeLoop = true; public float barrierHeight = 3.0f; public float barrierOffset = 0.3f; public float barrierThickness = 1.0f; public float roadWidth = 1.0f; public float roadHeightOffset = 0.0f; public float roadOffsetW = 0.0f; public GameObject roadPrefabMesh; public int iRoadMaterial = 0; public Material[] roadMaterials; public float[] roadOffsets; public float[] roadWidths; [Header("Terrain params (not working)")] public bool doFlattenAtStart = true; public bool doErodeTerrain = true; public bool doGenerateTerrain = true; public bool doFlattenArroundRoad = true; public bool doLiftRoadToTerrain = false; public TerrainToolkit terToolkit; public Terrain terrain; [Header("Aux")] public string savePath = "Assets\\generated_mesh.asset"; Texture customRoadTexure; public GameObject createdRoad; List debugCenterLine = new List(); List debugLeftRoadEdge = new List(); List debugRightRoadEdge = new List(); List debugLeftBarrierEdge = new List(); List debugRightBarrierEdge = new List(); [Header("Debug")] public bool drawDebugEdges = false; public bool showBarrierMeshes = false; public Color barrierDebugColor = new Color(1.0f, 0.15f, 0.15f, 0.35f); void Start() { if (terToolkit != null && doErodeTerrain) { terToolkit.SmoothTerrain(10, 1.0f); } } public void Init() { if (doBuildRoad) { InitRoad(pathManager.carPath); } } public void DestroyRoad() { GameObject[] prev = GameObject.FindGameObjectsWithTag("road_mesh"); foreach (GameObject g in prev) Destroy(g); iRoadMaterial += 1; } public void SetNewRoadVariation(int iVariation) { if (roadMaterials.Length > 0) customRoadTexure = roadMaterials[iVariation % roadMaterials.Length].mainTexture; if (roadOffsets.Length > 0) roadOffsetW = roadOffsets[iVariation % roadOffsets.Length]; if (roadWidths.Length > 0) roadWidth = roadWidths[iVariation % roadWidths.Length]; } public void NegateYTiling() { if (createdRoad == null) return; MeshRenderer mr = createdRoad.GetComponent(); Vector2 ms = mr.material.mainTextureScale; ms.y *= -1.0f; mr.material.mainTextureScale = ms; } Vector3 GetSegmentWidthNormal(Vector3 from, Vector3 to, Vector3 fallback) { Vector3 length = to - from; if (length.sqrMagnitude < 1e-6f) return fallback; Vector3 width = Vector3.Cross(length, Vector3.up); if (width.sqrMagnitude < 1e-6f) return fallback; return width.normalized; } Vector3 GetNodeWidthNormal(List nodes, int index) { Vector3 fallback = Vector3.right; int count = nodes.Count; if (count < 2) return fallback; if (closeLoop) { int prevIndex = (index - 1 + count) % count; int nextIndex = (index + 1) % count; Vector3 incoming = GetSegmentWidthNormal(nodes[prevIndex].pos, nodes[index].pos, fallback); Vector3 outgoing = GetSegmentWidthNormal(nodes[index].pos, nodes[nextIndex].pos, incoming); Vector3 combined = incoming + outgoing; if (combined.sqrMagnitude < 1e-6f) return outgoing; return combined.normalized; } if (index <= 0) return GetSegmentWidthNormal(nodes[0].pos, nodes[1].pos, fallback); if (index >= count - 1) return GetSegmentWidthNormal(nodes[count - 2].pos, nodes[count - 1].pos, fallback); Vector3 prev = GetSegmentWidthNormal(nodes[index - 1].pos, nodes[index].pos, fallback); Vector3 next = GetSegmentWidthNormal(nodes[index].pos, nodes[index + 1].pos, prev); Vector3 blend = prev + next; if (blend.sqrMagnitude < 1e-6f) return next; return blend.normalized; } List BuildOffsetEdge(List baseEdge, List widthNormals, float outwardOffset, bool isLeftSide) { List edge = new List(); int uniqueCount = baseEdge.Count; if (uniqueCount < 2) return edge; if (closeLoop && (baseEdge[0] - baseEdge[uniqueCount - 1]).sqrMagnitude < 1e-6f) uniqueCount -= 1; if (uniqueCount < 2) return edge; float side = isLeftSide ? 1.0f : -1.0f; for (int i = 0; i < uniqueCount; i++) { Vector3 edgeOffset; if (!closeLoop && i == 0) { edgeOffset = widthNormals[i] * side * outwardOffset; } else if (!closeLoop && i == uniqueCount - 1) { edgeOffset = widthNormals[i - 1] * side * outwardOffset; } else { int prevIndex = closeLoop ? (i - 1 + uniqueCount) % uniqueCount : i - 1; int nextIndex = i; Vector3 prevNormal = widthNormals[prevIndex] * side; Vector3 nextNormal = widthNormals[nextIndex] * side; Vector3 bisector = prevNormal + nextNormal; if (bisector.sqrMagnitude < 1e-6f) { edgeOffset = nextNormal * outwardOffset; } else { bisector.Normalize(); float denom = Mathf.Abs(Vector3.Dot(bisector, nextNormal)); denom = Mathf.Max(denom, 0.35f); float miterLength = outwardOffset / denom; miterLength = Mathf.Min(miterLength, outwardOffset * 1.5f); edgeOffset = bisector * miterLength; } } edge.Add(baseEdge[i] + edgeOffset); } return edge; } public GameObject InitRoad(CarPath path) { if (path == null) { Debug.LogWarning("No path in RoadBuilder::InitRoad"); return null; } if (terToolkit != null && doFlattenAtStart) terToolkit.Flatten(); if (terToolkit != null && doGenerateTerrain) terToolkit.PerlinGenerator(1, 0.1f, 10, 0.5f); GameObject go = GameObject.Instantiate(roadPrefabMesh); MeshRenderer mr = go.GetComponent(); MeshFilter mf = go.GetComponent(); MeshCollider mc = go.GetComponent(); Mesh mesh = new Mesh(); createdRoad = go; if (customRoadTexure != null) { mr.material.mainTexture = customRoadTexure; } else if (roadMaterials != null && iRoadMaterial < roadMaterials.Length) { Material material = roadMaterials[iRoadMaterial]; if (mr != null && material != null) mr.material = material; } go.tag = "road_mesh"; int numQuads = closeLoop ? path.nodes.Count : path.nodes.Count - 1; int numVerts = path.nodes.Count * 2; int numTris = numQuads * 2; Vector3[] vertices = new Vector3[numVerts]; int[] tri = new int[numTris * 3]; Vector3[] normals = new Vector3[numVerts]; Vector2[] uv = new Vector2[numVerts]; for (int iN = 0; iN < numVerts; iN++) normals[iN] = Vector3.up; path.centerNodes = new List(); List centerLine = new List(); List leftRoadEdge = new List(); List rightRoadEdge = new List(); List widthNormals = new List(); for (int iNode = 0; iNode < path.nodes.Count; iNode++) { PathNode node = path.nodes[iNode]; Vector3 pos = node.pos; Vector3 widthNormal = GetNodeWidthNormal(path.nodes, iNode); if (terToolkit != null && doFlattenArroundRoad && (iNode % 5) == 0) terToolkit.FlattenArround(pos + widthNormal * roadOffsetW, 10.0f, 30.0f); if (doLiftRoadToTerrain) pos.y = terrain.SampleHeight(pos) + 1.0f; pos.y += roadHeightOffset; Vector3 roadCenter = pos + widthNormal * roadOffsetW; Vector3 leftPos = roadCenter + widthNormal * roadWidth; Vector3 rightPos = roadCenter - widthNormal * roadWidth; PathNode centerNode = new PathNode(); centerNode.pos = roadCenter; centerNode.rotation = node.rotation; path.centerNodes.Add(centerNode); centerLine.Add(roadCenter); leftRoadEdge.Add(leftPos); rightRoadEdge.Add(rightPos); widthNormals.Add(widthNormal); int iVert = iNode * 2; vertices[iVert] = leftPos; vertices[iVert + 1] = rightPos; uv[iVert] = new Vector2(0.2f * iNode, 0.0f); uv[iVert + 1] = new Vector2(0.2f * iNode, 1.0f); } int iVertOffset = 0; int iTriOffset = 0; for (int iQuad = 0; iQuad < numQuads; iQuad++) { int nextVertOffset = closeLoop ? (iVertOffset + 2) % numVerts : iVertOffset + 2; tri[0 + iTriOffset] = iVertOffset; tri[1 + iTriOffset] = nextVertOffset; tri[2 + iTriOffset] = iVertOffset + 1; tri[3 + iTriOffset] = nextVertOffset; tri[4 + iTriOffset] = nextVertOffset + 1; tri[5 + iTriOffset] = iVertOffset + 1; iVertOffset += 2; iTriOffset += 6; } mesh.vertices = vertices; mesh.triangles = tri; mesh.normals = normals; mesh.uv = uv; mesh.Optimize(); mesh.RecalculateBounds(); mf.mesh = mesh; mc.sharedMesh = mesh; if (doAddBarriers && centerLine.Count > 1) { List leftEdge = BuildOffsetEdge(leftRoadEdge, widthNormals, barrierOffset, true); List rightEdge = BuildOffsetEdge(rightRoadEdge, widthNormals, barrierOffset, false); if (leftEdge.Count > 1 && rightEdge.Count > 1) { if (closeLoop) { leftEdge.Add(leftEdge[0]); rightEdge.Add(rightEdge[0]); } CreateBarrier(leftEdge, "left_barrier", go.transform); CreateBarrier(rightEdge, "right_barrier", go.transform); if (!closeLoop && centerLine.Count >= 2) { // Seal the open ends so the car cannot drive off either end of the track. // Push the start cap 1 m back along the road so it doesn't sit on the spawn point. Vector3 startDir = (centerLine[1] - centerLine[0]).normalized; CreateEndCap( leftEdge[0] - startDir, rightEdge[0] - startDir, "start_cap", go.transform); CreateEndCap( leftEdge[leftEdge.Count - 1], rightEdge[rightEdge.Count - 1], "end_cap", go.transform); } } debugLeftBarrierEdge = new List(leftEdge); debugRightBarrierEdge = new List(rightEdge); } else { debugLeftBarrierEdge = new List(); debugRightBarrierEdge = new List(); } debugCenterLine = new List(centerLine); debugLeftRoadEdge = new List(leftRoadEdge); debugRightRoadEdge = new List(rightRoadEdge); if (terToolkit != null && doErodeTerrain) { terToolkit.SmoothTerrain(10, 1.0f); if (doFlattenArroundRoad) { foreach (PathNode n in path.nodes) terToolkit.FlattenArround(n.pos, 8.0f, 10.0f); } } return go; } // Creates a series of solid BoxColliders along a barrier edge path. // Each segment gets its own box with real 3D volume so fast-moving cars // cannot tunnel through between physics frames. void CreateBarrier(List edgePositions, string barrierName, Transform parent) { int n = edgePositions.Count; if (n < 2) return; GameObject containerGo = new GameObject(barrierName); containerGo.transform.parent = parent; containerGo.transform.localPosition = Vector3.zero; containerGo.transform.localRotation = Quaternion.identity; // Overlap each segment slightly into its neighbours to close the tiny // gap that would otherwise exist at the miter point between two boxes // meeting at a corner. float overlap = barrierThickness * 0.5f; for (int i = 0; i < n - 1; i++) { Vector3 a = edgePositions[i]; Vector3 b = edgePositions[i + 1]; Vector3 delta = b - a; float segLen = delta.magnitude; if (segLen < 0.001f) continue; // Box centre: midpoint horizontally, half-height up Vector3 center = (a + b) * 0.5f + Vector3.up * (barrierHeight * 0.5f); Quaternion rot = Quaternion.LookRotation(delta.normalized, Vector3.up); GameObject segGo = new GameObject(barrierName + "_seg" + i); segGo.transform.parent = containerGo.transform; segGo.transform.position = center; segGo.transform.rotation = rot; // X = thickness (perpendicular to road surface — blocks lateral escape) // Y = height // Z = length along segment + overlap to close corner gaps BoxCollider bc = segGo.AddComponent(); bc.size = new Vector3(barrierThickness, barrierHeight, segLen + overlap); bc.center = Vector3.zero; if (showBarrierMeshes) { MeshFilter meshFilter = segGo.AddComponent(); meshFilter.sharedMesh = CreateBoxMesh(barrierThickness, barrierHeight, segLen + overlap); MeshRenderer meshRend = segGo.AddComponent(); meshRend.sharedMaterial = CreateBarrierDebugMaterial(); meshRend.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; meshRend.receiveShadows = false; } } } // Solid wall across the track at an open end — prevents the car driving off // the start or finish of a non-looping track. void CreateEndCap(Vector3 leftPoint, Vector3 rightPoint, string capName, Transform parent) { Vector3 delta = rightPoint - leftPoint; float width = delta.magnitude; if (width < 0.001f) return; Vector3 center = (leftPoint + rightPoint) * 0.5f + Vector3.up * (barrierHeight * 0.5f); // LookRotation: Z axis points left→right, so the box spans the full width Quaternion rot = Quaternion.LookRotation(delta.normalized, Vector3.up); GameObject capGo = new GameObject(capName); capGo.transform.parent = parent; capGo.transform.position = center; capGo.transform.rotation = rot; BoxCollider bc = capGo.AddComponent(); // Z = left-to-right span (plus thickness on each side to overlap the side barriers) // X = road-direction thickness bc.size = new Vector3(barrierThickness, barrierHeight, width + barrierThickness * 2f); bc.center = Vector3.zero; if (showBarrierMeshes) { MeshFilter meshFilter = capGo.AddComponent(); meshFilter.sharedMesh = CreateBoxMesh(barrierThickness, barrierHeight, width + barrierThickness * 2f); MeshRenderer meshRend = capGo.AddComponent(); meshRend.sharedMaterial = CreateBarrierDebugMaterial(); meshRend.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; meshRend.receiveShadows = false; } } // Unit box mesh centred at origin — used for debug barrier visualisation. Mesh CreateBoxMesh(float w, float h, float d) { float hw = w * 0.5f, hh = h * 0.5f, hd = d * 0.5f; Vector3[] verts = new Vector3[] { new Vector3(-hw, -hh, -hd), new Vector3( hw, -hh, -hd), new Vector3( hw, hh, -hd), new Vector3(-hw, hh, -hd), new Vector3(-hw, -hh, hd), new Vector3( hw, -hh, hd), new Vector3( hw, hh, hd), new Vector3(-hw, hh, hd), }; int[] tris = new int[] { 0,2,1, 0,3,2, // back 4,5,6, 4,6,7, // front 0,1,5, 0,5,4, // bottom 3,7,6, 3,6,2, // top 0,4,7, 0,7,3, // left 1,2,6, 1,6,5, // right }; Mesh m = new Mesh(); m.vertices = verts; m.triangles = tris; m.RecalculateNormals(); m.RecalculateBounds(); return m; } Material CreateBarrierDebugMaterial() { Material mat = new Material(Shader.Find("Standard")); Color c = barrierDebugColor; if (c.a <= 0.0f || (c.r + c.g + c.b) / 3.0f < 0.1f) c = new Color(1.0f, 0.75f, 0.75f, 0.35f); mat.color = c; mat.SetFloat("_Mode", 3.0f); mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off); mat.EnableKeyword("_ALPHABLEND_ON"); mat.EnableKeyword("_EMISSION"); mat.SetColor("_EmissionColor", c); mat.renderQueue = 3000; return mat; } void OnDrawGizmosSelected() { if (!drawDebugEdges) return; DrawDebugPolyline(debugCenterLine, Color.cyan); DrawDebugPolyline(debugLeftRoadEdge, Color.green); DrawDebugPolyline(debugRightRoadEdge, Color.green); DrawDebugPolyline(debugLeftBarrierEdge, Color.red); DrawDebugPolyline(debugRightBarrierEdge, Color.red); } void DrawDebugPolyline(List points, Color color) { if (points == null || points.Count < 2) return; Gizmos.color = color; for (int i = 0; i < points.Count - 1; i++) Gizmos.DrawLine(points[i], points[i + 1]); if (closeLoop && points.Count > 2) Gizmos.DrawLine(points[points.Count - 1], points[0]); } }