Rework world scene into a textured tile terrain on engine WorldGen
Bump the engine submodule to 5365945 (MrGameEng.WorldGen) and drive terrain from it: WorldGenerator is now a thin adapter over the engine's seed-based Perlin/fBm/island HeightmapGenerator instead of the old random+smoothing pass. Strip WorldScene down to terrain + camera: render the world as a single Tilemap entity using per-biome surface textures from the atlas (water falls back to a tinted tile), and remove pawns, plants, AI systems, population and save-restore for now. Delete the TerrainScene demo (its tile approach now lives in WorldScene) along with its console command and the hud.terrain/population localization strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9433db6212
commit
923644dd19
@@ -1,180 +0,0 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Sim;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Assets;
|
||||
using MrGameEng.Collisions;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Input;
|
||||
using MrGameEng.Tilemaps;
|
||||
using MrGameEng.UI;
|
||||
using Myra.Graphics2D.UI;
|
||||
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
/// <summary>
|
||||
/// Демо-сцена тайлового террейна: карта-Tilemap по дефам рельефа (поверхность из атласа
|
||||
/// или тонированный тайл), растительность по скаттеру дефов (деревья — с коллайдером
|
||||
/// ствола), животные — дефы вида "animal" с расталкиванием. Команды: "regen", "world".
|
||||
/// </summary>
|
||||
public sealed class TerrainScene : Scene
|
||||
{
|
||||
private const int TerrainWidth = 44;
|
||||
private const int TerrainHeight = 28;
|
||||
private const int CellSize = 24;
|
||||
private const int Population = 14;
|
||||
|
||||
private static readonly RectF TerrainBounds = new(
|
||||
0f,
|
||||
0f,
|
||||
TerrainWidth * CellSize,
|
||||
TerrainHeight * CellSize
|
||||
);
|
||||
|
||||
private const uint AnimalLayer = 0b01;
|
||||
private const uint ObstacleLayer = 0b10;
|
||||
|
||||
private readonly int _seed;
|
||||
|
||||
public TerrainScene(int seed) => _seed = seed;
|
||||
|
||||
protected override void OnLoad()
|
||||
{
|
||||
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
||||
var content = Context.Services.Get<GameContent>();
|
||||
var atlases = Context.Services.Get<ModAtlases>();
|
||||
var device = Context.GraphicsDevice;
|
||||
var input = this.UseInput();
|
||||
var renderer = this.UseRenderer2D(
|
||||
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
||||
);
|
||||
GameLayers.EnsureRegistered(renderer);
|
||||
this.UseTilemaps();
|
||||
|
||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||
|
||||
// Тайлсет из дефов: у дефа есть поверхность — текстура из атласа, нет — тонированный тайл.
|
||||
var tiles = new TileSet();
|
||||
var tileByDef = new Dictionary<TerrainDef, ushort>();
|
||||
foreach (var terrain in content.Terrains.All)
|
||||
{
|
||||
tileByDef[terrain] = terrain.Surface is { } surface
|
||||
? tiles.Add(atlases.GetRegion(device, surface))
|
||||
: tiles.Add(white, terrain.Tint);
|
||||
}
|
||||
|
||||
// Террейн строится кодом: высоты по сиду → деф клетки → id тайла; поверх — скаттер.
|
||||
var random = new Random(_seed);
|
||||
var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed);
|
||||
var grid = new TileGrid(TerrainWidth, TerrainHeight);
|
||||
var landCells = new List<Point>();
|
||||
for (var x = 0; x < TerrainWidth; x++)
|
||||
{
|
||||
for (var y = 0; y < TerrainHeight; y++)
|
||||
{
|
||||
var terrain = content.Terrains.Classify(heights[x, y]);
|
||||
grid[x, y] = tileByDef[terrain];
|
||||
var cell = new Point(x, y);
|
||||
if (terrain.IsLand)
|
||||
{
|
||||
landCells.Add(cell);
|
||||
}
|
||||
|
||||
ScatterSpawner.Spawn(
|
||||
this,
|
||||
content,
|
||||
atlases,
|
||||
device,
|
||||
terrain,
|
||||
cell,
|
||||
CellSize,
|
||||
random,
|
||||
obstacleLayer: ObstacleLayer,
|
||||
collidesWith: AnimalLayer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||||
|
||||
// Животные: дефы вида "animal", не наслаиваются (коллайдеры + расталкивание).
|
||||
var animals = content.Defs.All<PawnDef>().Where(p => p.Kind == PawnDef.KindAnimal).ToList();
|
||||
for (var i = 0; i < Population && landCells.Count > 0 && animals.Count > 0; i++)
|
||||
{
|
||||
var cell = landCells[random.Next(landCells.Count)];
|
||||
var animal = animals[random.Next(animals.Count)];
|
||||
var body = atlases.GetRegion(device, animal.Texture);
|
||||
var sprite = new Sprite(body, GameLayers.Beings);
|
||||
sprite.CenterOrigin();
|
||||
var collider = Collider.Circle(animal.SizeCells * CellSize * 0.32f);
|
||||
collider.Layer = AnimalLayer;
|
||||
collider.CollidesWith = AnimalLayer | ObstacleLayer;
|
||||
Store.CreateEntity(
|
||||
new Transform2D(
|
||||
new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
|
||||
scale: new Vector2(CellSize * animal.SizeCells / body.Width)
|
||||
),
|
||||
sprite,
|
||||
new Wander(),
|
||||
collider
|
||||
);
|
||||
}
|
||||
|
||||
var camera = Store.CreateEntity(
|
||||
new Camera(TerrainBounds.Center, zoom: 1.6f, bounds: TerrainBounds)
|
||||
);
|
||||
|
||||
var desktop = this.UseUI();
|
||||
var hudLabel = new Label { Left = 10, Top = 8 };
|
||||
desktop.Root = hudLabel;
|
||||
|
||||
var console = this.UseDevConsole();
|
||||
ContentCommands.Register(console, content, atlases);
|
||||
console.Register(
|
||||
"regen",
|
||||
"regen [seed] — regenerate the terrain",
|
||||
(c, args) =>
|
||||
{
|
||||
if (Context.Scenes.IsTransitioning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||||
c.WriteLine($"regenerating terrain, seed {seed}");
|
||||
Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
|
||||
}
|
||||
);
|
||||
console.Register(
|
||||
"world",
|
||||
"world [seed] — switch to the world scene",
|
||||
(c, args) =>
|
||||
{
|
||||
if (Context.Scenes.IsTransitioning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seed = args.Length > 0 ? int.Parse(args[0]) : _seed;
|
||||
Context.Scenes.Switch(
|
||||
new WorldScene(new App.WorldConfig { Name = "World", Seed = seed }),
|
||||
Transition.Fade(0.6f)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
UpdateSystems.Add(new WanderSystem(_seed, TerrainBounds));
|
||||
var collisions = this.UseCollisions(cellSize: CellSize * 2f); // после движения
|
||||
UpdateSystems.Add(new SeparationSystem(collisions));
|
||||
UpdateSystems.Add(new GodCameraSystem(camera, input, console));
|
||||
UpdateSystems.Add(
|
||||
new HudSystem(Context, content.Languages, hudLabel, "hud.terrain", _seed, Population)
|
||||
);
|
||||
|
||||
Log.Info(
|
||||
$"Terrain generated: seed {_seed}, {TerrainWidth}x{TerrainHeight} cells, {Population} animals"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.App;
|
||||
using LittleSim.Content;
|
||||
@@ -13,6 +12,7 @@ using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Input;
|
||||
using MrGameEng.Tilemaps;
|
||||
using MrGameEng.UI;
|
||||
using Myra.Graphics2D;
|
||||
using Myra.Graphics2D.UI;
|
||||
@@ -20,10 +20,11 @@ using Myra.Graphics2D.UI;
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
/// <summary>
|
||||
/// Мир LittleSim: рельеф, растения и жители целиком описаны дефами Core-мода. Сцена строится
|
||||
/// из <see cref="WorldConfig"/> (размер/сид/население/сглаживание) и опционально восстанавливает
|
||||
/// полное состояние симуляции из <see cref="WorldSave"/>. Поверх мира — HUD, полоса скорости
|
||||
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc) и дев-консоль.
|
||||
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
|
||||
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
|
||||
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
|
||||
/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие
|
||||
/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн.
|
||||
/// </summary>
|
||||
public sealed class WorldScene : Scene
|
||||
{
|
||||
@@ -41,7 +42,7 @@ public sealed class WorldScene : Scene
|
||||
public WorldScene(WorldConfig config)
|
||||
: this(config, null) { }
|
||||
|
||||
/// <summary>Мир, восстановленный из сохранения (рельеф — из сида, жители — из снимка).</summary>
|
||||
/// <summary>Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).</summary>
|
||||
public WorldScene(WorldConfig config, WorldSave? save)
|
||||
{
|
||||
_config = config;
|
||||
@@ -63,47 +64,9 @@ public sealed class WorldScene : Scene
|
||||
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
||||
);
|
||||
GameLayers.EnsureRegistered(renderer);
|
||||
this.UseTilemaps();
|
||||
|
||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||
|
||||
// Рельеф детерминирован сидом и параметром сглаживания — и для нового мира, и для загрузки.
|
||||
var random = new Random(_config.Seed);
|
||||
var heights = WorldGenerator.GenerateHeights(
|
||||
_config.Width,
|
||||
_config.Height,
|
||||
_config.Seed,
|
||||
_config.SmoothPasses
|
||||
);
|
||||
for (var x = 0; x < _config.Width; x++)
|
||||
{
|
||||
for (var y = 0; y < _config.Height; y++)
|
||||
{
|
||||
var terrain = content.Terrains.Classify(heights[x, y]);
|
||||
Store.CreateEntity(
|
||||
Transform2D.At(new Vector2(x * CellSize, y * CellSize)),
|
||||
new Sprite(white) { Color = terrain.Tint }
|
||||
);
|
||||
ScatterSpawner.Spawn(
|
||||
this,
|
||||
content,
|
||||
atlases,
|
||||
device,
|
||||
terrain,
|
||||
new Point(x, y),
|
||||
CellSize,
|
||||
random
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (_save is null)
|
||||
{
|
||||
SpawnPopulation(content, atlases, device, heights, random);
|
||||
}
|
||||
else
|
||||
{
|
||||
RestorePopulation(content, atlases, device);
|
||||
}
|
||||
BuildTerrain(content, atlases, device, assets);
|
||||
|
||||
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
||||
|
||||
@@ -124,21 +87,9 @@ public sealed class WorldScene : Scene
|
||||
var console = this.UseDevConsole();
|
||||
RegisterCommands(console, content, atlases);
|
||||
|
||||
var decisions = new PawnDecisionSystem();
|
||||
UpdateSystems.Add(decisions);
|
||||
UpdateSystems.Add(new WanderSystem(_config.Seed, _bounds));
|
||||
UpdateSystems.Add(new PawnNeedsSystem());
|
||||
UpdateSystems.Add(new PawnAppearanceSystem());
|
||||
UpdateSystems.Add(new GodCameraSystem(camera, input, console, () => _pause.IsOpen));
|
||||
UpdateSystems.Add(
|
||||
new HudSystem(
|
||||
Context,
|
||||
content.Languages,
|
||||
hudLabel,
|
||||
"hud.world",
|
||||
_config.Seed,
|
||||
_config.Population
|
||||
)
|
||||
new HudSystem(Context, content.Languages, hudLabel, "hud.world", _config.Seed, 0)
|
||||
);
|
||||
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
|
||||
|
||||
@@ -148,95 +99,46 @@ public sealed class WorldScene : Scene
|
||||
);
|
||||
}
|
||||
|
||||
private void SpawnPopulation(
|
||||
/// <summary>
|
||||
/// Строит одну сущность-<see cref="Tilemap"/>: тайлсет из дефов рельефа (поверхность из
|
||||
/// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации.
|
||||
/// </summary>
|
||||
private void BuildTerrain(
|
||||
GameContent content,
|
||||
ModAtlases atlases,
|
||||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||||
float[,] heights,
|
||||
Random random
|
||||
AssetManager assets
|
||||
)
|
||||
{
|
||||
var landCells = new List<Point>();
|
||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||
|
||||
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
|
||||
var tiles = new TileSet();
|
||||
var tileByDef = new Dictionary<TerrainDef, ushort>();
|
||||
foreach (var terrain in content.Terrains.All)
|
||||
{
|
||||
tileByDef[terrain] = terrain.Surface is { } surface
|
||||
? tiles.Add(atlases.GetRegion(device, surface))
|
||||
: tiles.Add(white, terrain.Tint);
|
||||
}
|
||||
|
||||
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
|
||||
var heights = WorldGenerator.Generate(
|
||||
_config.Width,
|
||||
_config.Height,
|
||||
_config.Seed,
|
||||
_config.SmoothPasses
|
||||
);
|
||||
var grid = new TileGrid(_config.Width, _config.Height);
|
||||
for (var x = 0; x < _config.Width; x++)
|
||||
{
|
||||
for (var y = 0; y < _config.Height; y++)
|
||||
{
|
||||
if (content.Terrains.Classify(heights[x, y]).IsLand)
|
||||
{
|
||||
landCells.Add(new Point(x, y));
|
||||
}
|
||||
grid[x, y] = tileByDef[content.Terrains.Classify(heights[x, y])];
|
||||
}
|
||||
}
|
||||
|
||||
var beings = content.Defs.All<PawnDef>().Where(p => p.Kind == PawnDef.KindBeing).ToList();
|
||||
for (var i = 0; i < _config.Population && landCells.Count > 0 && beings.Count > 0; i++)
|
||||
{
|
||||
var cell = landCells[random.Next(landCells.Count)];
|
||||
var being = beings[random.Next(beings.Count)];
|
||||
CreatePawn(
|
||||
atlases,
|
||||
device,
|
||||
being,
|
||||
new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
|
||||
energy: 0.5f + random.NextSingle() * 0.5f,
|
||||
action: PawnAction.Wander,
|
||||
decideIn: random.NextSingle() * 0.75f,
|
||||
direction: Vector2.Zero,
|
||||
changeIn: 0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestorePopulation(
|
||||
GameContent content,
|
||||
ModAtlases atlases,
|
||||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device
|
||||
)
|
||||
{
|
||||
foreach (var pawn in _save!.Pawns)
|
||||
{
|
||||
if (!content.Defs.TryGet<PawnDef>(pawn.DefName, out var being))
|
||||
{
|
||||
continue; // деф пропал (мод убрали) — пропускаем жителя
|
||||
}
|
||||
|
||||
CreatePawn(
|
||||
atlases,
|
||||
device,
|
||||
being,
|
||||
new Vector2(pawn.X, pawn.Y),
|
||||
energy: pawn.Energy,
|
||||
action: (PawnAction)pawn.Action,
|
||||
decideIn: pawn.DecideIn,
|
||||
direction: new Vector2(pawn.DirX, pawn.DirY),
|
||||
changeIn: pawn.ChangeIn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreatePawn(
|
||||
ModAtlases atlases,
|
||||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||||
PawnDef being,
|
||||
Vector2 position,
|
||||
float energy,
|
||||
PawnAction action,
|
||||
float decideIn,
|
||||
Vector2 direction,
|
||||
float changeIn
|
||||
)
|
||||
{
|
||||
var body = atlases.GetRegion(device, being.Texture);
|
||||
var sprite = new Sprite(body, GameLayers.Beings);
|
||||
sprite.CenterOrigin();
|
||||
Store.CreateEntity(
|
||||
new Transform2D(position, scale: new Vector2(CellSize * being.SizeCells / body.Width)),
|
||||
sprite,
|
||||
new Wander { Direction = direction, ChangeIn = changeIn },
|
||||
new PawnNeeds { Energy = energy },
|
||||
new PawnBrain { Action = action, DecideIn = decideIn },
|
||||
new PawnId { DefName = being.DefName }
|
||||
);
|
||||
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||||
}
|
||||
|
||||
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
||||
@@ -312,6 +214,7 @@ public sealed class WorldScene : Scene
|
||||
|
||||
private string SaveWorld()
|
||||
{
|
||||
// Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида.
|
||||
var save = new WorldSave
|
||||
{
|
||||
Name = _config.Name,
|
||||
@@ -324,38 +227,8 @@ public sealed class WorldScene : Scene
|
||||
ElapsedSeconds = Context.Clock.TotalTime,
|
||||
};
|
||||
|
||||
Store
|
||||
.Query<PawnId, Transform2D, Wander, PawnNeeds, PawnBrain>()
|
||||
.ForEachEntity(
|
||||
(
|
||||
ref PawnId id,
|
||||
ref Transform2D transform,
|
||||
ref Wander wander,
|
||||
ref PawnNeeds needs,
|
||||
ref PawnBrain brain,
|
||||
Entity _
|
||||
) =>
|
||||
{
|
||||
save.Pawns.Add(
|
||||
new PawnSave
|
||||
{
|
||||
DefName = id.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
Scale = transform.Scale.X,
|
||||
Energy = needs.Energy,
|
||||
Action = (int)brain.Action,
|
||||
DecideIn = brain.DecideIn,
|
||||
DirX = wander.Direction.X,
|
||||
DirY = wander.Direction.Y,
|
||||
ChangeIn = wander.ChangeIn,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
new SaveStore().Write(save);
|
||||
Log.Info($"World '{_config.Name}' saved ({save.Pawns.Count} pawns)");
|
||||
Log.Info($"World '{_config.Name}' saved");
|
||||
return _config.Name;
|
||||
}
|
||||
|
||||
@@ -389,20 +262,6 @@ public sealed class WorldScene : Scene
|
||||
);
|
||||
}
|
||||
);
|
||||
console.Register(
|
||||
"terrain",
|
||||
"terrain [seed] — switch to the tile terrain demo scene",
|
||||
(c, args) =>
|
||||
{
|
||||
if (Context.Scenes.IsTransitioning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seed = args.Length > 0 ? int.Parse(args[0]) : _config.Seed;
|
||||
Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
|
||||
}
|
||||
);
|
||||
console.Register(
|
||||
"menu",
|
||||
"menu — return to the main menu",
|
||||
|
||||
@@ -1,77 +1,33 @@
|
||||
using MrGameEng.WorldGen;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Детерминированная генерация рельефа: случайное поле, сглаженное несколькими проходами.
|
||||
/// Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой симуляции.
|
||||
/// Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>).
|
||||
/// Игровой адаптер процедурной генерации: превращает параметры мира в настройки движкового
|
||||
/// <see cref="HeightmapGenerator"/> (шум Перлина → fBm → island-маска) и отдаёт нормализованную
|
||||
/// карту высот. Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой
|
||||
/// симуляции. Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>).
|
||||
/// </summary>
|
||||
public static class WorldGenerator
|
||||
{
|
||||
public static float[,] GenerateHeights(int width, int height, int seed, int passes = 4)
|
||||
/// <summary>
|
||||
/// Строит нормализованную карту высот <paramref name="width"/>×<paramref name="height"/> из
|
||||
/// <paramref name="seed"/>. <paramref name="detail"/> (1..8, из меню «масштаб деталей»/сейва)
|
||||
/// задаёт размер «континентов»: больше — крупнее и плавнее суша (ниже частота шума).
|
||||
/// </summary>
|
||||
public static Heightmap Generate(int width, int height, int seed, int detail = 4)
|
||||
{
|
||||
var random = new Random(seed);
|
||||
var heights = new float[width, height];
|
||||
for (var x = 0; x < width; x++)
|
||||
var scale = Math.Clamp(detail, 1, 8);
|
||||
var settings = new HeightmapSettings
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
heights[x, y] = random.NextSingle();
|
||||
}
|
||||
}
|
||||
|
||||
// Сглаживание усреднением окрестности 3×3 — дёшево и даёт связные "континенты".
|
||||
// Больше проходов — крупнее и плавнее "континенты".
|
||||
for (var pass = 0; pass < passes; pass++)
|
||||
{
|
||||
var smoothed = new float[width, height];
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
var sum = 0f;
|
||||
var count = 0;
|
||||
for (var dx = -1; dx <= 1; dx++)
|
||||
{
|
||||
for (var dy = -1; dy <= 1; dy++)
|
||||
{
|
||||
var nx = x + dx;
|
||||
var ny = y + dy;
|
||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
|
||||
{
|
||||
sum += heights[nx, ny];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
smoothed[x, y] = sum / count;
|
||||
}
|
||||
}
|
||||
|
||||
heights = smoothed;
|
||||
}
|
||||
|
||||
Normalize(heights, width, height);
|
||||
return heights;
|
||||
}
|
||||
|
||||
private static void Normalize(float[,] heights, int width, int height)
|
||||
{
|
||||
var min = float.MaxValue;
|
||||
var max = float.MinValue;
|
||||
foreach (var value in heights)
|
||||
{
|
||||
min = Math.Min(min, value);
|
||||
max = Math.Max(max, value);
|
||||
}
|
||||
|
||||
var range = Math.Max(0.0001f, max - min);
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
heights[x, y] = (heights[x, y] - min) / range;
|
||||
}
|
||||
}
|
||||
Seed = seed,
|
||||
Frequency = 6f / scale, // больше «масштаба деталей» → ниже частота → крупнее континенты
|
||||
Octaves = 5,
|
||||
Persistence = 0.5f,
|
||||
Lacunarity = 2f,
|
||||
IslandStrength = 0.55f, // края мира уходят под воду — мир выглядит как континент
|
||||
IslandPower = 3f,
|
||||
};
|
||||
return new HeightmapGenerator(settings).Generate(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user