riversong code showcase

This commit is contained in:
Daniele Marotta
2026-05-21 15:52:18 +02:00
commit 4c9eea1c02
462 changed files with 23406 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class CritterHerdsGeneratorOperation : IWorldGeneratorOperation
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IGameDatabase _gameDatabase;
[InjectService]
private IAgentFactory _agentFactory;
public UniTask Execute(World world)
{
var critters = _gameDatabase.OfType<CritterDefinition>();
foreach (var eligibleCenter in world.CritterHerdsState.EligibleCenters)
{
var critter = critters[Random.Range(0, critters.Count)];
var herd = _entityCollection.CreateAndAdd<CritterHerd>();
herd.CritterDefinition = critter;
herd.Center = eligibleCenter;
herd.MaxCritterCount = Random.Range(critter.MinCritterCount, critter.MaxCritterCount + 1);
_agentFactory.InitializeAgentSource(herd);
}
return UniTask.CompletedTask;
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using Random = UnityEngine.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class FreeProductStacksGeneratorOperation : IWorldGeneratorOperation
{
[InjectService]
private IProductStackFactory _productStackFactory;
[InjectService]
private GameConfig _config;
public async UniTask Execute(World world)
{
var config = _config.WorldGen.ProductStacks;
if (config.EligibleProducts.Count == 0) return;
var eligibleProducts = new List<ProductDefinition>(config.EligibleProducts.Count);
foreach (var assetRef in config.EligibleProducts) eligibleProducts.Add(await assetRef.LoadAssetAsync());
var closed = new HashSet<int2>();
var candidateTiles = new List<int2>();
foreach (var resource in world.RawResources) GetCandidateTilesAround(world, resource.Tile, closed, candidateTiles);
foreach (var tile in candidateTiles)
{
if (world.BlockMap.IsBlocked(tile, BlockReason.InvalidElevation)) continue;
if (Random.value > config.Chance) continue;
var product = eligibleProducts[Random.Range(0, eligibleProducts.Count)];
_productStackFactory.Create(tile, product, config.ProductAmount);
}
}
private static void GetCandidateTilesAround(World world, int2 tile, HashSet<int2> closed, List<int2> candidateTiles)
{
const int radius = 1;
for (var x = -radius; x <= radius; x++)
for (var y = -radius; y <= radius; y++)
{
var p = tile + new int2(x, y);
if (!closed.Add(p) || !world.Contains(p) || world.BlockMap.IsBlocked(p)) continue;
candidateTiles.Add(p);
}
}
}
}

View File

@@ -0,0 +1,125 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
using Random = Unity.Mathematics.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class MapDataInitializationOperation : IWorldGeneratorOperation
{
private const int HeightStep = 16;
[InjectService]
private GameConfig _config;
public async UniTask Execute(World world)
{
var mapTexture = await GetRandomMapTextureAsync(world.Seed);
var worldSize = new int2(mapTexture.width, mapTexture.height);
world.Size = worldSize;
world.Heightmap = new WorldHeightmap(worldSize);
world.BlockMap = new BlockMap(worldSize);
world.Fertility = new FertilityMap(worldSize);
world.WaterMap = new WaterMap(worldSize);
world.EntityIdMap = new EntityIdMap(worldSize);
world.RoadNetwork = new RoadNetwork(worldSize);
ApplyMapTexture(world, mapTexture);
await InitializeWaterMapAsync(world);
}
private async UniTask<Texture2D> GetRandomMapTextureAsync(int seed)
{
var config = _config.WorldGen;
var mapIndex = new Random((uint)seed).NextInt(config.MapTextures.Count);
var mapTexture = await config.MapTextures[mapIndex].LoadAssetAsync<Texture2D>();
return mapTexture;
}
private void ApplyMapTexture(World world, Texture2D mapTexture)
{
var mapData = mapTexture.GetPixelData<Color32>(0);
var blockMapData = world.BlockMap.GetNativeArray();
var fertilityData = world.Fertility.GetNativeArray();
for (var tileIndex = 0; tileIndex < mapData.Length; tileIndex++)
{
var color = mapData[tileIndex];
var h = color.r / HeightStep;
world.Heightmap.SetValue(tileIndex, h);
if (h != _config.GeneralSettings.BaseElevation)
{
blockMapData[tileIndex] |= BlockReason.InvalidElevation;
continue;
}
var fertility = (float)color.g / byte.MaxValue;
fertilityData[tileIndex] = new FertilityMapValue
{
CurrentFertility = fertility,
MaxFertility = fertility,
LockId = Entity.InvalidId
};
if (color.b > 0)
{
var stride = world.Size.x;
var tile = new int2(tileIndex % stride, tileIndex / stride);
world.CritterHerdsState.EligibleCenters.Add(tile);
}
}
}
private async UniTask InitializeWaterMapAsync(World world)
{
const int MaxWaterDistance = 3;
var budget = new AsyncBudget(world.Size.x * 8);
var open = new Queue<int2>();
foreach (var tile in TileRange.From(0, world.Size - 1))
{
if (BelowWater(world, tile))
open.Enqueue(tile);
else
world.WaterMap.SetValue(tile, int.MaxValue);
await budget.TickAsync();
}
while (open.Count > 0)
{
var current = open.Dequeue();
var currentDistance = world.WaterMap.GetValue(current);
if (currentDistance >= MaxWaterDistance) continue;
foreach (var direction in DirectionVectors.Directions8)
{
var other = current + direction;
if (!world.Contains(other)) continue;
var distance = BelowWater(world, other) ? 0 : currentDistance + 1;
if (distance >= world.WaterMap.GetValue(other)) continue;
world.WaterMap.SetValue(other, distance);
if (distance < MaxWaterDistance) open.Enqueue(other);
}
await budget.TickAsync();
}
}
private bool BelowWater(World world, int2 tile)
{
return world.Heightmap.GetValue(tile) < _config.GeneralSettings.BaseElevation;
}
}
}

