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,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"hud.world": "LittleSim | seed {0} | population {1} | {2} FPS",
|
"hud.world": "LittleSim | seed {0} | {2} FPS",
|
||||||
"hud.terrain": "LittleSim | terrain | seed {0} | animals: {1}",
|
|
||||||
"hud.controls": "WASD — camera, wheel — zoom, ` — console ('help' lists commands)",
|
"hud.controls": "WASD — camera, wheel — zoom, ` — console ('help' lists commands)",
|
||||||
"hud.paused": "PAUSED",
|
"hud.paused": "PAUSED",
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"hud.world": "LittleSim | сид {0} | население {1} | {2} FPS",
|
"hud.world": "LittleSim | сид {0} | {2} FPS",
|
||||||
"hud.terrain": "LittleSim | террейн | сид {0} | животных: {1}",
|
|
||||||
"hud.controls": "WASD — камера, колесо — зум, ` — консоль ('help' — список команд)",
|
"hud.controls": "WASD — камера, колесо — зум, ` — консоль ('help' — список команд)",
|
||||||
"hud.paused": "ПАУЗА",
|
"hud.paused": "ПАУЗА",
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule engine updated: ef1111bcb6...53659450a6
@@ -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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Friflo.Engine.ECS;
|
using Friflo.Engine.ECS;
|
||||||
using LittleSim.App;
|
using LittleSim.App;
|
||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
@@ -13,6 +12,7 @@ using MrGameEng.Core;
|
|||||||
using MrGameEng.DevConsole;
|
using MrGameEng.DevConsole;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
using MrGameEng.Input;
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.Tilemaps;
|
||||||
using MrGameEng.UI;
|
using MrGameEng.UI;
|
||||||
using Myra.Graphics2D;
|
using Myra.Graphics2D;
|
||||||
using Myra.Graphics2D.UI;
|
using Myra.Graphics2D.UI;
|
||||||
@@ -20,10 +20,11 @@ using Myra.Graphics2D.UI;
|
|||||||
namespace LittleSim.Scenes;
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Мир LittleSim: рельеф, растения и жители целиком описаны дефами Core-мода. Сцена строится
|
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
|
||||||
/// из <see cref="WorldConfig"/> (размер/сид/население/сглаживание) и опционально восстанавливает
|
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
|
||||||
/// полное состояние симуляции из <see cref="WorldSave"/>. Поверх мира — HUD, полоса скорости
|
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
|
||||||
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc) и дев-консоль.
|
/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие
|
||||||
|
/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WorldScene : Scene
|
public sealed class WorldScene : Scene
|
||||||
{
|
{
|
||||||
@@ -41,7 +42,7 @@ public sealed class WorldScene : Scene
|
|||||||
public WorldScene(WorldConfig config)
|
public WorldScene(WorldConfig config)
|
||||||
: this(config, null) { }
|
: this(config, null) { }
|
||||||
|
|
||||||
/// <summary>Мир, восстановленный из сохранения (рельеф — из сида, жители — из снимка).</summary>
|
/// <summary>Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).</summary>
|
||||||
public WorldScene(WorldConfig config, WorldSave? save)
|
public WorldScene(WorldConfig config, WorldSave? save)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -63,47 +64,9 @@ public sealed class WorldScene : Scene
|
|||||||
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
||||||
);
|
);
|
||||||
GameLayers.EnsureRegistered(renderer);
|
GameLayers.EnsureRegistered(renderer);
|
||||||
|
this.UseTilemaps();
|
||||||
|
|
||||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
BuildTerrain(content, atlases, device, assets);
|
||||||
|
|
||||||
// Рельеф детерминирован сидом и параметром сглаживания — и для нового мира, и для загрузки.
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
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();
|
var console = this.UseDevConsole();
|
||||||
RegisterCommands(console, content, atlases);
|
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 GodCameraSystem(camera, input, console, () => _pause.IsOpen));
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new HudSystem(
|
new HudSystem(Context, content.Languages, hudLabel, "hud.world", _config.Seed, 0)
|
||||||
Context,
|
|
||||||
content.Languages,
|
|
||||||
hudLabel,
|
|
||||||
"hud.world",
|
|
||||||
_config.Seed,
|
|
||||||
_config.Population
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
|
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,
|
GameContent content,
|
||||||
ModAtlases atlases,
|
ModAtlases atlases,
|
||||||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||||||
float[,] heights,
|
AssetManager assets
|
||||||
Random random
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
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 x = 0; x < _config.Width; x++)
|
||||||
{
|
{
|
||||||
for (var y = 0; y < _config.Height; y++)
|
for (var y = 0; y < _config.Height; y++)
|
||||||
{
|
{
|
||||||
if (content.Terrains.Classify(heights[x, y]).IsLand)
|
grid[x, y] = tileByDef[content.Terrains.Classify(heights[x, y])];
|
||||||
{
|
|
||||||
landCells.Add(new Point(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(
|
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
||||||
@@ -312,6 +214,7 @@ public sealed class WorldScene : Scene
|
|||||||
|
|
||||||
private string SaveWorld()
|
private string SaveWorld()
|
||||||
{
|
{
|
||||||
|
// Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида.
|
||||||
var save = new WorldSave
|
var save = new WorldSave
|
||||||
{
|
{
|
||||||
Name = _config.Name,
|
Name = _config.Name,
|
||||||
@@ -324,38 +227,8 @@ public sealed class WorldScene : Scene
|
|||||||
ElapsedSeconds = Context.Clock.TotalTime,
|
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);
|
new SaveStore().Write(save);
|
||||||
Log.Info($"World '{_config.Name}' saved ({save.Pawns.Count} pawns)");
|
Log.Info($"World '{_config.Name}' saved");
|
||||||
return _config.Name;
|
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(
|
console.Register(
|
||||||
"menu",
|
"menu",
|
||||||
"menu — return to the main menu",
|
"menu — return to the main menu",
|
||||||
|
|||||||
@@ -1,77 +1,33 @@
|
|||||||
|
using MrGameEng.WorldGen;
|
||||||
|
|
||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Детерминированная генерация рельефа: случайное поле, сглаженное несколькими проходами.
|
/// Игровой адаптер процедурной генерации: превращает параметры мира в настройки движкового
|
||||||
/// Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой симуляции.
|
/// <see cref="HeightmapGenerator"/> (шум Перлина → fBm → island-маска) и отдаёт нормализованную
|
||||||
/// Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>).
|
/// карту высот. Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой
|
||||||
|
/// симуляции. Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WorldGenerator
|
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 scale = Math.Clamp(detail, 1, 8);
|
||||||
var heights = new float[width, height];
|
var settings = new HeightmapSettings
|
||||||
for (var x = 0; x < width; x++)
|
|
||||||
{
|
{
|
||||||
for (var y = 0; y < height; y++)
|
Seed = seed,
|
||||||
{
|
Frequency = 6f / scale, // больше «масштаба деталей» → ниже частота → крупнее континенты
|
||||||
heights[x, y] = random.NextSingle();
|
Octaves = 5,
|
||||||
}
|
Persistence = 0.5f,
|
||||||
}
|
Lacunarity = 2f,
|
||||||
|
IslandStrength = 0.55f, // края мира уходят под воду — мир выглядит как континент
|
||||||
// Сглаживание усреднением окрестности 3×3 — дёшево и даёт связные "континенты".
|
IslandPower = 3f,
|
||||||
// Больше проходов — крупнее и плавнее "континенты".
|
};
|
||||||
for (var pass = 0; pass < passes; pass++)
|
return new HeightmapGenerator(settings).Generate(width, height);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user