/// <summary> /// Sets the decoration for a specific cell /// </summary> /// <param name="cell">The cell to set for</param> /// <param name="cellMappedData">The cell's mapped data</param> /// <param name="mesh">The decoration mesh</param> private void SetCellDecoration(Cell cell, CellMappedData cellMappedData, string decoration) { if (cellMappedData.Decoration.Key == decoration) { return; } // Free old decoration if (cellMappedData.Decoration.Key != null) { this.decorations[cellMappedData.Decoration.Key].FreeIndex(cellMappedData.Decoration.Value); } // Set new decoration if (decoration != null && decoration.Length > 0) { var index = this.decorations[decoration].GetFreeIndex(); cellMappedData.Decoration = new KeyValuePair <string, int>(decoration, index); this.decorations[decoration].SetPosition(index, new Vector3(TopographyHelper.HexToWorld(cell.Coordinates), (float)cell.Elevation)); } else { cellMappedData.Decoration = new KeyValuePair <string, int>(null, 0); } }
public void GenerateWorld(int radius) { double statLowestPoint = double.MaxValue; double statHighestMountain = double.MinValue; int statSeaTiles = 0; Perlin perlin = new Perlin(5); float boundsX = TopographyHelper.HexToWorld(new Point(radius, 0)).X; float boundsY = TopographyHelper.HexToWorld(new Point(0, radius)).Y; var perlinZ = random.NextDouble(); scene.WorldMap = new Map(radius); for (int p = -radius; p <= radius; p++) { for (int q = -radius; q <= radius; q++) { var currentPoint = new Point(p, q); var cell = scene.WorldMap.GetCell(p, q); if (cell == null) { continue; } // TODO: Remove Magic Numbers, put them in WorldSettings // Set cell neighbors cell.Neighbours = scene.WorldMap.GetNeighbours(currentPoint); // Set cell height var worldPoint = TopographyHelper.HexToWorld(currentPoint); var perlinPoint = (worldPoint + new Vector2(boundsX, boundsY)) * 0.01f; var elevation = perlin.OctavePerlin(perlinPoint.X, perlinPoint.Y, perlinZ, 4, 0.7); // Flat valleys elevation = Math.Pow(elevation, scene.Settings.ElevationPower); // Actually multiply to max height elevation *= scene.Settings.ElevationMultiplier; // Give cells closer to the edge of the map a negative bias. This means the entire map will be surrounded by oceans. var distanceFromEdge = (radius - TopographyHelper.Distance(new Point(0, 0), currentPoint)) / radius; if (distanceFromEdge < 0 || distanceFromEdge > 1) { throw new Exception("Value out of range, check math"); } cell.Elevation = elevation * Math.Pow(distanceFromEdge, scene.Settings.EdgeDistancePower); // Update stats if (cell.Elevation > statHighestMountain) { statHighestMountain = cell.Elevation; } if (cell.Elevation < statLowestPoint) { // Now all data has been set, calculate the modifiers statLowestPoint = cell.Elevation; } // Set wetness by rain var rain = perlin.OctavePerlin(perlinPoint.X, perlinPoint.Y, perlinZ * 100, 2, 0.7) * scene.Settings.RainMultiplier; cell.Wetness = rain; } } // Creates oceans by flood filling from map edge FloodFillOceans(); // Do final touchups and modifier calculation // TODO: This can contain NULL cells, this is bad var cells = scene.WorldMap.Cells; foreach (var cell in cells) { if (cell == null) { continue; } // Remove single-cell 'seas' and islands if (cell.IsWater) { bool canBeWater = false; foreach (var n in cell.Neighbours) { if (n.IsWater) { canBeWater = true; break; } } cell.IsWater = canBeWater; } else { bool canBeLand = false; foreach (var n in cell.Neighbours) { if (!n.IsWater) { canBeLand = true; break; } } cell.IsWater = !canBeLand; } // Calculate temperature var distanceFromEdge = (radius - TopographyHelper.Distance(new Point(0, 0), cell.Coordinates)) / radius; var temperature = MathHelper.Lerp(scene.Settings.PoleTemperature, scene.Settings.EquatorTemperature, (float)Math.Pow(distanceFromEdge, scene.Settings.TemperatureCurvePower)); var relElevation = (float)cell.Elevation - scene.Settings.SeaLevel; if (!cell.IsWater) { // Ocean tiles should have temperature at sea level only, otherwise use altitude multiplier temperature += relElevation * scene.Settings.ElevationTemperatureMultiplier; } cell.Temperature = temperature; // Set values if (cell.IsWater == true) { statSeaTiles++; cell.FoodMod = random.NextDouble() + random.Next(1, 2); cell.ResourceMod = random.NextDouble() + random.Next(1, 2); cell.MaxHousing = 0; } else { // Now all data has been set, calculate the modifiers cell.FoodMod = random.NextDouble() + random.Next(1, 5); cell.ResourceMod = random.NextDouble() + random.Next(1, 5); cell.WealthMod = random.NextDouble() + random.Next(1, 5); cell.MaxHousing = random.Next(0, 1250) + random.Next(0, 1250) + random.Next(0, 1250) + random.Next(0, 1250); } // Set biome AssignBiome(cell); } //SimulateWaterflow(); // Log stats Debug.WriteLine("Stats for World Generation"); Debug.WriteLine("Highest Point: " + statHighestMountain); Debug.WriteLine("Lowest Point: " + statLowestPoint); Debug.WriteLine("Ocean Cells: " + statSeaTiles); Debug.WriteLine("Land Cells: " + (scene.WorldMap.CellCount - statSeaTiles)); Debug.WriteLine("Water coverage:" + ((float)statSeaTiles / scene.WorldMap.CellCount * 100).ToString("##.##") + "%"); }
/// <summary> /// Generates terrain meshes, cell and biome data from a scene. Can be put in a task. /// </summary> /// <param name="scene">The scene to generate from</param> /// <returns>Results</returns> private MeshGenerationResult GenerateMeshesFromScene(Scene scene) { // Results var renderableMeshes = new List <RenderableMesh>(); var rawMeshes = new List <Mesh>(); var cellData = new Dictionary <Cell, CellMappedData>(); // Stopwatch used for performance logging Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); // Create world meshes var hexCombiner = new MeshCombiner(); int range = scene.WorldMap.Radius; for (int p = -range; p <= range; p++) { for (int q = -range; q <= range; q++) { var cell = scene.WorldMap.GetCell(p, q); // Due to the hexagonal shape we might be out of bounds if (cell == null) { continue; } var worldPoint = TopographyHelper.HexToWorld(new Point(p, q)); var position = new Vector3(worldPoint, (float)cell.Elevation); var color = GetCellColor(cell); Mesh mesh = biomeMappedData[cell.Biome.Name].HexMesh; // Ensure sea is actually level if (cell.IsWater && cell.Elevation < scene.Settings.SeaLevel) { position.Z = scene.Settings.SeaLevel; } // Add it to the combiner, using the cell's location as a tag so we can later // get the vertexes of this hex inside the larger mesh int meshIndex = hexCombiner.Add(new MeshInstance { mesh = mesh, matrix = Matrix.CreateTranslation(position), tag = new Point(p, q), color = color, useColor = true, }); // Register partial cell mapped data to be used later var cellMappedData = new CellMappedData { MeshIndex = meshIndex }; cellData[cell] = cellMappedData; } } // Combine meshes var meshList = hexCombiner.GetCombinedMeshes(); for (int i = 0; i < meshList.Count; i++) { var renderable = new RenderableMesh(orbis.GraphicsDevice, meshList[i]); renderableMeshes.Add(renderable); } // Finish cell mapped data foreach (var cell in cellData) { var mesh = meshList[cell.Value.MeshIndex]; cell.Value.VertexIndexes = mesh.TagIndexMap[cell.Key.Coordinates]; } stopwatch.Stop(); Debug.WriteLine("Generated " + renderableMeshes.Count + " meshes in " + stopwatch.ElapsedMilliseconds + " ms"); return(new MeshGenerationResult { cellData = cellData, rawMeshes = rawMeshes, renderableMeshes = renderableMeshes }); }
public override void Update(GameTime gameTime) { // Not worth updating if we don't have a scene if (renderedScene == null) { return; } #region Mesh and data updating // Check if new meshes have been generated by task if (meshTask != null) { if (meshTask.Status == TaskStatus.Canceled) { Debug.WriteLine("Mesh task was cancelled, shouldn't ever happen, call programmer."); meshTask = null; } else if (meshTask.Status == TaskStatus.Faulted) { Debug.WriteLine("Mesh task crashed due to unhandled exception, call programmer."); meshTask = null; } else if (meshTask.Status == TaskStatus.RanToCompletion) { // Just in case this isn't the first map generated we must remove all cell decorations and existing terrain hex meshes // Terrain meshes must be disposed since they won't be used again foreach (var cellMesh in this.cellMeshes) { cellMesh.Dispose(); } foreach (var decorationPool in this.decorations) { decorationPool.Value.Clear(); } // Cell decorations are in cellMappedData, so they will be overwritten. Their meshes will be reused so MUST NOT be disposed // Update main data sets var meshData = meshTask.Result; this.cellMappedData = meshData.cellData; this.cellMeshes = meshData.renderableMeshes; // Set cell decorations, this can lock up the application for a while var stopwatch = new Stopwatch(); stopwatch.Start(); defaultDecorationQueue = new Queue <KeyValuePair <Cell, CellMappedData> >(); foreach (var cell in this.cellMappedData) { defaultDecorationQueue.Enqueue(cell); } meshTask = null; stopwatch.Stop(); Debug.WriteLine("Took " + stopwatch.Elapsed.TotalMilliseconds + "ms to set decorations"); } } // Spread default decoration setting over several frames if (defaultDecorationQueue.Count > 0) { var stopwatch = new Stopwatch(); stopwatch.Start(); do { var cell = defaultDecorationQueue.Dequeue(); var biomeData = biomeMappedData[cell.Key.Biome.Name]; if (biomeData.DefaultDecoration != null && random.NextDouble() < MathHelper.Clamp(biomeData.DefaultDensity, 0, DecorationDensityCap)) { SetCellDecoration(cell.Key, cell.Value, biomeData.DefaultDecoration); } } while (defaultDecorationQueue.Count > 0 && stopwatch.Elapsed.TotalMilliseconds <= this.MaxUpdateTime); // Check which decorations need to be updated this frame var set = new HashSet <RenderableMesh>(); foreach (var dec in this.decorations) { dec.Value.Update(set); } foreach (var mesh in set) { meshUpdateQueue.Enqueue(mesh); } stopwatch.Stop(); } // Update some vertex buffers if we have to without impact framerate too much if (meshUpdateQueue.Count > 0) { var stopwatch = new Stopwatch(); stopwatch.Start(); // Use do-while to ensure we update at least one mesh per frame do { var mesh = meshUpdateQueue.Dequeue(); mesh.UpdateVertexBuffer(); } while(meshUpdateQueue.Count > 0 && stopwatch.Elapsed.TotalMilliseconds <= this.MaxUpdateTime); stopwatch.Stop(); } #endregion // Camera movement var camMoveDelta = Vector3.Zero; float movementSpeed = 100 * (float)gameTime.ElapsedGameTime.TotalSeconds * (distance / 10); float rotationSpeed = 100 * (float)gameTime.ElapsedGameTime.TotalSeconds; float zoomSpeed = 100 * (float)gameTime.ElapsedGameTime.TotalSeconds; #region Cam Input if (orbis.Input.IsKeyHeld(Keys.LeftShift)) { movementSpeed /= 5; rotationSpeed /= 5; zoomSpeed /= 5; } if (orbis.Input.IsKeyHeld(Keys.LeftControl)) { movementSpeed *= 5; rotationSpeed *= 5; zoomSpeed *= 5; } if (orbis.Input.IsKeyHeld(Keys.Up)) { angle -= rotationSpeed; } if (orbis.Input.IsKeyHeld(Keys.Down)) { angle += rotationSpeed; } if (orbis.Input.IsKeyHeld(Keys.Left)) { rotation -= rotationSpeed; } if (orbis.Input.IsKeyHeld(Keys.Right)) { rotation += rotationSpeed; } if (orbis.Input.IsKeyHeld(Keys.OemPlus)) { distance -= zoomSpeed; } if (orbis.Input.IsKeyHeld(Keys.OemMinus)) { distance += zoomSpeed; } // Mouse Camera movement if (orbis.Input.IsMouseHold(Engine.MouseButton.Right)) { //Zooming in/out with the mousewheel if (orbis.Input.MouseScroll() != 0) { distance -= orbis.Input.MouseScroll() / 10; } //Rotating the Camera if the mouse moved if (orbis.Input.MouseMove() != Point.Zero) { rotation += orbis.Input.MouseMove().X / 2; angle += orbis.Input.MouseMove().Y / 2; } } if (orbis.Input.IsKeyHeld(Keys.W)) { camMoveDelta.Y += movementSpeed * 0.07f; } if (orbis.Input.IsKeyHeld(Keys.A)) { camMoveDelta.X -= movementSpeed * 0.07f; } if (orbis.Input.IsKeyHeld(Keys.S)) { camMoveDelta.Y -= movementSpeed * 0.07f; } if (orbis.Input.IsKeyHeld(Keys.D)) { camMoveDelta.X += movementSpeed * 0.07f; } #endregion // Clamp to > -90, otherwise camera can do a flip angle = MathHelper.Clamp(angle, -89.9f, -5); distance = MathHelper.Clamp(distance, 1, 4000); camera.LookTarget = camera.LookTarget + Vector3.Transform(camMoveDelta, Matrix.CreateRotationZ(MathHelper.ToRadians(rotation))); // Update highlighted cell, snap Camera elevation to it if we can var camTile = TopographyHelper.RoundWorldToHex(new Vector2(camera.LookTarget.X, camera.LookTarget.Y)); HighlightedCell = renderedScene.WorldMap.GetCell(camTile.X, camTile.Y); if (HighlightedCell != null) { var pos = camera.LookTarget; pos.Z = HighlightedCell.IsWater ? renderedScene.Settings.SeaLevel : (float)HighlightedCell.Elevation; camera.LookTarget = pos; for (int i = 0; i < tileHighlightMesh.VertexData.Length; i++) { tileHighlightMesh.VertexData[i].Color = HighlightedCell.Owner != null ? HighlightedCell.Owner.Color : new Color(230, 232, 237); } tileHighlightMesh.UpdateVertexBuffer(); // Draw some cell debug data orbis.DrawDebugLine("Current cell: " + HighlightedCell.Coordinates); orbis.DrawDebugLine("Owner: " + (HighlightedCell.Owner != null ? HighlightedCell.Owner.Name : "Nobody")); orbis.DrawDebugLine("Biome: " + HighlightedCell.Biome.Name); orbis.DrawDebugLine("Temperature: " + HighlightedCell.Temperature.ToString("#.#")); orbis.DrawDebugLine("Elevation: " + ((HighlightedCell.Elevation - renderedScene.Settings.SeaLevel) * 450).ToString("#.#")); orbis.DrawDebugLine("Population: " + HighlightedCell.population); orbis.DrawDebugLine("Food: " + HighlightedCell.food.ToString("#.#")); } // Move camera back from its target var camMatrix = Matrix.CreateTranslation(0, -distance, 0) * Matrix.CreateRotationX(MathHelper.ToRadians(angle)) * Matrix.CreateRotationZ(MathHelper.ToRadians(rotation)); camera.Position = Vector3.Transform(Vector3.Zero, camMatrix) + camera.LookTarget; // Recalculate renderinstances renderInstances = new List <RenderInstance>(); foreach (var mesh in this.cellMeshes) { renderInstances.Add(new RenderInstance { mesh = mesh, matrix = Matrix.Identity }); } foreach (var decoration in this.decorations) { renderInstances.AddRange(decoration.Value.GetActiveInstances()); } base.Update(gameTime); }