//$ Copyright 2015-22, Code Respawn Technologies Pvt Ltd - All Rights Reserved $// using UnityEngine; using System.Collections.Generic; using DungeonArchitect.RoadNetworks; using DungeonArchitect.Splatmap; namespace DungeonArchitect.Builders.CircularCity { public static class CircularCityDungeonMarkerNames { public static readonly string House = "House"; public static readonly string WallMarkerName = "CityWall"; public static readonly string DoorMarkerName = "CityDoor"; public static readonly string GroundMarkerName = "CityGround"; public static readonly string CornerTowerMarkerName = "CornerTower"; public static readonly string WallPaddingMarkerName = "CityWallPadding"; } public class CircularCityDungeonBuilder : DungeonBuilder { CircularCityDungeonConfig cityConfig; CircularCityDungeonModel cityModel; new System.Random random; /// /// Builds the dungeon layout. In this method, you should build your dungeon layout and save it in your model file /// No markers should be emitted here. (EmitMarkers function will be called later by the engine to do that) /// /// The builder configuration /// The dungeon model that the builder will populate public override void BuildDungeon(DungeonConfig config, DungeonModel model) { var sw = System.Diagnostics.Stopwatch.StartNew(); base.BuildDungeon(config, model); random = new System.Random((int)config.Seed); // We know that the dungeon prefab would have the appropriate config and models attached to it // Cast and save it for future reference cityConfig = config as CircularCityDungeonConfig; cityModel = model as CircularCityDungeonModel; cityModel.Config = cityConfig; // Generate the city layout and save it in a model. No markers are emitted here. GenerateCityLayout(); sw.Stop(); Debug.Log("Time elapsed: " + (sw.ElapsedMilliseconds / 1000.0f) + " s"); } public override void OnDestroyed() { base.OnDestroyed(); cityModel = model as CircularCityDungeonModel; if (cityModel.roadGraph != null) { //cityModel.roadGraph.nodes = new RoadGraphNode[0]; } } /// /// Override the builder's emit marker function to emit our own markers based on the layout that we built /// You should emit your markers based on the layout you have saved in the model generated previously /// When the user is designing the theme interactively, this function will be called whenever the graph state changes, /// so the theme engine can populate the scene (BuildDungeon will not be called if there is no need to rebuild the layout again) /// public override void EmitMarkers() { base.EmitMarkers(); EmitCityMarkers(); ProcessMarkerOverrideVolumes(); } delegate void InsertHouseDelegate(); /// /// Grabs the texture stored in the splat asset. The index is based on the texture info array you define in the splat component /// /// /// Texture2D GetSplatTexture(int index) { var splatComponent = GetComponent(); if (splatComponent == null) return null; if (splatComponent.splatmap == null) return null; var splatmap = splatComponent.splatmap; if (index >= splatmap.splatTextures.Length) return null; return splatmap.splatTextures[index]; } Texture2D GetRoadmap() { return GetSplatTexture(0); } /// /// Generate a layout and save it in the model /// void GenerateCityLayout() { var roadGraphSettings = new RoadGraphBuilderSettings(); roadGraphSettings.interNodeDistance = cityConfig.interNodeDistance; var roadGraphBuilder = new RoadGraphBuilder(roadGraphSettings); float startRadius = cityConfig.startRadius; float endRadius = cityConfig.endRadius; var center = Vector3.zero; float mainRoadStrength = cityConfig.mainRoadStrength; float sideRoadStrength = cityConfig.sideRoadStrength; float interRingDistance = (endRadius - startRadius) / (cityConfig.numRings - 1); // Draw the city rings { float ringRadius = startRadius; for (int r = 0; r < cityConfig.numRings; r++) { roadGraphBuilder.CreateCircle(center, ringRadius, mainRoadStrength); if (r < cityConfig.numRings - 1) { float ringLaneRadius = ringRadius + interRingDistance / 2.0f; roadGraphBuilder.CreateCircle(center, ringLaneRadius, sideRoadStrength); } ringRadius += interRingDistance; } } // Draw the ray lanes ejecting out from the center float interRayAngle = Mathf.PI * 2 / cityConfig.numRays; for (int i = 0; i < cityConfig.numRays; i++) { float ringRadius = startRadius; int mainSegmentToRemove = random.Next() % (cityConfig.numRings - 1); for (int r = 0; r < cityConfig.numRings - 1; r++) { float nextRingRadius = ringRadius + interRingDistance; bool bDrawMainSegment = (r != mainSegmentToRemove); if (bDrawMainSegment) { float angle = i * interRayAngle; var direction = new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)); float midRingRadius = ringRadius + (nextRingRadius - ringRadius) / 2.0f; var start = center + direction * ringRadius; var end = center + direction * midRingRadius; roadGraphBuilder.CreateLine(start, end, mainRoadStrength); start = center + direction * midRingRadius; end = center + direction * nextRingRadius; roadGraphBuilder.CreateLine(start, end, mainRoadStrength); } // Draw the side segments { float interSideRayAngle = interRayAngle / (r + 2); float sideRayAngle = i * interRayAngle; float interSideRingDistance = interRingDistance / 2.0f; for (int si = 0; si <= r; si++) { sideRayAngle += interSideRayAngle; for (int t = 0; t < 2; t++) { bool bRemoveSideSegment = (random.NextFloat() < cityConfig.sideRoadRemovalProbability); if (!bRemoveSideSegment) { float perimeter = Mathf.PI * 2 * ringRadius; float angleRandomization = cityConfig.randomSideLaneOffsetAngle * Mathf.Deg2Rad / perimeter; float randomValue = random.NextFloat() * 2 - 1; float randomizedAngle = sideRayAngle + angleRandomization * randomValue; float laneStartRadius = ringRadius + interSideRingDistance * t; float laneEndRadius = ringRadius + interSideRingDistance * (t + 1); var direction = new Vector3(Mathf.Cos(randomizedAngle), 0, Mathf.Sin(randomizedAngle)); var start = center + direction * laneStartRadius; var end = center + direction * laneEndRadius; roadGraphBuilder.CreateLine(start, end, sideRoadStrength); } } } } ringRadius = nextRingRadius; } } cityModel.roadGraph = roadGraphBuilder.BakeRoadGraph(); var layoutBuilder = new RoadLayoutBuilder(cityModel.roadGraph, cityConfig.roadMesh); layoutBuilder.RoadBlockLayoutBuilt += LayoutBuilder_RoadBlockLayoutBuilt; cityModel.layoutGraph = layoutBuilder.BakeLayoutGraph(); } private void LayoutBuilder_RoadBlockLayoutBuilt(ref Vector3[] layout) { } void DebugDrawGraphGizmo(RoadGraph graph, Color edgeColor, Color nodeColor) { if (graph == null || graph.nodes == null) { return; } // Create a hash table of the nodes for fast lookup var nodes = new Dictionary(); foreach (var node in graph.nodes) { nodes.Add(node.nodeId, node); } // Draw the edges Gizmos.color = edgeColor; foreach (var node in graph.nodes) { foreach (var edge in node.adjacentEdges) { var otherNode = nodes[edge.otherNodeId]; Gizmos.DrawLine(node.position, otherNode.position); } } // Draw the nodes Gizmos.color = nodeColor; foreach (var node in graph.nodes) { Gizmos.DrawSphere(node.position, .25f); } } public override void DebugDrawGizmos() { if (cityConfig == null) { cityConfig = GetComponent(); if (cityConfig == null) return; } if (model == null) { model = GetComponent(); } if (cityModel == null) { cityModel = model as CircularCityDungeonModel; } if (cityModel == null) { return; } DebugDrawGraphGizmo(cityModel.roadGraph, Color.green, Color.red); DebugDrawGraphGizmo(cityModel.layoutGraph, Color.yellow, Color.blue); } class SpatialPartitionCache { private int gridSize; Dictionary> occupancyGrid = new Dictionary>(); public SpatialPartitionCache(int gridSize) { this.gridSize = gridSize; } public void RegisterAsOccupied(Vector3 position) { var cell = GetCell(position); if (!occupancyGrid.ContainsKey(cell)) { occupancyGrid.Add(cell, new List()); } occupancyGrid[cell].Add(position); } public bool IsFree(Vector3 position, float distanceSearch) { var baseCell = GetCell(position); for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { var cell = baseCell + new IntVector2(dx, dy); if (!IsFree(position, cell, distanceSearch)) { return false; } } } return true; } bool IsFree(Vector3 sourcePosition, IntVector2 cell, float distanceSearch) { if (!occupancyGrid.ContainsKey(cell)) { return true; } float distanceSearchSq = distanceSearch * distanceSearch; foreach (var position in occupancyGrid[cell]) { var distanceSq = (sourcePosition - position).sqrMagnitude; if (distanceSq < distanceSearchSq) { return false; } } return true; } IntVector2 GetCell(Vector3 position) { var cell = new IntVector2(); cell.x = Mathf.RoundToInt(position.x / gridSize); cell.y = Mathf.RoundToInt(position.z / gridSize); return cell; } } /// /// Emit marker points so that the theme can decorate the scene layout that we just built /// void EmitCityMarkers() { var basePosition = transform.position; float occupancySearchDistance = cityConfig.buildingSize * 1.0f; int cacheGridResolution = Mathf.RoundToInt(occupancySearchDistance * 2.0f); var spatialOccupancy = new SpatialPartitionCache(cacheGridResolution); foreach (var node in cityModel.roadGraph.nodes) { spatialOccupancy.RegisterAsOccupied(node.position); } foreach (var node in cityModel.layoutGraph.nodes) { spatialOccupancy.RegisterAsOccupied(node.position); } for (float r = cityConfig.startRadius; r < cityConfig.endRadius; r += cityConfig.buildingSize) { float circumference = 2 * Mathf.PI * r; int numBuildings = Mathf.RoundToInt(circumference / cityConfig.buildingSize); for (int i = 0; i < numBuildings; i++) { float angle = 2 * Mathf.PI * i / (float)numBuildings; var offset = Vector3.zero; offset.x = Mathf.Cos(angle) * r; offset.z = Mathf.Sin(angle) * r; var position = basePosition + offset; if (!spatialOccupancy.IsFree(offset, occupancySearchDistance)) { continue; } var rotation = Quaternion.Euler(0, -angle * Mathf.Rad2Deg, 0); EmitMarkerAt(CircularCityDungeonMarkerNames.House, position, rotation); } } } void EmitMarkerAt(string markerName, Vector3 worldPosition, Quaternion rotation) { var transformation = Matrix4x4.TRS(worldPosition, rotation, Vector3.one); var gridPosition = IntVector.Zero; EmitMarker(markerName, transformation, gridPosition, -1); } } }