From 54b833b6207a11010bcaa9ae7131ac63ec67f3a0 Mon Sep 17 00:00:00 2001 From: Tiberiu Chibici Date: Fri, 29 May 2015 19:03:08 +0300 Subject: [PATCH] Finished road generator --- Game/Assembly-CSharp-firstpass-vs.csproj | 2 +- Game/Assembly-CSharp-firstpass.csproj | 2 +- Game/Assembly-CSharp-vs.csproj | 8 +- Game/Assembly-CSharp.csproj | 8 +- ...ssembly-UnityScript-firstpass-vs.unityproj | 2 +- Game/Assembly-UnityScript-firstpass.unityproj | 2 +- Game/Assets/Data/Biomes/Grassland.xml | 2 +- Game/Assets/Data/Biomes/Mountain.xml.meta | 6 +- .../Assets/Scripts/Generator/CityGenerator.cs | 2 - .../Generator/PopulationCentersGenerator.cs | 15 +- .../Assets/Scripts/Generator/RoadGenerator.cs | 435 ++++++++++++++---- Game/Assets/Scripts/Model/Map.cs | 81 +++- Game/Assets/Scripts/Model/Road/RoadNetwork.cs | 123 ++++- Game/Assets/Scripts/Model/Road/RoadNode.cs | 26 +- Game/Assets/Scripts/Model/Road/RoadSegment.cs | 17 +- Game/Assets/Scripts/Model/Vector2.cs | 7 +- .../Scripts/Unity/TerrainGeneratorScript.cs | 44 +- Game/Game-csharp.sln | 2 +- Game/Game.sln | 2 +- Game/UnityVS.Game.CSharp.csproj | 6 +- .../MapViewer/MapViewer/Storage/MapStorage.cs | 15 +- 21 files changed, 638 insertions(+), 169 deletions(-) diff --git a/Game/Assembly-CSharp-firstpass-vs.csproj b/Game/Assembly-CSharp-firstpass-vs.csproj index 7d5a212..21d4c8f 100644 --- a/Game/Assembly-CSharp-firstpass-vs.csproj +++ b/Game/Assembly-CSharp-firstpass-vs.csproj @@ -49,9 +49,9 @@ - + C:/Program Files/Unity/Editor/Data/UnityExtensions/Unity/GUISystem/UnityEngine.UI.dll diff --git a/Game/Assembly-CSharp-firstpass.csproj b/Game/Assembly-CSharp-firstpass.csproj index 7d5a212..21d4c8f 100644 --- a/Game/Assembly-CSharp-firstpass.csproj +++ b/Game/Assembly-CSharp-firstpass.csproj @@ -49,9 +49,9 @@ - + C:/Program Files/Unity/Editor/Data/UnityExtensions/Unity/GUISystem/UnityEngine.UI.dll diff --git a/Game/Assembly-CSharp-vs.csproj b/Game/Assembly-CSharp-vs.csproj index 48d0c2e..c9086fb 100644 --- a/Game/Assembly-CSharp-vs.csproj +++ b/Game/Assembly-CSharp-vs.csproj @@ -54,12 +54,15 @@ + + - + + @@ -69,15 +72,16 @@ + - + C:/Users/Tibi/Google Drive/FacultateCY/$ Licenta/Game/Library/ScriptAssemblies/Assembly-UnityScript-firstpass.dll diff --git a/Game/Assembly-CSharp.csproj b/Game/Assembly-CSharp.csproj index a05e351..1be76cc 100644 --- a/Game/Assembly-CSharp.csproj +++ b/Game/Assembly-CSharp.csproj @@ -54,12 +54,15 @@ + + - + + @@ -69,15 +72,16 @@ + - + C:/Program Files/Unity/Editor/Data/UnityExtensions/Unity/GUISystem/UnityEngine.UI.dll diff --git a/Game/Assembly-UnityScript-firstpass-vs.unityproj b/Game/Assembly-UnityScript-firstpass-vs.unityproj index e22bc66..06ace76 100644 --- a/Game/Assembly-UnityScript-firstpass-vs.unityproj +++ b/Game/Assembly-UnityScript-firstpass-vs.unityproj @@ -51,9 +51,9 @@ - + C:/Program Files/Unity/Editor/Data/UnityExtensions/Unity/GUISystem/UnityEngine.UI.dll diff --git a/Game/Assembly-UnityScript-firstpass.unityproj b/Game/Assembly-UnityScript-firstpass.unityproj index e22bc66..06ace76 100644 --- a/Game/Assembly-UnityScript-firstpass.unityproj +++ b/Game/Assembly-UnityScript-firstpass.unityproj @@ -51,9 +51,9 @@ - + C:/Program Files/Unity/Editor/Data/UnityExtensions/Unity/GUISystem/UnityEngine.UI.dll diff --git a/Game/Assets/Data/Biomes/Grassland.xml b/Game/Assets/Data/Biomes/Grassland.xml index bd3864d..ef6b143 100644 --- a/Game/Assets/Data/Biomes/Grassland.xml +++ b/Game/Assets/Data/Biomes/Grassland.xml @@ -2,7 +2,7 @@ Grassland 100 - + diff --git a/Game/Assets/Data/Biomes/Mountain.xml.meta b/Game/Assets/Data/Biomes/Mountain.xml.meta index cfec85e..f010a54 100644 --- a/Game/Assets/Data/Biomes/Mountain.xml.meta +++ b/Game/Assets/Data/Biomes/Mountain.xml.meta @@ -1,4 +1,8 @@ fileFormatVersion: 2 -guid: 50d52558244dee444936bce1f2bd9e0a +guid: 04f446df0e76dde479e71408d721ffcc +timeCreated: 1432915085 +licenseType: Free TextScriptImporter: userData: + assetBundleName: + assetBundleVariant: diff --git a/Game/Assets/Scripts/Generator/CityGenerator.cs b/Game/Assets/Scripts/Generator/CityGenerator.cs index 4eefbcc..0f208db 100644 --- a/Game/Assets/Scripts/Generator/CityGenerator.cs +++ b/Game/Assets/Scripts/Generator/CityGenerator.cs @@ -35,8 +35,6 @@ namespace TransportGame.Generator RoadGenerator roadGenerator = new RoadGenerator(); roadGenerator.Generate(map); - Logger.DumpMap(map, "withroads.map"); - // Done return map; } diff --git a/Game/Assets/Scripts/Generator/PopulationCentersGenerator.cs b/Game/Assets/Scripts/Generator/PopulationCentersGenerator.cs index 215c3c2..1014435 100644 --- a/Game/Assets/Scripts/Generator/PopulationCentersGenerator.cs +++ b/Game/Assets/Scripts/Generator/PopulationCentersGenerator.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text; using TransportGame.Model; using TransportGame.Noise; -using UnityEngine; namespace TransportGame.Generator { @@ -14,9 +13,13 @@ namespace TransportGame.Generator public void Generate(Map map) { + // Generate range + float mp = (float)(map.Width * map.Height) / (1024 * 1024); // For 4k x 4k range should be around 900 + map.PopulationCenterRange = mp * 155f / 5f + 1490f / 3f; // For 2k x 2k range should be around 600 + // Generate a number of points - int maxPoints = map.Width * map.Height / (1024 * 512); - int points = random.Next(maxPoints / 4, maxPoints); + int maxPoints = 16 * (int) Math.Sqrt(mp); + int points = random.Next(maxPoints / 2, maxPoints); for (int i = 0; i < points; ++i) { @@ -24,12 +27,12 @@ namespace TransportGame.Generator do { - px = random.Next(map.Width); - py = random.Next(map.Height); + px = random.Next(map.Width / 6, 5 * map.Width / 6); + py = random.Next(map.Height / 6, 5 * map.Height / 6); } while (map.IsWater(px, py)); - map.PopulationCenters.Add(new Point(px, py)); + map.PopulationCenters.Add(new Vector2(px, py)); } } } diff --git a/Game/Assets/Scripts/Generator/RoadGenerator.cs b/Game/Assets/Scripts/Generator/RoadGenerator.cs index a8853dd..ea2632a 100644 --- a/Game/Assets/Scripts/Generator/RoadGenerator.cs +++ b/Game/Assets/Scripts/Generator/RoadGenerator.cs @@ -4,142 +4,383 @@ using System.Linq; using System.Text; using TransportGame.Model; using TransportGame.Model.Road; -using UnityEngine; +using TransportGame.Utils; +using Vector2 = TransportGame.Model.Vector2; namespace TransportGame.Generator { public class RoadGenerator { + class RoadGeneratorSegment + { + public RoadNode Terminal1; + public Vector2 Terminal2Pos; + public RoadNode Terminal2; + public bool Highway; + public int Time; + + public RoadGeneratorSegment(RoadNode term1, Vector2 term2pos, bool highway, int time = 0) + { + Terminal1 = term1; + Terminal2Pos = term2pos; + Highway = highway; + Time = time; + } + + public override string ToString() + { + string str = String.Format("(gensegment, {0}->", Terminal1); + + if (Terminal2 == null) + str += Terminal2Pos.ToString(); + else str += Terminal2.ToString(); + + str += ", "; + if (Highway) + str += "highway, "; + str += String.Format("time={0})", Time); + + return str; + } + } + + QuadTree qtree; + List queue; + System.Random random = new System.Random(); Map map; - const int segmentCountLimit = 100; - const int maxSegmentLength = 20; - const int minSegmentLength = 1; + const float HighwaySegmentLength = 60; + const float DefaultBranchPopulationTreshold = 0.12f; + const float DefaultBranchProbability = 0.2f; + const float DefaultSegmentLength = 24; + const float SteepnessLimit = 10; + const float SlopeLimit = (float)Math.PI / 6; + const float RoadSegmentAngleLimit = (float)Math.PI / 4; + const float RoadSnapDistance = 19; + const float MinNodeDistance = 12; + const int MaximumRandomStraightAngle = 45; // in degrees + const int MaximumBranchAngleVariation = 12; // in degrees + const float HighwayBranchPopulationTreshold = .4f; // 0..1 + const float HighwayBranchProbability = .01f; + const int HighwayBranchDelay = 3; + const int MaximumIntersectingRoads = 5; public RoadGenerator() { } - public void Generate(Map map) + public void Initialize(Map map) { this.map = map; map.RoadNetwork = new RoadNetwork(); + qtree = new QuadTree(0, 0, map.Width, map.Height); + queue = new List(); - Queue queue = new Queue(); - RoadNode first = map.RoadNetwork.CreateNode(); - queue.Enqueue(first); - - // Set starting point - if (map.PopulationCenters != null && map.PopulationCenters.Count > 0) + // Generate positions + Vector2 center = new Vector2(map.Width / 2, map.Height / 2); + int maxDistanceFromCenter = map.Width / 3; + + Vector2 p0, p1, p2; // p2 goes in opposite direction + + do { - first.X = map.PopulationCenters.First().X; - first.Y = map.PopulationCenters.First().Y; + // Generate point close to center of the map + float gen_x = random.Next(-maxDistanceFromCenter, maxDistanceFromCenter) + random.NextSingle(); + float gen_y = random.Next(-maxDistanceFromCenter, maxDistanceFromCenter) + random.NextSingle(); + + p0 = center + new Vector2(gen_x, gen_y); + + // Generate a random direction + Vector2 dir = Vector2.FromDegrees(random.Next(360)) * (HighwaySegmentLength / 2); + p1 = p0 + dir; + p2 = p0 - dir; + + } while (IsObstacle(p0) || IsObstacle(p1) || IsObstacle(p2)); + + // Logger.Info("Generated initial segment: {0} -> {1}", p0, p1); + // Logger.Info("Generated initial segment: {0} -> {1}", p0, p2); + + // Create root node + var node0 = map.RoadNetwork.CreateNode(p0); + qtree.Add(node0); + + // Logger.Info("Generated root node: {0}", node0); + + // Create & enqueue segments + queue.Add(new RoadGeneratorSegment(node0, p1, true)); + queue.Add(new RoadGeneratorSegment(node0, p2, true)); + } + + public void Step() + { + // Logger.Info(">>> BEGAN STEP <<<"); + + var segment = queue.OrderBy(x => x.Time).First(); + queue.Remove(segment); + + // Logger.Info("Dequeued segment {0}", segment); + + // Check local constraints + if (CheckLocalConstraints(segment)) + { + // Logger.Info("Local constraints check succeeded."); + RoadSegment createdSegment; + + // Finish to create segment + if (segment.Terminal2 != null) + createdSegment = map.RoadNetwork.CreateArticulationSegment(segment.Terminal1, segment.Terminal2); + + else + createdSegment = map.RoadNetwork.CreateArticulationSegment(segment.Terminal1, segment.Terminal2Pos); + + qtree.Add(createdSegment.Terminal2); + createdSegment.LanesTo1 = createdSegment.LanesTo2 = (segment.Highway) ? 3 : 1; + + // Use global goals to get new segments + foreach (var newSegment in GlobalGoals(createdSegment)) + { + newSegment.Time += segment.Time + 1; + queue.Add(newSegment); + // Logger.Info("Added segment to queue: {0}", newSegment); + } } else { - first.X = random.Next(map.Width); - first.Y = random.Next(map.Height); + // Logger.Info("Local constraints check failed!"); } + } - // Go through each node - for (int i = 0; i < segmentCountLimit && queue.Count > 0; i++) + private bool IsObstacle(Vector2 p) + { + return !map.IsInside(p.X, p.Y) || map.IsWater(p.X, p.Y) || map.GetSteepness(p.X, p.Y) > SteepnessLimit; + } + + public void Generate(Map map) + { + Initialize(map); + + int iterationCount = (map.Width * map.Height) / 512; + + for (int i = 0; i < iterationCount && queue.Count > 0; i++) + Step(); + } + + private IEnumerable GlobalGoals(RoadSegment segment) + { + Vector2 prevPos = segment.Terminal2.Position; + Vector2 dir = (segment.Terminal2.Position - segment.Terminal1.Position).Normalized; + bool highway = (segment.LanesTo1 >= 3); + bool highwayBranched = false; + + // Logger.Info("> Computing global goals. prevPos={0}, dir={1}, highway={2}", prevPos, dir, highway); + + // Going straight + Vector2 straight = prevPos + dir * ((highway) ? HighwaySegmentLength : DefaultSegmentLength); + float straightPopulation = map.GetPopulation(straight); + + // Logger.Info("> Straight={0} StraightPopulation={1}", straight, straightPopulation); + + // Highways... + if (highway) { - RoadNode node = queue.Dequeue(); + // Logger.Info("> Highway case:"); - // Produce solutions based on global goals - foreach (var next in GlobalGoals(node)) + Vector2 randomStraight = prevPos + HighwaySegmentLength * dir.RotateDeg(random.Next(-MaximumRandomStraightAngle, MaximumRandomStraightAngle)); + float randomPopulation = map.GetPopulation(randomStraight); + + // Logger.Info("> RandomStraight={0} RandomPopulation={1}", randomStraight, randomPopulation); + + if (randomPopulation > straightPopulation) { - if (CheckLocalConstraints(node, next)) + // Logger.Info("> Yielding random straight vector."); + yield return new RoadGeneratorSegment(segment.Terminal2, randomStraight, highway); + } + else + { + // Logger.Info("> Yielding straight vector."); + yield return new RoadGeneratorSegment(segment.Terminal2, straight, highway); + } + + // Branch highway + if (Math.Max(straightPopulation, randomPopulation) > HighwayBranchPopulationTreshold) + { + // Logger.Info("> Above treshold. Branching..."); + if (random.NextSingle() < HighwayBranchProbability) { - // Next is a temporary node - create a node on the road network - var other = map.RoadNetwork.CreateNode(); - other.X = next.X; - other.Y = next.Y; + Vector2 leftBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(-90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation)); + yield return new RoadGeneratorSegment(segment.Terminal2, leftBranch, highway, HighwayBranchDelay); - // Create a segment - var segment = map.RoadNetwork.CreateArticulationSegment(); + highwayBranched = true; + // Logger.Info("> Branch to the left: {0}", leftBranch); + } + if (random.NextSingle() < HighwayBranchProbability) + { + Vector2 rightBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation)); + yield return new RoadGeneratorSegment(segment.Terminal2, rightBranch, highway, HighwayBranchDelay); - // Assign IDs - segment.Terminal1Id = node.Id; - segment.Terminal2Id = other.Id; - node.ArticulationSegmentIds.Add(segment.Id); - other.ArticulationSegmentIds.Add(segment.Id); - - // Enqueue node - queue.Enqueue(other); + highwayBranched = true; + // Logger.Info("> Branch to the right: {0}", rightBranch); } } - } - } - public IEnumerable GlobalGoals(RoadNode node) - { - // Goal #1 - connect population centers - foreach (var popCenter in map.PopulationCenters.Skip(1)) + // Don't allow more branches + if (highwayBranched) + yield break; + } + + else if (random.NextSingle() < straightPopulation) { - // Get direction vector - float dx = popCenter.X - node.X; - float dy = popCenter.Y - node.Y; - - // Ignore if too close to population center - if (Math.Abs(dx) <= 1 && Math.Abs(dy) <= 1) - continue; - - // Calculate length of direction vector (we need to normalize it) - float dlen = Mathf.Sqrt(dx * dx + dy * dy); - - // Length of segment - int length = random.Next(minSegmentLength, maxSegmentLength); - - // Calculate coordinates - yield return new RoadNode() - { - X = node.X + length * dx / dlen, - Y = node.Y + length * dy / dlen - }; + // Logger.Info("> Not highway. Yielding straight vector."); + yield return new RoadGeneratorSegment(segment.Terminal2, straight, false); } - // Goal #2 - random segments depending on how populated is area - max 25% chance - if (random.NextDouble() < map.GetPopulation(Convert.ToInt32(node.X), Convert.ToInt32(node.Y)) * 4) + // Branch normal road + if (straightPopulation > DefaultBranchPopulationTreshold) { - // Generate direction vector - float dx = Convert.ToSingle(random.NextDouble()) * 2 - 1; - float dy = Convert.ToSingle(random.NextDouble()) * 2 - 1; - - int length = random.Next(minSegmentLength, maxSegmentLength); - - // Calculate coordinates - yield return new RoadNode() + // Logger.Info("Straight population above branch treshold. Branching..."); + if (random.NextSingle() < DefaultBranchProbability * straightPopulation) { - X = node.X + dx * length, - Y = node.Y + dy * length - }; - } - } + Vector2 leftBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(-90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation)); + yield return new RoadGeneratorSegment(segment.Terminal2, leftBranch, false, (highway) ? HighwayBranchDelay : 0); - public bool CheckLocalConstraints(RoadNode first, RoadNode second) - { - // Make sure point is inside map - if (!map.IsInside(Convert.ToInt32(second.X), Convert.ToInt32(second.Y))) - return false; - - // Cannot build on water - if (map.IsWater(Convert.ToInt32(second.X), Convert.ToInt32(second.Y))) - return false; - - // Check steepness - int mix = Convert.ToInt32(Math.Min(first.X, second.X)); - int max = Convert.ToInt32(Math.Max(first.X, second.X)); - int miy = Convert.ToInt32(Math.Min(first.Y, second.Y)); - int may = Convert.ToInt32(Math.Max(first.Y, second.Y)); - - for (int x = mix; x <= max; x++) - for (int y = miy; y <= may; y++) - { - if (map.GetSteepness(x, y) > 1) - return false; + // Logger.Info("> Branch to the left: {0}", leftBranch); } + if (random.NextSingle() < DefaultBranchProbability * straightPopulation) + { + Vector2 rightBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation)); + yield return new RoadGeneratorSegment(segment.Terminal2, rightBranch, false, (highway) ? HighwayBranchDelay : 0); + + // Logger.Info("> Branch to the right: {0}", rightBranch); + } + } + } + + private bool CheckLocalConstraints(RoadGeneratorSegment segment) + { + // Logger.Info("Checking local constraints..."); + + // Constraint #1: check for obstacles + if (IsObstacle(segment.Terminal2Pos)) + { + // Logger.Info("Obstacle."); + return false; + } + + // Constraint #2: slope + float segmentLength = (segment.Highway) ? HighwaySegmentLength : DefaultSegmentLength; + float levelDiff = map.GetHeight((int)segment.Terminal1.X, (int)segment.Terminal1.Y) - + map.GetHeight((int)segment.Terminal2Pos.X, (int)segment.Terminal2Pos.Y); + float sinSlope = Math.Abs(levelDiff) / segmentLength; + + // Logger.Info("Level difference is {0}, slope is (rads) {1}", levelDiff, Math.Asin(sinSlope)); + + if (Math.Asin(sinSlope) > SlopeLimit) + { + // Logger.Info("Slope too big!"); + return false; + } + + // Constraint #3: Number of intersecting roads + if (segment.Terminal1.ArticulationSegmentIds.Count > MaximumIntersectingRoads) + return false; + + // Constraint #4: intersections & snapping + Rectangle queryArea = new Rectangle( + Math.Min(segment.Terminal1.X, segment.Terminal2Pos.X) - 3 * HighwaySegmentLength, + Math.Min(segment.Terminal1.Y, segment.Terminal2Pos.Y) - 3 * HighwaySegmentLength, + Math.Max(segment.Terminal1.X, segment.Terminal2Pos.X) + 3 * HighwaySegmentLength, + Math.Max(segment.Terminal1.Y, segment.Terminal2Pos.Y) + 3 * HighwaySegmentLength); + + // Logger.Info("Searching area {0} for intersecting segments.", queryArea); + + IEnumerable segmentIds = Enumerable.Empty(); + + // Look for nearby segments + foreach (var node in qtree.Query(queryArea)) + { + // Logger.Info("Found node: {0}", node); + if (node == segment.Terminal1) + { + // Logger.Info("Node is originating node. Will ignore."); + continue; + } + + // Too close to another node in the area + if ((node.Position - segment.Terminal2Pos).LengthSq < MinNodeDistance * MinNodeDistance) + return false; + + segmentIds = segmentIds.Concat(node.ArticulationSegmentIds); + } + + // Filter & sort the segments by distance + segmentIds = segmentIds.Distinct().OrderBy(id => + { + var seg = map.RoadNetwork.ArticulationSegments[id]; + var line = new LineSegment(seg.Terminal1.Position, seg.Terminal2.Position); + return LineSegment.Distance(line, segment.Terminal2Pos); + }); + + foreach (var segmentId in segmentIds) + { + var other = map.RoadNetwork.ArticulationSegments[segmentId]; + var line1 = new LineSegment(segment.Terminal1.Position, segment.Terminal2Pos); + var line2 = new LineSegment(other.Terminal1.Position, other.Terminal2.Position); + + // Logger.Info("Found segment: {0}. Will test intersection between segments {0} and {1}", other, line1, line2); + + Vector2? inters = LineSegment.Intersect(line1, line2); + + // Case #1: there is an intersection with another segment. We cut the rest of the segment + if (inters.HasValue && inters.Value != segment.Terminal1.Position) + { + // Logger.Info("Found intersection point: {0}", inters); + + // Check angle between segments + float cos = Vector2.Dot((line1.P1 - line1.P0).Normalized, (line2.P1 - line2.P0).Normalized); + if (Math.Abs(Math.Acos(cos)) < RoadSegmentAngleLimit) + { + // Logger.Info("Angle between segments is too small ({0} rads)", Math.Abs(Math.Acos(cos))); + return false; + } + + // Split segment + var newNode = map.RoadNetwork.SplitArticulationSegment(other, inters.Value); + segment.Terminal2Pos = inters.Value; + segment.Terminal2 = newNode; + // Logger.Info("Performed split in point: {0}", newNode); + return true; + } + else // Logger.Info("Does not intersect."); + + // Case #2: no intersection, but the point is close enough to an existing intersection + if ((segment.Terminal2Pos - other.Terminal2.Position).LengthSq <= RoadSnapDistance * RoadSnapDistance) + { + // Check angle between intersecting segments + foreach (var intersSeg in other.Terminal2.ArticulationSegments) + { + float cos = Vector2.Dot((line1.P1 - line1.P0).Normalized, (intersSeg.Terminal2.Position - intersSeg.Terminal1.Position).Normalized); + if (Math.Abs(Math.Acos(cos)) < RoadSegmentAngleLimit) + { + // Logger.Info("Angle between segments is too small ({0} rads)", Math.Abs(Math.Acos(cos))); + return false; + } + } + + // Logger.Info("Point is close to existing intersection: {0}, will snap.", other.Terminal2); + segment.Terminal2Pos = other.Terminal2.Position; + segment.Terminal2 = other.Terminal2; + return true; + } + + //// TODO: Case #3: the point is close enough to an existing road segment + //float dist = LineSegment.Distance(line2, segment.Terminal2Pos); + //if (dist < RoadSnapDistance) + //{ + // float proj0 = (float)Math.Sqrt((line2.P0 - segment.Terminal2Pos).LengthSq - dist * dist); + // float percent = proj0 / + //} + } return true; } diff --git a/Game/Assets/Scripts/Model/Map.cs b/Game/Assets/Scripts/Model/Map.cs index 53e7407..7fa7123 100644 --- a/Game/Assets/Scripts/Model/Map.cs +++ b/Game/Assets/Scripts/Model/Map.cs @@ -73,7 +73,9 @@ namespace TransportGame.Model /// [XmlArray("populationCenters")] [XmlArrayItem("center")] - public List PopulationCenters { get; set; } + public List PopulationCenters { get; set; } + + public float PopulationCenterRange { get; set; } /// /// Gets or sets the articulation road network @@ -93,7 +95,7 @@ namespace TransportGame.Model /// public Map() { - PopulationCenters = new List(); + PopulationCenters = new List(); } /// @@ -104,7 +106,7 @@ namespace TransportGame.Model public Map(int width, int height) { heightmap = new float[width, height]; - PopulationCenters = new List(); + PopulationCenters = new List(); } #endregion @@ -137,9 +139,19 @@ namespace TransportGame.Model /// X /// Y /// - public bool IsWater(int x, int y) + public bool IsWater(float x, float y) { - return GetHeight(x, y) <= WaterLevel; + return GetHeight((int)x, (int)y) <= WaterLevel; + } + + /// + /// Returns true if specified cell is a water cell + /// + /// Position vector + /// + public bool IsWater(Vector2 p) + { + return IsWater(p.X, p.Y); } /// @@ -148,11 +160,21 @@ namespace TransportGame.Model /// X /// Y /// True if coordinates are inside the map - public bool IsInside(int x, int y) + public bool IsInside(float x, float y) { return x >= 0 && y >= 0 && x < Width && y < Height; } + /// + /// Returns true if given coordinates is inside the map + /// + /// Position vector + /// True if coordinates are inside the map + public bool IsInside(Vector2 p) + { + return IsInside(p.X, p.Y); + } + /// /// Gets steepness in specified point /// @@ -170,27 +192,56 @@ namespace TransportGame.Model return dx * dx + dy * dy; } + /// + /// Gets steepness in specified point + /// + /// X + /// Y + /// Steepness + public float GetSteepness(float x, float y) + { + return GetSteepness((int)x, (int)y); + } + + /// + /// Gets steepness in specified point + /// + /// Position + /// Steepness + public float GetSteepness(Vector2 pos) + { + return GetSteepness((int)pos.X, (int)pos.Y); + } + /// /// Gets population using terrain coordinates /// /// X /// Y /// Population - public float GetPopulation(int x, int y) + public float GetPopulation(float x, float y) { - const int maxDistance = 400; - float value = 0; + return GetPopulation(new Vector2(x, y)); + } + + /// + /// Gets population using terrain coordinates + /// + /// Position + /// Population + public float GetPopulation(Vector2 p) + { + float value = 0.05f; foreach (var point in PopulationCenters) { - int x1 = x - point.X; - int y1 = y - point.Y; + Vector2 diff = p - point; + float dist = diff.LengthSq; - int dist = x1 * x1 + y1 * y1; - if (dist < maxDistance * maxDistance) + if (dist < PopulationCenterRange * PopulationCenterRange) { - float influence = 1 - (float)dist / (float)(maxDistance * maxDistance); - influence = Mathf.Pow(influence, 3); // Ease + float influence = 1 - (float)dist / (float)(PopulationCenterRange * PopulationCenterRange); + influence = Mathf.Pow(influence, 6) * 0.7f; // Ease value = Mathf.Clamp01(value + influence); } } diff --git a/Game/Assets/Scripts/Model/Road/RoadNetwork.cs b/Game/Assets/Scripts/Model/Road/RoadNetwork.cs index ab9afe5..832aac0 100644 --- a/Game/Assets/Scripts/Model/Road/RoadNetwork.cs +++ b/Game/Assets/Scripts/Model/Road/RoadNetwork.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml.Serialization; +using TransportGame.Utils; namespace TransportGame.Model.Road { @@ -12,20 +13,19 @@ namespace TransportGame.Model.Road private int lastNodeId = -1, lastSegmentId = -1; /// - /// Gets or sets the road nodes + /// Gets the road nodes /// [XmlIgnore] public Dictionary Nodes { get; private set; } /// - /// Gets or sets the road segments for the articulation graph + /// Gets the road segments for the articulation graph /// [XmlIgnore] public Dictionary ArticulationSegments { get; private set; } - /// - /// Gets or sets the road segments for the intersection graph + /// Gets the road segments for the intersection graph /// [XmlIgnore] public Dictionary IntersectionSegments { get; private set; } @@ -44,7 +44,10 @@ namespace TransportGame.Model.Road { Nodes.Clear(); foreach (var node in value) + { + node.ParentNetwork = this; Nodes.Add(node.Id, node); + } } } @@ -62,7 +65,10 @@ namespace TransportGame.Model.Road { ArticulationSegments.Clear(); foreach (var segment in value) + { + segment.ParentNetwork = this; ArticulationSegments.Add(segment.Id, segment); + } } } @@ -80,7 +86,10 @@ namespace TransportGame.Model.Road { ArticulationSegments.Clear(); foreach (var segment in value) + { + segment.ParentNetwork = this; ArticulationSegments.Add(segment.Id, segment); + } } } @@ -97,8 +106,9 @@ namespace TransportGame.Model.Road /// /// Creates a node and returns it /// - /// Created node - public RoadNode CreateNode() + /// Position + /// + public RoadNode CreateNode(Vector2 pos) { // Skip IDs that already exist while (Nodes.ContainsKey(++lastNodeId)) ; @@ -107,18 +117,39 @@ namespace TransportGame.Model.Road RoadNode node = new RoadNode() { Id = lastNodeId, - ParentNetwork = this + ParentNetwork = this, + Position = pos }; Nodes.Add(node.Id, node); return node; } + /// + /// Creates a node and returns it + /// + /// Created node + public RoadNode CreateNode() + { + return CreateNode(Vector2.Zero); + } + /// /// Creates a segment and returns it /// /// Created segment public RoadSegment CreateArticulationSegment() + { + return CreateArticulationSegment(null, null); + } + + /// + /// Creates a segment and returns it + /// + /// First terminal + /// Second terminal + /// Created segment + public RoadSegment CreateArticulationSegment(RoadNode term1, RoadNode term2) { // Skip IDs that already exist while (ArticulationSegments.ContainsKey(++lastSegmentId)) ; @@ -127,13 +158,59 @@ namespace TransportGame.Model.Road RoadSegment segment = new RoadSegment() { Id = lastSegmentId, - ParentNetwork = this + ParentNetwork = this, + Terminal1 = term1, + Terminal2 = term2 }; + // Set links + if (term1 != null) + term1.ArticulationSegmentIds.Add(segment.Id); + + if (term2 != null) + term2.ArticulationSegmentIds.Add(segment.Id); + ArticulationSegments.Add(segment.Id, segment); return segment; } + /// + /// Creates an articulation segment + /// + /// First terminal + /// Position of second terminal + /// Road segment + public RoadSegment CreateArticulationSegment(RoadNode term1, Vector2 term2pos) + { + var term2 = CreateNode(term2pos); + return CreateArticulationSegment(term1, term2); + } + + /// + /// Creates an articulation segment + /// + /// Position of first terminal + /// Second terminal + /// Road segment + public RoadSegment CreateArticulationSegment(Vector2 term1pos, RoadNode term2) + { + var term1 = CreateNode(term1pos); + return CreateArticulationSegment(term1, term2); + } + + /// + /// Creates an articulation segment + /// + /// Position of first terminal + /// Position of second terminal + /// Road segment + public RoadSegment CreateArticulationSegment(Vector2 term1pos, Vector2 term2pos) + { + var term1 = CreateNode(term1pos); + var term2 = CreateNode(term2pos); + return CreateArticulationSegment(term1, term2); + } + /// /// Creates a segment and returns it /// @@ -153,5 +230,35 @@ namespace TransportGame.Model.Road IntersectionSegments.Add(segment.Id, segment); return segment; } + + /// + /// Splits an articulation segment in two segments + /// + /// + /// + /// Newly created road node + public RoadNode SplitArticulationSegment(RoadSegment segment, Vector2 point) + { + // Get current terminals + var term1 = segment.Terminal1; + var term2 = segment.Terminal2; + int l1 = segment.LanesTo1, l2 = segment.LanesTo2; + + // Create new terminal + var newTerm = CreateNode(point); + + // Delete exinsting segment + term1.ArticulationSegmentIds.Remove(segment.Id); + term2.ArticulationSegmentIds.Remove(segment.Id); + ArticulationSegments.Remove(segment.Id); + + // Create split segments + var seg1 = CreateArticulationSegment(term1, newTerm); + var seg2 = CreateArticulationSegment(newTerm, term2); + seg1.LanesTo1 = seg2.LanesTo1 = l1; + seg1.LanesTo2 = seg2.LanesTo2 = l2; + + return newTerm; + } } } diff --git a/Game/Assets/Scripts/Model/Road/RoadNode.cs b/Game/Assets/Scripts/Model/Road/RoadNode.cs index 9f15b14..4a9c2d3 100644 --- a/Game/Assets/Scripts/Model/Road/RoadNode.cs +++ b/Game/Assets/Scripts/Model/Road/RoadNode.cs @@ -10,7 +10,7 @@ namespace TransportGame.Model.Road /// Road node /// [XmlRoot("node")] - public class RoadNode + public class RoadNode : IPositionable { /// /// Gets or sets a unique identifier for this node @@ -63,7 +63,7 @@ namespace TransportGame.Model.Road } /// - /// Gets the adjacent setments + /// Gets the adjacent segments /// [XmlIgnore] public IEnumerable Segments @@ -82,5 +82,27 @@ namespace TransportGame.Model.Road ArticulationSegmentIds = new List(); IntersectionSegmentIds = new List(); } + + /// + /// Gets or sets the position + /// + [XmlIgnore] + public Vector2 Position + { + get + { + return new Vector2(X, Y); + } + set + { + X = value.X; + Y = value.Y; + } + } + + public override string ToString() + { + return string.Format("(node id={0}, {1})", Id, Position); + } } } diff --git a/Game/Assets/Scripts/Model/Road/RoadSegment.cs b/Game/Assets/Scripts/Model/Road/RoadSegment.cs index f92cc9b..a066e5d 100644 --- a/Game/Assets/Scripts/Model/Road/RoadSegment.cs +++ b/Game/Assets/Scripts/Model/Road/RoadSegment.cs @@ -44,11 +44,11 @@ namespace TransportGame.Model.Road { get { - return ParentNetwork.Nodes[Terminal1Id]; + return (Terminal1Id == -1) ? null : ParentNetwork.Nodes[Terminal1Id]; } set { - Terminal1Id = value.Id; + Terminal1Id = (value == null) ? -1 : value.Id; } } @@ -60,11 +60,11 @@ namespace TransportGame.Model.Road { get { - return ParentNetwork.Nodes[Terminal2Id]; + return (Terminal2Id == -1) ? null : ParentNetwork.Nodes[Terminal2Id]; } set { - Terminal2Id = value.Id; + Terminal2Id = (value == null) ? -1 : value.Id; } } @@ -79,7 +79,7 @@ namespace TransportGame.Model.Road /// [XmlAttribute("lanesTo1")] public int LanesTo1 { get; set; } - + /// /// Initializes road segment /// @@ -87,6 +87,13 @@ namespace TransportGame.Model.Road { LanesTo1 = 1; LanesTo2 = 1; + Terminal1Id = -1; + Terminal2Id = -1; + } + + public override string ToString() + { + return string.Format("(segment id={0}, {1}->{2})", Id, Terminal1, Terminal2); } } } diff --git a/Game/Assets/Scripts/Model/Vector2.cs b/Game/Assets/Scripts/Model/Vector2.cs index 710319c..7cdeac2 100644 --- a/Game/Assets/Scripts/Model/Vector2.cs +++ b/Game/Assets/Scripts/Model/Vector2.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Xml.Serialization; using UnityEngine; namespace TransportGame.Model @@ -21,12 +22,14 @@ namespace TransportGame.Model /// /// Gets the X component /// - public float X { get; private set; } + [XmlAttribute("x")] + public float X { get; set; } /// /// Gets the Y component /// - public float Y { get; private set; } + [XmlAttribute("y")] + public float Y { get; set; } /// /// Initializes a vector2 diff --git a/Game/Assets/Scripts/Unity/TerrainGeneratorScript.cs b/Game/Assets/Scripts/Unity/TerrainGeneratorScript.cs index 72baf19..373babd 100644 --- a/Game/Assets/Scripts/Unity/TerrainGeneratorScript.cs +++ b/Game/Assets/Scripts/Unity/TerrainGeneratorScript.cs @@ -39,10 +39,10 @@ public class TerrainGeneratorScript : MonoBehaviour }; water.triangles = new[] { 0, 1, 2, 2, 1, 3 }; water.uv = new[] { - new Vector2(0, 0), - new Vector2(0, 1), - new Vector2(1, 0), - new Vector2(1, 1) + new UnityEngine.Vector2(0, 0), + new UnityEngine.Vector2(0, 1), + new UnityEngine.Vector2(1, 0), + new UnityEngine.Vector2(1, 1) }; water.RecalculateNormals(); @@ -102,7 +102,27 @@ public class TerrainGeneratorScript : MonoBehaviour yield return null; // Set up textures - SetupSplatmaps(terrainData); + //SetupSplatmaps(terrainData); + + // -- DEBUG -- + + foreach (var center in map.PopulationCenters) + Debug.DrawLine(new Vector3(center.Y, 0, center.X), new Vector3(center.Y, map.Biome.Height, center.X), Color.yellow, 100000); + + // Debug - draw lines + if (map != null && map.RoadNetwork != null) + { + foreach (var segment in map.RoadNetwork.ArticulationSegments) + { + Color color = (segment.Value.LanesTo1 >= 3) ? Color.magenta : Color.red; + Debug.DrawLine(new Vector3(segment.Value.Terminal1.Y, map.Biome.Height / 2, segment.Value.Terminal1.X), new Vector3(segment.Value.Terminal2.Y, map.Biome.Height / 2, segment.Value.Terminal2.X), color, 10000); + } + + foreach (var node in map.RoadNetwork.Nodes) + { + Debug.DrawLine(new Vector3(node.Value.Y, map.Biome.Height / 2, node.Value.X), new Vector3(node.Value.Y, map.Biome.Height / 2 + 5, node.Value.X), Color.blue, 10000); + } + } } private void SetupSplatmaps(TerrainData terrainData) @@ -162,4 +182,18 @@ public class TerrainGeneratorScript : MonoBehaviour void Update() { } + + void OnGUI() + { + Event e = Event.current; + if (e.type == EventType.KeyDown) + { + if (e.keyCode == KeyCode.Home) + { + Logger.Warning("Writing to file..."); + Logger.DumpMap(map, "map.map"); + Logger.Warning("Wrote map to file."); + } + } + } } diff --git a/Game/Game-csharp.sln b/Game/Game-csharp.sln index c227275..f961dca 100644 --- a/Game/Game-csharp.sln +++ b/Game/Game-csharp.sln @@ -23,7 +23,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution + GlobalSection(MonoDevelopProperties) = preSolution StartupItem = Assembly-CSharp.csproj Policies = $0 $0.TextStylePolicy = $1 diff --git a/Game/Game.sln b/Game/Game.sln index b90b4af..4705d3c 100644 --- a/Game/Game.sln +++ b/Game/Game.sln @@ -29,7 +29,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution + GlobalSection(MonoDevelopProperties) = preSolution StartupItem = Assembly-CSharp.csproj Policies = $0 $0.TextStylePolicy = $1 diff --git a/Game/UnityVS.Game.CSharp.csproj b/Game/UnityVS.Game.CSharp.csproj index e2863ea..ae3020a 100644 --- a/Game/UnityVS.Game.CSharp.csproj +++ b/Game/UnityVS.Game.CSharp.csproj @@ -78,12 +78,15 @@ + + - + + @@ -93,6 +96,7 @@ + diff --git a/Tools/MapViewer/MapViewer/Storage/MapStorage.cs b/Tools/MapViewer/MapViewer/Storage/MapStorage.cs index e8c1e2d..55b223b 100644 --- a/Tools/MapViewer/MapViewer/Storage/MapStorage.cs +++ b/Tools/MapViewer/MapViewer/Storage/MapStorage.cs @@ -14,20 +14,7 @@ namespace TransportGame.MapViewer.Storage { public static Map Read(string file) { - Map map = SerializationHelper.DeserializeXml(file); - - // Fix road network issue - foreach (var pair in map.RoadNetwork.ArticulationSegments) - pair.Value.ParentNetwork = map.RoadNetwork; - - foreach (var pair in map.RoadNetwork.IntersectionSegments) - pair.Value.ParentNetwork = map.RoadNetwork; - - foreach (var pair in map.RoadNetwork.Nodes) - pair.Value.ParentNetwork = map.RoadNetwork; - - // Done - return map; + return SerializationHelper.DeserializeXml(file); } } }