538 lines
19 KiB
C#
Executable File
538 lines
19 KiB
C#
Executable File
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<Vector3> debugCenterLine = new List<Vector3>();
|
|
List<Vector3> debugLeftRoadEdge = new List<Vector3>();
|
|
List<Vector3> debugRightRoadEdge = new List<Vector3>();
|
|
List<Vector3> debugLeftBarrierEdge = new List<Vector3>();
|
|
List<Vector3> debugRightBarrierEdge = new List<Vector3>();
|
|
|
|
[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<MeshRenderer>();
|
|
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<PathNode> 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<Vector3> BuildOffsetEdge(List<Vector3> baseEdge, List<Vector3> widthNormals, float outwardOffset, bool isLeftSide)
|
|
{
|
|
List<Vector3> edge = new List<Vector3>();
|
|
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<MeshRenderer>();
|
|
MeshFilter mf = go.GetComponent<MeshFilter>();
|
|
MeshCollider mc = go.GetComponent<MeshCollider>();
|
|
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<PathNode>();
|
|
|
|
List<Vector3> centerLine = new List<Vector3>();
|
|
List<Vector3> leftRoadEdge = new List<Vector3>();
|
|
List<Vector3> rightRoadEdge = new List<Vector3>();
|
|
List<Vector3> widthNormals = new List<Vector3>();
|
|
|
|
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<Vector3> leftEdge = BuildOffsetEdge(leftRoadEdge, widthNormals, barrierOffset, true);
|
|
List<Vector3> 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<Vector3>(leftEdge);
|
|
debugRightBarrierEdge = new List<Vector3>(rightEdge);
|
|
}
|
|
else
|
|
{
|
|
debugLeftBarrierEdge = new List<Vector3>();
|
|
debugRightBarrierEdge = new List<Vector3>();
|
|
}
|
|
|
|
debugCenterLine = new List<Vector3>(centerLine);
|
|
debugLeftRoadEdge = new List<Vector3>(leftRoadEdge);
|
|
debugRightRoadEdge = new List<Vector3>(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<Vector3> 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<BoxCollider>();
|
|
bc.size = new Vector3(barrierThickness, barrierHeight, segLen + overlap);
|
|
bc.center = Vector3.zero;
|
|
|
|
if (showBarrierMeshes)
|
|
{
|
|
MeshFilter meshFilter = segGo.AddComponent<MeshFilter>();
|
|
meshFilter.sharedMesh = CreateBoxMesh(barrierThickness, barrierHeight, segLen + overlap);
|
|
MeshRenderer meshRend = segGo.AddComponent<MeshRenderer>();
|
|
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<BoxCollider>();
|
|
// 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>();
|
|
meshFilter.sharedMesh = CreateBoxMesh(barrierThickness, barrierHeight, width + barrierThickness * 2f);
|
|
MeshRenderer meshRend = capGo.AddComponent<MeshRenderer>();
|
|
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<Vector3> 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]);
|
|
}
|
|
}
|