diff --git a/Game/Assets/Scripts/Generator/CityGenerator.cs b/Game/Assets/Scripts/Generator/CityGenerator.cs index 7144195..4eefbcc 100644 --- a/Game/Assets/Scripts/Generator/CityGenerator.cs +++ b/Game/Assets/Scripts/Generator/CityGenerator.cs @@ -32,9 +32,10 @@ namespace TransportGame.Generator populationGen.Generate(map); // Generate roads - // TODO: Generate roads + RoadGenerator roadGenerator = new RoadGenerator(); + roadGenerator.Generate(map); - Logger.DumpMap(map, "withpop.map"); + Logger.DumpMap(map, "withroads.map"); // Done return map; diff --git a/Game/Assets/Scripts/Generator/RoadGenerator.cs b/Game/Assets/Scripts/Generator/RoadGenerator.cs index 0a13f20..a8853dd 100644 --- a/Game/Assets/Scripts/Generator/RoadGenerator.cs +++ b/Game/Assets/Scripts/Generator/RoadGenerator.cs @@ -3,17 +3,145 @@ using System.Collections.Generic; using System.Linq; using System.Text; using TransportGame.Model; +using TransportGame.Model.Road; +using UnityEngine; -namespace Assets.Scripts.Generator +namespace TransportGame.Generator { public class RoadGenerator { + System.Random random = new System.Random(); + Map map; + + const int segmentCountLimit = 100; + const int maxSegmentLength = 20; + const int minSegmentLength = 1; + public RoadGenerator() { } public void Generate(Map map) { + this.map = map; + map.RoadNetwork = new RoadNetwork(); + + Queue queue = new Queue(); + RoadNode first = map.RoadNetwork.CreateNode(); + queue.Enqueue(first); + + // Set starting point + if (map.PopulationCenters != null && map.PopulationCenters.Count > 0) + { + first.X = map.PopulationCenters.First().X; + first.Y = map.PopulationCenters.First().Y; + } + else + { + first.X = random.Next(map.Width); + first.Y = random.Next(map.Height); + } + + // Go through each node + for (int i = 0; i < segmentCountLimit && queue.Count > 0; i++) + { + RoadNode node = queue.Dequeue(); + + // Produce solutions based on global goals + foreach (var next in GlobalGoals(node)) + { + if (CheckLocalConstraints(node, next)) + { + // 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; + + // Create a segment + var segment = map.RoadNetwork.CreateArticulationSegment(); + + // 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); + } + } + } + } + + public IEnumerable GlobalGoals(RoadNode node) + { + // Goal #1 - connect population centers + foreach (var popCenter in map.PopulationCenters.Skip(1)) + { + // 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 + }; + } + + // 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) + { + // 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() + { + X = node.X + dx * length, + Y = node.Y + dy * length + }; + } + } + + 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; + } + + return true; } } } diff --git a/Game/Assets/Scripts/Model/Road/RoadNetwork.cs b/Game/Assets/Scripts/Model/Road/RoadNetwork.cs index 2c309ba..ab9afe5 100644 --- a/Game/Assets/Scripts/Model/Road/RoadNetwork.cs +++ b/Game/Assets/Scripts/Model/Road/RoadNetwork.cs @@ -84,6 +84,16 @@ namespace TransportGame.Model.Road } } + /// + /// Initializes the road network + /// + public RoadNetwork() + { + Nodes = new Dictionary(); + ArticulationSegments = new Dictionary(); + IntersectionSegments = new Dictionary(); + } + /// /// Creates a node and returns it /// diff --git a/Game/Game-csharp.sln b/Game/Game-csharp.sln index dd92830..c227275 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 bd3a86c..b90b4af 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/Tools/MapViewer/MapViewer/Business/MapRenderer.cs b/Tools/MapViewer/MapViewer/Business/MapRenderer.cs index 8723d2b..e2b7317 100644 --- a/Tools/MapViewer/MapViewer/Business/MapRenderer.cs +++ b/Tools/MapViewer/MapViewer/Business/MapRenderer.cs @@ -13,7 +13,8 @@ namespace TransportGame.MapViewer None = 0, Elevation = 1, Population = 2, - All = Elevation | Population + RoadArticulations = 3, + All = Elevation | Population | RoadArticulations }; /// @@ -57,9 +58,13 @@ namespace TransportGame.MapViewer // Create texture on which to draw Bitmap24 bitmap = new Bitmap24(Convert.ToInt32(map.Width * Scale), Convert.ToInt32(map.Height * Scale)); - // First layer - cells + // Elevation, population DrawPixels(bitmap, map, (layers & Layers.Elevation) > 0, (layers & Layers.Population) > 0); + // Roads + if ((layers & Layers.RoadArticulations) > 0) + DrawRoads(bitmap, map); + // Done return bitmap; } @@ -83,6 +88,12 @@ namespace TransportGame.MapViewer int mapX = Convert.ToInt32(x / Scale); int mapY = Convert.ToInt32(y / Scale); + if (mapX >= map.Width) + mapX = map.Width - 1; + + if (mapY >= map.Height) + mapY = map.Height - 1; + // Draw water if (map.IsWater(mapX, mapY)) bitmap[x, y] = WaterColor; @@ -108,5 +119,18 @@ namespace TransportGame.MapViewer } } } + + private void DrawRoads(Bitmap24 bitmap, Map map) + { + // Draw road segments + foreach (var pair in map.RoadNetwork.ArticulationSegments) + { + bitmap.DrawLine(Convert.ToInt32(pair.Value.Terminal1.X * Scale), + Convert.ToInt32(pair.Value.Terminal1.Y * Scale), + Convert.ToInt32(pair.Value.Terminal2.X * Scale), + Convert.ToInt32(pair.Value.Terminal2.Y * Scale), + Colors.Black); + } + } } } diff --git a/Tools/MapViewer/MapViewer/MainWindow.xaml b/Tools/MapViewer/MapViewer/MainWindow.xaml index 82f29c9..2601e1c 100644 --- a/Tools/MapViewer/MapViewer/MainWindow.xaml +++ b/Tools/MapViewer/MapViewer/MainWindow.xaml @@ -62,6 +62,7 @@ (All) Elevation Population + Roads (articulation map)