View File

@@ -0,0 +1,127 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using Random = Unity.Mathematics.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class StoneResourcesGeneratorOperation : IWorldGeneratorOperation
{
[InjectService]
private GameConfig _config;
[InjectService]
private ITileSpace _tileSpace;
public async UniTask Execute(World world)
{
return; // Temporarily disabled
var blockMap = world.BlockMap;
var resourcesState = world.RawResources;
var config = _config.WorldGen.Stone;
var stoneDefinition = await config.StoneDefinition.LoadAssetAsync<ResourceNodeDefinition>();
var random = new Random((uint)world.Seed);
var count = random.NextInt(config.Count.x, config.Count.y + 1);
var candidates = new List<(int2, int2, Directions)>();
foreach (var tile in TileRange.From(1, world.Size - 2))
if (ValidateCandidate(world, tile, out var side, out var orientation))
candidates.Add((tile, side, orientation));
var current = new List<int2>(count);
while (candidates.Count > 0 && current.Count < count)
{
var i = random.NextInt(candidates.Count);
var (tile, side, orientation) = candidates[i];
candidates[i] = candidates[^1];
candidates.RemoveAt(candidates.Count - 1);
if (TooCloseToOtherResources(tile, current, config.Spacing)) continue;
current.Add(tile);
var position = _tileSpace.TileToWorld(tile);
resourcesState.AddResourceNode(
new ResourceNode
{
Id = 1 + resourcesState.Count,
DefinitionId = stoneDefinition.RuntimeId,
Position = position,
Tile = tile,
Orientation = orientation,
Elevation = _config.GeneralSettings.BaseElevation
});
blockMap.AddReason(tile, BlockReason.RawResource);
blockMap.AddReason(tile + side, BlockReason.RawResource);
blockMap.AddReason(tile - side, BlockReason.RawResource);
}
}
private bool ValidateCandidate(World world, int2 tile, out int2 side, out Directions orientation)
{
side = default;
orientation = default;
return world.Heightmap.GetValue(tile) == _config.GeneralSettings.BaseElevation &&
!world.BlockMap.IsBlocked(tile) &&
NextToCliff(world, tile, out side, out orientation);
}
private bool NextToCliff(World world, int2 tile, out int2 side, out Directions orientation)
{
side = new int2(0, 1);
if (NextToCliff(world, tile, new int2(1, 0), side))
{
orientation = Directions.East;
return true;
}
if (NextToCliff(world, tile, new int2(-1, 0), side))
{
orientation = Directions.West;
return true;
}
side = new int2(1, 0);
if (NextToCliff(world, tile, new int2(0, 1), side))
{
orientation = Directions.North;
return true;
}
if (NextToCliff(world, tile, new int2(0, -1), side))
{
orientation = Directions.South;
return true;
}
orientation = default;
return false;
}
private bool NextToCliff(World world, int2 bottomTile, int2 direction, int2 side)
{
var topTile = bottomTile + direction;
var baseElevation = _config.GeneralSettings.BaseElevation;
return world.Heightmap.GetValue(bottomTile + side) == baseElevation &&
world.Heightmap.GetValue(bottomTile - side) == baseElevation &&
world.Heightmap.GetValue(topTile) > baseElevation &&
world.Heightmap.GetValue(topTile + side) > baseElevation &&
world.Heightmap.GetValue(topTile - side) > baseElevation;
}
private static bool TooCloseToOtherResources(int2 tile, List<int2> alreadyPlaced, int spacing)
{
foreach (var other in alreadyPlaced)
if (TileMath.StepCount(tile, other) < spacing)
return true;
return false;
}
}
}

View File

