public Car() { intersectionSpline = new TrackSpline(); }
public void Update() { TrackSpline currentSpline = roadSpline; Vector2 extrudePoint; if (isInsideIntersection) { currentSpline = intersectionSpline; } // acceleration normalizedSpeed += Time.deltaTime * 2f; if (normalizedSpeed > 1f) { normalizedSpeed = 1f; } // splineTimer goes from 0 to 1, so we need to adjust our speed // based on our current spline's length. splineTimer += normalizedSpeed * maxSpeed / currentSpline.measuredLength * Time.deltaTime; List <Car> queue = null; if (isInsideIntersection == false) { float approachSpeed = 1f; // find other cars in our lane queue = roadSpline.GetQueue(splineDirection, splineSide); int index = queue.IndexOf(this); if (index > 0) { // someone's ahead of us - don't clip through them float maxT = queue[index - 1].splineTimer - roadSpline.carQueueSize; if (splineTimer > maxT) { splineTimer = maxT; normalizedSpeed = 0f; } else { // slow down when approaching another car approachSpeed = (maxT - splineTimer) * 5f; } } else { // we're "first in line" in our lane, but we still might need // to slow down if our next intersection is occupied if (splineDirection == 1) { if (roadSpline.endIntersection.occupied[(splineSide + 1) / 2]) { approachSpeed = (1f - splineTimer) * .8f + .2f; } } else { if (roadSpline.startIntersection.occupied[(splineSide + 1) / 2]) { approachSpeed = (1f - splineTimer) * .8f + .2f; } } } if (normalizedSpeed > approachSpeed) { normalizedSpeed = approachSpeed; } } else { // speed penalty when we're inside an intersection if (normalizedSpeed > .7f) { normalizedSpeed = .7f; } } float t = Mathf.Clamp01(splineTimer); // figure out our position in "unextruded" road space // (top of bottom of road, on the left or right side) if (isInsideIntersection == false) { if (splineDirection == -1) { t = 1f - t; } extrudePoint = new Vector2(-RoadGenerator.trackRadius * .5f * splineDirection * splineSide, RoadGenerator.trackThickness * .5f * splineSide); } else { extrudePoint = new Vector2(-RoadGenerator.trackRadius * .5f * intersectionSide, RoadGenerator.trackThickness * .5f * intersectionSide); } // find our position and orientation Vector3 forward, up; Vector3 splinePoint = currentSpline.Extrude(extrudePoint, t, out forward, out up); up *= splineSide; position = splinePoint + up.normalized * .06f; Vector3 moveDir = position - lastPosition; lastPosition = position; if (moveDir.sqrMagnitude > 0.0001f && up.sqrMagnitude > 0.0001f) { rotation = Quaternion.LookRotation(moveDir * splineDirection, up); } matrix = Matrix4x4.TRS(position, rotation, new Vector3(.1f, .08f, .12f)); if (splineTimer >= 1f) { // we've reached the end of our current segment if (isInsideIntersection) { // we're exiting an intersection - make sure the next road // segment has room for us before we proceed if (roadSpline.GetQueue(splineDirection, splineSide).Count <= roadSpline.maxCarCount) { intersectionSpline.startIntersection.occupied[(intersectionSide + 1) / 2] = false; isInsideIntersection = false; splineTimer = 0f; } else { splineTimer = 1f; normalizedSpeed = 0f; } } else { // we're exiting a road segment - first, we need to know // which intersection we're entering Intersection intersection; if (splineDirection == 1) { intersection = roadSpline.endIntersection; intersectionSpline.startPoint = roadSpline.endPoint; } else { intersection = roadSpline.startIntersection; intersectionSpline.startPoint = roadSpline.startPoint; } // now we need to know which road segment we'll move into // (dead-ends force u-turns, but otherwise, u-turns are not allowed) int newSplineIndex = 0; if (intersection.neighbors.Count > 1) { int mySplineIndex = intersection.neighborSplines.IndexOf(roadSpline); newSplineIndex = Random.Range(0, intersection.neighborSplines.Count - 1); if (newSplineIndex >= mySplineIndex) { newSplineIndex++; } } TrackSpline newSpline = intersection.neighborSplines[newSplineIndex]; // make sure that our side of the intersection (top/bottom) // is empty before we enter if (intersection.occupied[(intersectionSide + 1) / 2]) { splineTimer = 1f; normalizedSpeed = 0f; } else { // to avoid flipping between top/bottom of our roads, // we need to know our new spline's normal at our entrance point Vector3 newNormal; if (newSpline.startIntersection == intersection) { splineDirection = 1; newNormal = newSpline.startNormal; intersectionSpline.endPoint = newSpline.startPoint; } else { splineDirection = -1; newNormal = newSpline.endNormal; intersectionSpline.endPoint = newSpline.endPoint; } // now we'll prepare our intersection spline - this lets us // create a "temporary lane" inside the current intersection intersectionSpline.anchor1 = (intersection.position + intersectionSpline.startPoint) * .5f; intersectionSpline.anchor2 = (intersection.position + intersectionSpline.endPoint) * .5f; intersectionSpline.startTangent = Vector3Int.RoundToInt((intersection.position - intersectionSpline.startPoint).normalized); intersectionSpline.endTangent = Vector3Int.RoundToInt((intersection.position - intersectionSpline.endPoint).normalized); intersectionSpline.startNormal = intersection.normal; intersectionSpline.endNormal = intersection.normal; if (roadSpline == newSpline) { // u-turn - make our intersection spline more rounded than usual Vector3 perp = Vector3.Cross(intersectionSpline.startTangent, intersectionSpline.startNormal); intersectionSpline.anchor1 += (Vector3)intersectionSpline.startTangent * RoadGenerator.intersectionSize * .5f; intersectionSpline.anchor2 += (Vector3)intersectionSpline.startTangent * RoadGenerator.intersectionSize * .5f; intersectionSpline.anchor1 -= perp * RoadGenerator.trackRadius * .5f * intersectionSide; intersectionSpline.anchor2 += perp * RoadGenerator.trackRadius * .5f * intersectionSide; } intersectionSpline.startIntersection = intersection; intersectionSpline.endIntersection = intersection; intersectionSpline.MeasureLength(); isInsideIntersection = true; // to maintain our current orientation, should we be // on top of or underneath our next road segment? // (each road segment has its own "up" direction, at each end) if (Vector3.Dot(newNormal, up) > 0f) { splineSide = 1; } else { splineSide = -1; } // should we be on top of or underneath the intersection? if (Vector3.Dot(intersectionSpline.startNormal, up) > 0f) { intersectionSide = 1; } else { intersectionSide = -1; } // block other cars from entering this intersection intersection.occupied[(intersectionSide + 1) / 2] = true; // remove ourselves from our previous lane's list of cars if (queue != null) { queue.Remove(this); } // add "leftover" spline timer value to our new spline timer // (avoids a stutter when changing between splines) splineTimer = (splineTimer - 1f) * roadSpline.measuredLength / intersectionSpline.measuredLength; roadSpline = newSpline; newSpline.GetQueue(splineDirection, splineSide).Add(this); } } } }
IEnumerator SpawnRoads() { // first generation pass: plan roads as basic voxel data only trackVoxels = new bool[voxelCount, voxelCount, voxelCount]; List <Vector3Int> activeVoxels = new List <Vector3Int>(); trackVoxels[voxelCount / 2, voxelCount / 2, voxelCount / 2] = true; activeVoxels.Add(new Vector3Int(voxelCount / 2, voxelCount / 2, voxelCount / 2)); // after voxel generation, we'll convert our network into non-voxels intersections = new List <Intersection>(); intersectionsGrid = new Intersection[voxelCount, voxelCount, voxelCount]; intersectionPairs = new HashSet <long>(); trackSplines = new List <TrackSpline>(); roadMeshes = new List <Mesh>(); intersectionMatrices = new List <List <Matrix4x4> >(); cars = new List <Car>(); carMatrices = new List <List <Matrix4x4> >(); carMatrices.Add(new List <Matrix4x4>()); carColors = new List <List <Vector4> >(); carColors.Add(new List <Vector4>()); carMatProps = new MaterialPropertyBlock(); carMatProps.SetVectorArray("_Color", new Vector4[instancesPerBatch]); // plan roads broadly: first, as a grid of true/false voxels int ticker = 0; while (activeVoxels.Count > 0 && ticker < 50000) { ticker++; int index = Random.Range(0, activeVoxels.Count); Vector3Int pos = activeVoxels[index]; Vector3Int dir = dirs[Random.Range(0, dirs.Length)]; Vector3Int pos2 = new Vector3Int(pos.x + dir.x, pos.y + dir.y, pos.z + dir.z); if (GetVoxel(pos2) == false) { // when placing a new voxel, it must have fewer than three // diagonal-or-cardinal neighbors. // (this blocks nonplanar intersections from forming) if (CountNeighbors(pos2, true) < 3) { activeVoxels.Add(pos2); trackVoxels[pos2.x, pos2.y, pos2.z] = true; } } int neighborCount = CountNeighbors(pos); if (neighborCount >= 3) { // no more than three cardinal neighbors for any voxel (no 4-way intersections allowed) // (really, this is to avoid nonplanar intersections) Intersection intersection = new Intersection(pos, (Vector3)pos * voxelSize, Vector3Int.zero); intersection.id = intersections.Count; intersections.Add(intersection); intersectionsGrid[pos.x, pos.y, pos.z] = intersection; activeVoxels.RemoveAt(index); } if (ticker % 1000 == 0) { yield return(null); } } Debug.Log(intersections.Count + " intersections"); // at this point, we've generated our full layout, but everything // is voxels, and we've identified which voxels are intersections. // next, we'll reinterpret our voxels as a network of intersections: // we'll find all "neighboring intersections" in our voxel map // (neighboring intersections are connected by a chain of voxels, // which we'll replace with splines) for (int i = 0; i < intersections.Count; i++) { Intersection intersection = intersections[i]; Vector3Int axesWithNeighbors = Vector3Int.zero; for (int j = 0; j < dirs.Length; j++) { if (GetVoxel(intersection.index + dirs[j], false)) { axesWithNeighbors.x += Mathf.Abs(dirs[j].x); axesWithNeighbors.y += Mathf.Abs(dirs[j].y); axesWithNeighbors.z += Mathf.Abs(dirs[j].z); Vector3Int connectDir; Intersection neighbor = FindFirstIntersection(intersection.index, dirs[j], out connectDir); if (neighbor != null && neighbor != intersection) { // make sure we haven't already added the reverse-version of this spline long hash = HashIntersectionPair(intersection, neighbor); if (intersectionPairs.Contains(hash) == false) { intersectionPairs.Add(hash); TrackSpline spline = new TrackSpline(intersection, dirs[j], neighbor, connectDir); trackSplines.Add(spline); intersection.neighbors.Add(neighbor); intersection.neighborSplines.Add(spline); neighbor.neighbors.Add(intersection); neighbor.neighborSplines.Add(spline); } } } } // find this intersection's normal - it's the one axis // along which we have no neighbors for (int j = 0; j < 3; j++) { if (axesWithNeighbors[j] == 0) { if (intersection.normal == Vector3Int.zero) { intersection.normal = new Vector3Int(); intersection.normal[j] = -1 + Random.Range(0, 2) * 2; //Debug.DrawRay(intersection.position,(Vector3)intersection.normal * .5f,Color.red,1000f); } else { Debug.LogError("a straight line has been marked as an intersection!"); } } } if (intersection.normal == Vector3Int.zero) { Debug.LogError("nonplanar intersections are not allowed!"); } // NOTE - if you investigate the above logic, you might be confused about how // dead-ends are given normals, since we're assuming that all intersections // have two axes with neighbors and only one axis without. dead-ends only have // one neighbor-axis...and somehow they still get a normal without a special case. // // the "gotcha" is that the visible dead-ends in the demo have three // neighbors during the voxel phase, with two of their neighbor chains leading // to nothing. these "hanging chains" are not included as splines, so the // dead-ends that we see are actually "T" shapes with the top two segments hidden. if (i % 20 == 0) { yield return(null); } } Debug.Log(trackSplines.Count + " road splines"); // generate road meshes List <Vector3> vertices = new List <Vector3>(); List <Vector2> uvs = new List <Vector2>(); List <int> triangles = new List <int>(); int triCount = 0; for (int i = 0; i < trackSplines.Count; i++) { trackSplines[i].GenerateMesh(vertices, uvs, triangles); if (triangles.Count / 3 > trisPerMesh || i == trackSplines.Count - 1) { // our current mesh data is ready to go! if (triangles.Count > 0) { Mesh mesh = new Mesh(); mesh.name = "Generated Road Mesh"; mesh.SetVertices(vertices); mesh.SetUVs(0, uvs); mesh.SetTriangles(triangles, 0); mesh.RecalculateNormals(); mesh.RecalculateBounds(); roadMeshes.Add(mesh); triCount += triangles.Count / 3; } vertices.Clear(); uvs.Clear(); triangles.Clear(); } if (i % 10 == 0) { yield return(null); } } // generate intersection matrices for batch-rendering int batch = 0; intersectionMatrices.Add(new List <Matrix4x4>()); for (int i = 0; i < intersections.Count; i++) { intersectionMatrices[batch].Add(intersections[i].GetMatrix()); if (intersectionMatrices[batch].Count == instancesPerBatch) { batch++; intersectionMatrices.Add(new List <Matrix4x4>()); } } Debug.Log(triCount + " road triangles (" + roadMeshes.Count + " meshes)"); // spawn cars batch = 0; for (int i = 0; i < 4000; i++) { Car car = new Car(); car.maxSpeed = carSpeed; car.roadSpline = trackSplines[Random.Range(0, trackSplines.Count)]; car.splineTimer = 1f; car.splineDirection = -1 + Random.Range(0, 2) * 2; car.splineSide = -1 + Random.Range(0, 2) * 2; car.roadSpline.GetQueue(car.splineDirection, car.splineSide).Add(car); cars.Add(car); carMatrices[batch].Add(Matrix4x4.identity); carColors[batch].Add(Random.ColorHSV()); if (carMatrices[batch].Count == instancesPerBatch) { carMatrices.Add(new List <Matrix4x4>()); carColors.Add(new List <Vector4>()); batch++; } } }