@@ -0,0 +1,83 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TerrainGeneratorOperation : IWorldGeneratorOperation
{
[InjectService]
private GameConfig _config;
[InjectService]
private IScene _scene;
[InjectService]
private WorldRenderingState _renderingState;
public async UniTask Execute(World world)
{
var generators = new List<ChunkMeshGenerator>
{
new TerrainChunkMeshGenerator(world, _config),
new GrassChunkMeshGenerator(world, _config),
new CropsChunkMeshGenerator(world, _config)
};
var chunkSize = _config.WorldGen.ChunkSize;
var horizontalResolution = Mathf.CeilToInt((float)world.Size.x / chunkSize);
var verticalResolution = Mathf.CeilToInt((float)world.Size.y / chunkSize);
for (var x = 0; x < horizontalResolution; x++)
for (var y = 0; y < verticalResolution; y++)
{
var chunk = CreateChunk(x, y);
foreach (var generator in generators) generator.InitializeChunk(chunk);
_renderingState.AddTerrainChunk(chunk);
}
await UniTask.NextFrame();
var batch = new List<TerrainChunk>(_config.WorldGen.ChunkGenerationBatchCount);
var count = 0;
while (count < _renderingState.TerrainChunks.Count)
{
batch.Clear();
for (var i = 0; i < _config.WorldGen.ChunkGenerationBatchCount; i++)
{
batch.Add(_renderingState.TerrainChunks[count]);
if (++count >= _renderingState.TerrainChunks.Count) break;
}
foreach (var generator in generators) generator.BeginGeneratingChunks(batch);
foreach (var generator in generators) await generator.Wait();
foreach (var generator in generators)
{
generator.EndGeneratingChunks();
await UniTask.NextFrame();
}
}
}
private TerrainChunk CreateChunk(int x, int y)
{
var chunk = new TerrainChunk
{
Coords = new int2(x, y),
Root = new GameObject($"Chunk_{x}_{y}")
};
chunk.Root.transform.SetParent(_scene.SceneFolders.TerrainChunks);
chunk.GrassLODs = new TerrainChunk.RenderData[_config.Terrain.GrassLODs.Length];
chunk.CropsLODs = new TerrainChunk.RenderData[_config.Terrain.CropsLODs.Length];
return chunk;
}
}
}

View File

@@ -0,0 +1,21 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TerrainMaterialsInitializationOperation : IWorldGeneratorOperation
{
private static readonly int TerrainMapPropertyID = Shader.PropertyToID("_Terrain_Map");
[InjectService]
private GameConfig _config;
public UniTask Execute(World world)
{
var map = (Texture2D)_config.WorldGen.MapTextures[0].Asset;
Shader.SetGlobalTexture(TerrainMapPropertyID, map);
return UniTask.CompletedTask;
}
}
}

View File

@@ -0,0 +1,86 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
using Random = Unity.Mathematics.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TreeResourcesGeneratorOperation : IWorldGeneratorOperation
{
[InjectService]
private GameConfig _config;
[InjectService]
private ITileSpace _tileSpace;
public async UniTask Execute(World world)
{
await GenerateTreesAsync(world);
}
private async UniTask GenerateTreesAsync(World world)
{
var config = _config.WorldGen;
var treeDefinition = await config.Trees.TreeDefinition.LoadAssetAsync<ResourceNodeDefinition>();
var horizontalResolution = Mathf.FloorToInt(world.Size.x / config.Trees.Spacing);
var verticalResolution = Mathf.FloorToInt(world.Size.y / config.Trees.Spacing);
var random = new Random((uint)world.Seed);
var noiseSalt = world.Seed % ushort.MaxValue * 37;
for (var x = 0; x < horizontalResolution; x++)
for (var z = 0; z < verticalResolution; z++)
{
var treePosition = new Vector3((x + 0.5f) * config.Trees.Spacing, 0, (z + 0.5f) * config.Trees.Spacing);
if (z % 2 == 0) treePosition.x += 0.5f * config.Trees.Spacing;
treePosition.x += Mathf.Lerp(config.Trees.OffsetRange.x, config.Trees.OffsetRange.y, random.NextFloat());
treePosition.z += Mathf.Lerp(config.Trees.OffsetRange.x, config.Trees.OffsetRange.y, random.NextFloat());
var tile = _tileSpace.WorldToTile(treePosition);
if (!EnoughSpaceAround(world, tile)) continue;
var h = world.Heightmap.GetValue(tile);
if (h == 0) continue;
treePosition.y = h;
var noiseSamplePos = new float2(h * 997 + x, noiseSalt + z) * config.Trees.NoiseScale;
var n = noise.snoise(noiseSamplePos);
if (n < 1 - config.Trees.Coverage && random.NextFloat() < 1 - config.Trees.RandomTreeChance) continue;
world.RawResources.AddResourceNode(
new ResourceNode
{
Id = 1 + world.RawResources.Count,
DefinitionId = treeDefinition.RuntimeId,
Position = treePosition,
Tile = tile,
Elevation = _tileSpace.GetElevation(treePosition.y),
CanBeDeleted = true
});
world.BlockMap.AddReason(tile, BlockReason.RawResource);
}
}
private static bool EnoughSpaceAround(World world, int2 center)
{
const int radius = 1;
for (var x = -radius; x <= radius; x++)
for (var y = -radius; y <= radius; y++)
{
var p = center + new int2(x, y);
if (!world.Contains(p)) continue;
if (world.Fertility.GetValue(p).MaxFertility > 0) return false;
}
return true;
}
}
}