riversong code showcase
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class ChunkGenerationJobState
|
||||
{
|
||||
public int Lod;
|
||||
|
||||
public List<TerrainChunk> Chunks = new();
|
||||
|
||||
public List<Mesh> Meshes = new();
|
||||
|
||||
public Mesh.MeshDataArray MeshDataArray;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Lod = 0;
|
||||
Chunks.Clear();
|
||||
Meshes.Clear();
|
||||
MeshDataArray = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public abstract class ChunkMeshGenerator
|
||||
{
|
||||
private readonly List<ChunkGenerationJobState> _pendingJobs = new();
|
||||
|
||||
private JobHandle _dependency;
|
||||
|
||||
protected ChunkMeshGenerator(World world, GameConfig config)
|
||||
{
|
||||
World = world;
|
||||
Config = config;
|
||||
}
|
||||
|
||||
protected World World { get; }
|
||||
|
||||
protected GameConfig Config { get; }
|
||||
|
||||
public void InitializeChunk(TerrainChunk chunk)
|
||||
{
|
||||
for (var lod = 0; lod < GetLodCount(); lod++) InitializeChunk(chunk, lod);
|
||||
}
|
||||
|
||||
private void InitializeChunk(TerrainChunk chunk, int lod)
|
||||
{
|
||||
var childName = GetGameObjectName(chunk.Root.name, chunk.Coords, lod);
|
||||
|
||||
var child = new GameObject(childName);
|
||||
child.transform.SetParent(chunk.Root.transform);
|
||||
|
||||
var mesh = new Mesh { name = childName };
|
||||
mesh.MarkDynamic();
|
||||
|
||||
var meshFilter = child.AddComponent<MeshFilter>();
|
||||
meshFilter.sharedMesh = mesh;
|
||||
|
||||
var renderer = child.AddComponent<MeshRenderer>();
|
||||
|
||||
InitializeChunk(chunk, lod, child, mesh, renderer);
|
||||
}
|
||||
|
||||
protected abstract int GetLodCount();
|
||||
|
||||
protected abstract string GetGameObjectName(string chunkName, int2 chunkCoords, int lod);
|
||||
|
||||
protected abstract void InitializeChunk(TerrainChunk chunk, int lod, GameObject gameObject, Mesh mesh, Renderer renderer);
|
||||
|
||||
public void BeginGeneratingChunks(List<TerrainChunk> chunks)
|
||||
{
|
||||
var chunkCoords = new NativeArray<int2>(chunks.Count, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
|
||||
for (var i = 0; i < chunks.Count; i++) chunkCoords[i] = chunks[i].Coords;
|
||||
|
||||
for (var lod = 0; lod < GetLodCount(); lod++)
|
||||
{
|
||||
var jobState = CreateJobState(lod, chunks);
|
||||
_pendingJobs.Add(jobState);
|
||||
|
||||
var jobHandle = ScheduleJob(jobState, chunkCoords);
|
||||
_dependency = JobHandle.CombineDependencies(_dependency, jobHandle);
|
||||
}
|
||||
|
||||
chunkCoords.Dispose(_dependency);
|
||||
}
|
||||
|
||||
private ChunkGenerationJobState CreateJobState(int lod, List<TerrainChunk> chunks)
|
||||
{
|
||||
var jobState = new ChunkGenerationJobState { Lod = lod };
|
||||
|
||||
jobState.Chunks.AddRange(chunks);
|
||||
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
var mesh = GetMesh(chunk, lod);
|
||||
jobState.Meshes.Add(mesh);
|
||||
}
|
||||
|
||||
jobState.MeshDataArray = Mesh.AllocateWritableMeshData(jobState.Meshes);
|
||||
|
||||
return jobState;
|
||||
}
|
||||
|
||||
protected abstract Mesh GetMesh(TerrainChunk chunk, int lod);
|
||||
|
||||
protected abstract JobHandle ScheduleJob(ChunkGenerationJobState jobState, NativeArray<int2> chunkCoords);
|
||||
|
||||
public async UniTask Wait()
|
||||
{
|
||||
while (!_dependency.IsCompleted) await UniTask.NextFrame();
|
||||
|
||||
_dependency.Complete();
|
||||
_dependency = default;
|
||||
}
|
||||
|
||||
public void EndGeneratingChunks()
|
||||
{
|
||||
foreach (var jobState in _pendingJobs) OnCompleted(jobState);
|
||||
_pendingJobs.Clear();
|
||||
}
|
||||
|
||||
private void OnCompleted(ChunkGenerationJobState jobState)
|
||||
{
|
||||
Mesh.ApplyAndDisposeWritableMeshData(jobState.MeshDataArray, jobState.Meshes);
|
||||
|
||||
foreach (var mesh in jobState.Meshes)
|
||||
{
|
||||
mesh.RecalculateBounds();
|
||||
mesh.UploadMeshData(false);
|
||||
}
|
||||
|
||||
OnCompleted(jobState.Lod, jobState.Chunks);
|
||||
|
||||
jobState.Clear();
|
||||
}
|
||||
|
||||
protected abstract void OnCompleted(int lod, List<TerrainChunk> chunks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using Random = Unity.Mathematics.Random;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class CropsChunkMeshGenerator : GrassChunkMeshGeneratorBase<CropsGrassTileMask>
|
||||
{
|
||||
public CropsChunkMeshGenerator(World world, GameConfig config) : base(world, config)
|
||||
{
|
||||
}
|
||||
|
||||
protected override int GetLodCount()
|
||||
{
|
||||
return Config.Terrain.CropsLODs.Length;
|
||||
}
|
||||
|
||||
protected override string GetGameObjectName(string chunkName, int2 chunkCoords, int lod)
|
||||
{
|
||||
return $"{chunkName}_Crops_LOD{lod}";
|
||||
}
|
||||
|
||||
protected override void InitializeChunk(TerrainChunk chunk, int lod, GameObject gameObject, Mesh mesh, Renderer renderer)
|
||||
{
|
||||
base.InitializeChunk(chunk, lod, gameObject, mesh, renderer);
|
||||
|
||||
chunk.CropsLODs[lod] = new TerrainChunk.RenderData(mesh, renderer);
|
||||
}
|
||||
|
||||
protected override Mesh GetMesh(TerrainChunk chunk, int lod)
|
||||
{
|
||||
return chunk.CropsLODs[lod].Mesh;
|
||||
}
|
||||
|
||||
protected override JobHandle ScheduleJob(ChunkGenerationJobState jobState, NativeArray<int2> chunkCoords, NativeArray<Random> randomArray)
|
||||
{
|
||||
var job = new GenerateGrassChunkJob<CropsGrassTileMask>();
|
||||
PrepareJob(jobState, chunkCoords, randomArray, jobState.Lod, ref job);
|
||||
return job.Schedule(chunkCoords.Length, Config.WorldGen.ChunksPerThread);
|
||||
}
|
||||
|
||||
protected override Renderer GetRenderer(TerrainChunk chunk, int lod)
|
||||
{
|
||||
return chunk.CropsLODs[lod].Renderer;
|
||||
}
|
||||
|
||||
protected override Material GetMaterial(int lod)
|
||||
{
|
||||
return Config.Terrain.CropsMaterial;
|
||||
}
|
||||
|
||||
protected override GameConfig.WorldGenConfig.GrassConfig GetGrassGenerationConfig()
|
||||
{
|
||||
return Config.WorldGen.Crops;
|
||||
}
|
||||
|
||||
protected override CropsGrassTileMask GetMask()
|
||||
{
|
||||
var fertility = World.Fertility.GetNativeArray();
|
||||
return new CropsGrassTileMask { Fertility = fertility };
|
||||
}
|
||||
|
||||
protected override int GetDensityForLOD(int lod)
|
||||
{
|
||||
return Config.Terrain.CropsLODs[lod].Density;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Unity.Collections;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct CropsGrassTileMask : IGrassTileMask
|
||||
{
|
||||
public NativeArray<FertilityMapValue> Fertility;
|
||||
|
||||
public bool CanGenerateAtTile(int tileIndex)
|
||||
{
|
||||
return Fertility[tileIndex].MaxFertility > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using Random = Unity.Mathematics.Random;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[BurstCompile]
|
||||
public struct GenerateGrassChunkJob<T> : IJobParallelFor where T : unmanaged, IGrassTileMask
|
||||
{
|
||||
public int2 WorldSize;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<int2> ChunkCoordsArray;
|
||||
|
||||
public int ChunkSize;
|
||||
|
||||
[ReadOnly]
|
||||
public T Mask;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<int> Heightmap;
|
||||
|
||||
[NativeDisableParallelForRestriction]
|
||||
public Mesh.MeshDataArray MeshDataArray;
|
||||
|
||||
public NativeArray<Random> RandomArray;
|
||||
|
||||
public int Density;
|
||||
|
||||
public float NoiseScale;
|
||||
|
||||
public float DiscardThreshold;
|
||||
|
||||
public float BladeWidth;
|
||||
|
||||
public float2 BladeHeightRange;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var chunkCoords = ChunkCoordsArray[index];
|
||||
var random = RandomArray[index];
|
||||
|
||||
var vertexAttributes = new NativeArray<VertexAttributeDescriptor>(3, Allocator.Temp);
|
||||
vertexAttributes[0] = new VertexAttributeDescriptor(VertexAttribute.Position);
|
||||
vertexAttributes[1] = new VertexAttributeDescriptor(VertexAttribute.Color);
|
||||
vertexAttributes[2] = new VertexAttributeDescriptor(VertexAttribute.TexCoord0, dimension: 2);
|
||||
|
||||
var triangleCount = ChunkSize * ChunkSize * Density * Density;
|
||||
var vertexTemp = new NativeList<Vertex>(3 * triangleCount, Allocator.Temp);
|
||||
var indexTemp = new NativeList<int>(3 * triangleCount, Allocator.Temp);
|
||||
|
||||
var tileMin = chunkCoords * ChunkSize;
|
||||
var tileMax = math.min(tileMin + ChunkSize, WorldSize);
|
||||
var tile = int2.zero;
|
||||
|
||||
var spacing = 1f / Density;
|
||||
|
||||
var triangle = 0;
|
||||
for (tile.y = tileMin.y; tile.y < tileMax.y; tile.y++)
|
||||
for (tile.x = tileMin.x; tile.x < tileMax.x; tile.x++)
|
||||
{
|
||||
var tileIndex = math.mad(tile.y, WorldSize.x, tile.x);
|
||||
|
||||
if (!Mask.CanGenerateAtTile(tileIndex)) continue;
|
||||
|
||||
var worldY = Heightmap[tileIndex];
|
||||
|
||||
for (var stepZ = 0; stepZ < Density; stepZ++)
|
||||
{
|
||||
var worldZ = math.mad(0.5f + stepZ, spacing, tile.y);
|
||||
|
||||
for (var stepX = 0; stepX < Density; stepX++)
|
||||
{
|
||||
var worldX = math.mad(0.5f + stepX, spacing, tile.x);
|
||||
|
||||
var bladeOrigin = new float3(worldX, worldY, worldZ);
|
||||
bladeOrigin += 0.25f * spacing * new float3(random.NextFloat(-1, 1), 0, random.NextFloat(-1, 1));
|
||||
|
||||
const float edgeDistance = 0.3f;
|
||||
var d = math.frac(bladeOrigin.xz);
|
||||
if ((d.x < edgeDistance && (tile.x == 0 || Heightmap[tileIndex - 1] != worldY)) ||
|
||||
(d.x > 1 - edgeDistance && (tile.x == WorldSize.x - 1 || Heightmap[tileIndex + 1] != worldY)) ||
|
||||
(d.y < edgeDistance && (tile.y == 0 || Heightmap[tileIndex - WorldSize.x] != worldY)) ||
|
||||
(d.y > 1 - edgeDistance && (tile.y == WorldSize.y - 1 || Heightmap[tileIndex + WorldSize.x] != worldY)) ||
|
||||
(d is { x: < edgeDistance, y: < edgeDistance } && Heightmap[tileIndex - 1 - WorldSize.x] != worldY) ||
|
||||
(d is { x: > 1 - edgeDistance, y: < edgeDistance } && Heightmap[tileIndex + 1 - WorldSize.x] != worldY) ||
|
||||
(d is { x: < edgeDistance, y: > 1 - edgeDistance } && Heightmap[tileIndex - 1 + WorldSize.x] != worldY) ||
|
||||
(d is { x: > 1 - edgeDistance, y: > 1 - edgeDistance } && Heightmap[tileIndex + 1 + WorldSize.x] != worldY))
|
||||
continue;
|
||||
|
||||
var n = noise.snoise(NoiseScale * bladeOrigin);
|
||||
n = math.remap(-1, 1, 0, 1, n);
|
||||
if (n < DiscardThreshold) continue;
|
||||
|
||||
var rotation = quaternion.Euler(0, random.NextFloat(math.PI2), 0);
|
||||
var halfBladeWidth = 0.5f * BladeWidth;
|
||||
var bladeHeight = math.lerp(BladeHeightRange.x, BladeHeightRange.y, n);
|
||||
|
||||
var side = math.mul(rotation, new float3(halfBladeWidth, 0, 0));
|
||||
var v0 = bladeOrigin - side;
|
||||
var v1 = bladeOrigin + bladeHeight * math.up();
|
||||
var v2 = bladeOrigin + side;
|
||||
|
||||
vertexTemp.AddNoResize(new Vertex(v0, bladeOrigin, new float2(0, bladeHeight)));
|
||||
vertexTemp.AddNoResize(new Vertex(v1, bladeOrigin, new float2(bladeHeight, bladeHeight)));
|
||||
vertexTemp.AddNoResize(new Vertex(v2, bladeOrigin, new float2(0, bladeHeight)));
|
||||
|
||||
indexTemp.AddNoResize(triangle);
|
||||
indexTemp.AddNoResize(triangle + 1);
|
||||
indexTemp.AddNoResize(triangle + 2);
|
||||
triangle += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mesh = MeshDataArray[index];
|
||||
|
||||
mesh.SetVertexBufferParams(vertexTemp.Length, vertexAttributes);
|
||||
|
||||
var chunkVertexData = mesh.GetVertexData<Vertex>();
|
||||
chunkVertexData.CopyFrom(vertexTemp.AsArray());
|
||||
|
||||
mesh.SetIndexBufferParams(indexTemp.Length, IndexFormat.UInt32);
|
||||
var chunkIndexData = mesh.GetIndexData<int>();
|
||||
chunkIndexData.CopyFrom(indexTemp.AsArray());
|
||||
|
||||
mesh.subMeshCount = 1;
|
||||
mesh.SetSubMesh(0, new SubMeshDescriptor(0, indexTemp.Length));
|
||||
}
|
||||
|
||||
private struct Vertex
|
||||
{
|
||||
public float3 Position;
|
||||
|
||||
public float3 Color;
|
||||
|
||||
public float2 TexCoord0;
|
||||
|
||||
public Vertex(float3 position, float3 color, float2 texCoord0)
|
||||
{
|
||||
Position = position;
|
||||
Color = color;
|
||||
TexCoord0 = texCoord0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[BurstCompile]
|
||||
public struct GenerateTerrainChunkJob : IJobParallelFor
|
||||
{
|
||||
public int2 WorldSize;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<int2> ChunkCoordsArray;
|
||||
|
||||
public int ChunkSize;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<int> Heightmap;
|
||||
|
||||
[NativeDisableParallelForRestriction]
|
||||
public Mesh.MeshDataArray MeshDataArray;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var coords = ChunkCoordsArray[index];
|
||||
|
||||
var vertexAttributes = new NativeArray<VertexAttributeDescriptor>(3, Allocator.Temp);
|
||||
vertexAttributes[0] = new VertexAttributeDescriptor(VertexAttribute.Position);
|
||||
vertexAttributes[1] = new VertexAttributeDescriptor(VertexAttribute.Normal);
|
||||
vertexAttributes[2] = new VertexAttributeDescriptor(VertexAttribute.TexCoord0, dimension: 2);
|
||||
|
||||
var tileCount = ChunkSize * ChunkSize;
|
||||
var vertexTemp = new NativeList<Vertex>(20 * tileCount, Allocator.Temp);
|
||||
var indexTemp = new NativeList<int>(30 * tileCount, Allocator.Temp);
|
||||
|
||||
var tileMin = coords * ChunkSize;
|
||||
var tileMax = math.min(tileMin + ChunkSize, WorldSize);
|
||||
var tile = int2.zero;
|
||||
|
||||
for (tile.y = tileMin.y; tile.y < tileMax.y; tile.y++)
|
||||
for (tile.x = tileMin.x; tile.x < tileMax.x; tile.x++)
|
||||
{
|
||||
var h = Heightmap[math.mad(tile.y, WorldSize.x, tile.x)];
|
||||
if (h == 0) continue;
|
||||
|
||||
var v0 = new Vertex(new float3(tile.x, h, tile.y), math.up(), float2.zero);
|
||||
var v1 = new Vertex(new float3(tile.x, h, tile.y + 1), math.up(), float2.zero);
|
||||
var v2 = new Vertex(new float3(tile.x + 1, h, tile.y + 1), math.up(), float2.zero);
|
||||
var v3 = new Vertex(new float3(tile.x + 1, h, tile.y), math.up(), float2.zero);
|
||||
WriteQuad(vertexTemp, indexTemp, v0, v1, v2, v3, false);
|
||||
}
|
||||
|
||||
var cliffSubMeshIndexStart = indexTemp.Length;
|
||||
|
||||
for (tile.y = tileMin.y; tile.y < tileMax.y && tile.y < WorldSize.y - 1; tile.y++)
|
||||
for (tile.x = tileMin.x; tile.x < tileMax.x && tile.x < WorldSize.x - 1; tile.x++)
|
||||
{
|
||||
var h = Heightmap[math.mad(tile.y, WorldSize.x, tile.x)];
|
||||
|
||||
var hx = Heightmap[math.mad(tile.y, WorldSize.x, tile.x + 1)];
|
||||
if (h != hx)
|
||||
{
|
||||
var hMin = math.min(h, hx);
|
||||
var hMax = math.max(h, hx);
|
||||
|
||||
for (var y = hMin; y < hMax; y++)
|
||||
{
|
||||
var bottomUV = math.unlerp(hMin, hMax, y);
|
||||
var topUV = math.unlerp(hMin, hMax, y + 1);
|
||||
|
||||
var normal = h > hx ? math.right() : math.left();
|
||||
var v0 = new Vertex(new float3(tile.x + 1, y, tile.y), normal, bottomUV);
|
||||
var v1 = new Vertex(new float3(tile.x + 1, y + 1, tile.y), normal, topUV);
|
||||
var v2 = new Vertex(new float3(tile.x + 1, y + 1, tile.y + 1), normal, topUV);
|
||||
var v3 = new Vertex(new float3(tile.x + 1, y, tile.y + 1), normal, bottomUV);
|
||||
WriteQuad(vertexTemp, indexTemp, v0, v1, v2, v3, h < hx);
|
||||
}
|
||||
}
|
||||
|
||||
var hz = Heightmap[math.mad(tile.y + 1, WorldSize.x, tile.x)];
|
||||
if (h != hz)
|
||||
{
|
||||
var hMin = math.min(h, hz);
|
||||
var hMax = math.max(h, hz);
|
||||
|
||||
for (var y = hMin; y < hMax; y++)
|
||||
{
|
||||
var bottomUV = math.unlerp(hMin, hMax, y);
|
||||
var topUV = math.unlerp(hMin, hMax, y + 1);
|
||||
|
||||
var normal = h > hz ? math.forward() : math.back();
|
||||
var v0 = new Vertex(new float3(tile.x + 1, y, tile.y + 1), normal, bottomUV);
|
||||
var v1 = new Vertex(new float3(tile.x + 1, y + 1, tile.y + 1), normal, topUV);
|
||||
var v2 = new Vertex(new float3(tile.x, y + 1, tile.y + 1), normal, topUV);
|
||||
var v3 = new Vertex(new float3(tile.x, y, tile.y + 1), normal, bottomUV);
|
||||
WriteQuad(vertexTemp, indexTemp, v0, v1, v2, v3, h < hz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mesh = MeshDataArray[index];
|
||||
|
||||
mesh.SetVertexBufferParams(vertexTemp.Length, vertexAttributes);
|
||||
|
||||
var chunkVertexData = mesh.GetVertexData<Vertex>();
|
||||
chunkVertexData.CopyFrom(vertexTemp.AsArray());
|
||||
|
||||
mesh.SetIndexBufferParams(indexTemp.Length, IndexFormat.UInt32);
|
||||
var chunkIndexData = mesh.GetIndexData<int>();
|
||||
chunkIndexData.CopyFrom(indexTemp.AsArray());
|
||||
|
||||
mesh.subMeshCount = indexTemp.Length > cliffSubMeshIndexStart ? 2 : 1;
|
||||
mesh.SetSubMesh(0, new SubMeshDescriptor(0, cliffSubMeshIndexStart));
|
||||
if (mesh.subMeshCount > 1) mesh.SetSubMesh(1, new SubMeshDescriptor(cliffSubMeshIndexStart, chunkIndexData.Length - cliffSubMeshIndexStart));
|
||||
}
|
||||
|
||||
private void WriteQuad(NativeList<Vertex> vertices, NativeList<int> indices, in Vertex v0, in Vertex v1, in Vertex v2, in Vertex v3, bool flip)
|
||||
{
|
||||
var v = vertices.Length;
|
||||
|
||||
vertices.AddNoResize(v0);
|
||||
vertices.AddNoResize(v1);
|
||||
vertices.AddNoResize(v2);
|
||||
vertices.AddNoResize(v3);
|
||||
|
||||
var i0 = v + (flip ? 2 : 0);
|
||||
var i1 = v + 1;
|
||||
var i2 = v + (flip ? 0 : 2);
|
||||
var i3 = v + 3;
|
||||
|
||||
indices.AddNoResize(i0);
|
||||
indices.AddNoResize(i1);
|
||||
indices.AddNoResize(i2);
|
||||
indices.AddNoResize(i2);
|
||||
indices.AddNoResize(i3);
|
||||
indices.AddNoResize(i0);
|
||||
}
|
||||
|
||||
private struct Vertex
|
||||
{
|
||||
public float3 Position;
|
||||
|
||||
public float3 Normal;
|
||||
|
||||
public float2 TexCoord0;
|
||||
|
||||
public Vertex(float3 position, float3 normal, float2 texCoord0)
|
||||
{
|
||||
Position = position;
|
||||
Normal = normal;
|
||||
TexCoord0 = texCoord0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using Random = Unity.Mathematics.Random;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class GrassChunkMeshGenerator : GrassChunkMeshGeneratorBase<GrassTileMask>
|
||||
{
|
||||
public GrassChunkMeshGenerator(World world, GameConfig config) : base(world, config)
|
||||
{
|
||||
}
|
||||
|
||||
protected override int GetLodCount()
|
||||
{
|
||||
return Config.Terrain.GrassLODs.Length;
|
||||
}
|
||||
|
||||
protected override string GetGameObjectName(string chunkName, int2 chunkCoords, int lod)
|
||||
{
|
||||
return $"{chunkName}_Grass_LOD{lod}";
|
||||
}
|
||||
|
||||
protected override void InitializeChunk(TerrainChunk chunk, int lod, GameObject gameObject, Mesh mesh, Renderer renderer)
|
||||
{
|
||||
base.InitializeChunk(chunk, lod, gameObject, mesh, renderer);
|
||||
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
||||
|
||||
chunk.GrassLODs[lod] = new TerrainChunk.RenderData(mesh, renderer);
|
||||
}
|
||||
|
||||
protected override Mesh GetMesh(TerrainChunk chunk, int lod)
|
||||
{
|
||||
return chunk.GrassLODs[lod].Mesh;
|
||||
}
|
||||
|
||||
protected override JobHandle ScheduleJob(ChunkGenerationJobState jobState, NativeArray<int2> chunkCoords, NativeArray<Random> randomArray)
|
||||
{
|
||||
var job = new GenerateGrassChunkJob<GrassTileMask>();
|
||||
PrepareJob(jobState, chunkCoords, randomArray, jobState.Lod, ref job);
|
||||
return job.Schedule(chunkCoords.Length, Config.WorldGen.ChunksPerThread);
|
||||
}
|
||||
|
||||
protected override Renderer GetRenderer(TerrainChunk chunk, int lod)
|
||||
{
|
||||
return chunk.GrassLODs[lod].Renderer;
|
||||
}
|
||||
|
||||
protected override Material GetMaterial(int lod)
|
||||
{
|
||||
return Config.Terrain.GrassMaterial;
|
||||
}
|
||||
|
||||
protected override GameConfig.WorldGenConfig.GrassConfig GetGrassGenerationConfig()
|
||||
{
|
||||
return Config.WorldGen.Grass;
|
||||
}
|
||||
|
||||
protected override GrassTileMask GetMask()
|
||||
{
|
||||
var heightmap = World.Heightmap.GetNativeArray();
|
||||
var fertility = World.Fertility.GetNativeArray();
|
||||
return new GrassTileMask
|
||||
{
|
||||
Heightmap = heightmap,
|
||||
Fertility = fertility
|
||||
};
|
||||
}
|
||||
|
||||
protected override int GetDensityForLOD(int lod)
|
||||
{
|
||||
return Config.Terrain.GrassLODs[lod].Density;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using Random = Unity.Mathematics.Random;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public abstract class GrassChunkMeshGeneratorBase<T> : ChunkMeshGenerator where T : unmanaged, IGrassTileMask
|
||||
{
|
||||
protected GrassChunkMeshGeneratorBase(World world, GameConfig config) : base(world, config)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void InitializeChunk(TerrainChunk chunk, int lod, GameObject gameObject, Mesh mesh, Renderer renderer)
|
||||
{
|
||||
renderer.enabled = false;
|
||||
}
|
||||
|
||||
protected override void OnCompleted(int lod, List<TerrainChunk> chunks)
|
||||
{
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
var renderer = GetRenderer(chunk, lod);
|
||||
renderer.sharedMaterial = GetMaterial(lod);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Renderer GetRenderer(TerrainChunk chunk, int lod);
|
||||
|
||||
protected abstract Material GetMaterial(int lod);
|
||||
|
||||
protected override JobHandle ScheduleJob(ChunkGenerationJobState jobState, NativeArray<int2> chunkCoords)
|
||||
{
|
||||
var randomArray = new NativeArray<Random>(chunkCoords.Length, Allocator.Persistent);
|
||||
for (var i = 0; i < randomArray.Length; i++) randomArray[i] = Random.CreateFromIndex((uint)(World.Seed + i));
|
||||
|
||||
var jobHandle = ScheduleJob(jobState, chunkCoords, randomArray);
|
||||
|
||||
randomArray.Dispose(jobHandle);
|
||||
|
||||
return jobHandle;
|
||||
}
|
||||
|
||||
protected abstract JobHandle ScheduleJob(ChunkGenerationJobState jobState, NativeArray<int2> chunkCoords, NativeArray<Random> randomArray);
|
||||
|
||||
protected void PrepareJob(ChunkGenerationJobState jobState, NativeArray<int2> chunkCoords, NativeArray<Random> randomArray, int lod, ref GenerateGrassChunkJob<T> job)
|
||||
{
|
||||
var grassGenerationConfig = GetGrassGenerationConfig();
|
||||
|
||||
job.WorldSize = World.Size;
|
||||
job.ChunkCoordsArray = chunkCoords;
|
||||
job.ChunkSize = Config.WorldGen.ChunkSize;
|
||||
job.Mask = GetMask();
|
||||
job.Heightmap = World.Heightmap.GetNativeArray();
|
||||
job.MeshDataArray = jobState.MeshDataArray;
|
||||
job.RandomArray = randomArray;
|
||||
job.Density = GetDensityForLOD(lod);
|
||||
job.NoiseScale = grassGenerationConfig.NoiseScale;
|
||||
job.DiscardThreshold = grassGenerationConfig.NoiseDiscardThreshold;
|
||||
job.BladeWidth = grassGenerationConfig.BladeWidth;
|
||||
job.BladeHeightRange = grassGenerationConfig.BladeHeightRange;
|
||||
}
|
||||
|
||||
protected abstract GameConfig.WorldGenConfig.GrassConfig GetGrassGenerationConfig();
|
||||
|
||||
protected abstract T GetMask();
|
||||
|
||||
protected abstract int GetDensityForLOD(int lod);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Unity.Collections;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct GrassTileMask : IGrassTileMask
|
||||
{
|
||||
public NativeArray<int> Heightmap;
|
||||
|
||||
public NativeArray<FertilityMapValue> Fertility;
|
||||
|
||||
public bool CanGenerateAtTile(int tileIndex)
|
||||
{
|
||||
return Heightmap[tileIndex] > 0 && Fertility[tileIndex].MaxFertility == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public interface IGrassTileMask
|
||||
{
|
||||
bool CanGenerateAtTile(int tileIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class TerrainChunkMeshGenerator : ChunkMeshGenerator
|
||||
{
|
||||
public TerrainChunkMeshGenerator(World world, GameConfig config) : base(world, config)
|
||||
{
|
||||
}
|
||||
|
||||
protected override int GetLodCount()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected override string GetGameObjectName(string chunkName, int2 chunkCoords, int lod)
|
||||
{
|
||||
return $"{chunkName}_Terrain";
|
||||
}
|
||||
|
||||
protected override void InitializeChunk(TerrainChunk chunk, int lod, GameObject gameObject, Mesh mesh, Renderer renderer)
|
||||
{
|
||||
gameObject.layer = GameObjectLayers.Terrain;
|
||||
|
||||
chunk.Terrain = new TerrainChunk.RenderData(mesh, renderer);
|
||||
}
|
||||
|
||||
protected override Mesh GetMesh(TerrainChunk chunk, int lod)
|
||||
{
|
||||
return chunk.Terrain.Mesh;
|
||||
}
|
||||
|
||||
protected override JobHandle ScheduleJob(ChunkGenerationJobState jobState, NativeArray<int2> chunkCoords)
|
||||
{
|
||||
return new GenerateTerrainChunkJob
|
||||
{
|
||||
WorldSize = World.Size,
|
||||
ChunkCoordsArray = chunkCoords,
|
||||
ChunkSize = Config.WorldGen.ChunkSize,
|
||||
Heightmap = World.Heightmap.GetNativeArray(),
|
||||
MeshDataArray = jobState.MeshDataArray
|
||||
}.Schedule(chunkCoords.Length, Config.WorldGen.ChunksPerThread);
|
||||
}
|
||||
|
||||
protected override void OnCompleted(int lod, List<TerrainChunk> chunks)
|
||||
{
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
var mesh = chunk.Terrain.Mesh;
|
||||
var renderer = chunk.Terrain.Renderer;
|
||||
|
||||
var subMeshCount = mesh.subMeshCount;
|
||||
|
||||
var materials = new Material[subMeshCount];
|
||||
materials[0] = Config.Terrain.GroundMaterial;
|
||||
if (subMeshCount > 1) materials[1] = Config.Terrain.CliffMaterial;
|
||||
|
||||
renderer.sharedMaterials = materials;
|
||||
|
||||
renderer.gameObject.AddComponent<MeshCollider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public interface IOnWorldGenerationCompletedCallback
|
||||
{
|
||||
UniTask OnWorldGenerationCompletedAsync(World world);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public interface IWorldGeneratorOperation
|
||||
{
|
||||
UniTask Execute(World world);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class RequiresWorldReadyForUpdateAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
38
Source/Riversong/Game/WorldGen/WorldCreationSystem.cs
Normal file
38
Source/Riversong/Game/WorldGen/WorldCreationSystem.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[GameSystemGroup(typeof(WorldGenSystemGroup))]
|
||||
public class WorldCreationSystem : GameSystem, IServiceProvider, IDisposable
|
||||
{
|
||||
private World _world;
|
||||
|
||||
public WorldCreationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public void RegisterServices(IServiceLocator serviceLocator)
|
||||
{
|
||||
_world = new World();
|
||||
_world.Size = int2.zero;
|
||||
_world.Heightmap = new WorldHeightmap(int2.zero);
|
||||
_world.BlockMap = new BlockMap(int2.zero);
|
||||
_world.Fertility = new FertilityMap(int2.zero);
|
||||
_world.WaterMap = new WaterMap(int2.zero);
|
||||
_world.EntityIdMap = new EntityIdMap(int2.zero);
|
||||
_world.RoadNetwork = new RoadNetwork(int2.zero);
|
||||
|
||||
ServiceLocator.RegisterService(_world);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_world.Dispose();
|
||||
|
||||
Debug.Log("World disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Source/Riversong/Game/WorldGen/WorldGenSystemGroup.cs
Normal file
9
Source/Riversong/Game/WorldGen/WorldGenSystemGroup.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[InitializeAfter(typeof(EarlyGameSystemGroup))]
|
||||
[InitializeBefore(typeof(DefaultGameSystemGroup))]
|
||||
public class WorldGenSystemGroup : GameSystemGroup
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct WorldGenerationCompletedSignal
|
||||
{
|
||||
public List<IOnWorldGenerationCompletedCallback> Callbacks;
|
||||
|
||||
public WorldGenerationCompletedSignal(List<IOnWorldGenerationCompletedCallback> callbacks)
|
||||
{
|
||||
Callbacks = callbacks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[GameSystemGroup(typeof(WorldGenSystemGroup))]
|
||||
[InitializeAfter(typeof(WorldCreationSystem))]
|
||||
public class WorldGenerationOperationsSystem : GameSystem, IInitializable, IDisposable
|
||||
{
|
||||
[InjectService]
|
||||
private World _world;
|
||||
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
private readonly List<IWorldGeneratorOperation> _operations = new();
|
||||
|
||||
public WorldGenerationOperationsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync()
|
||||
{
|
||||
_operations.Add(new MapDataInitializationOperation());
|
||||
_operations.Add(new TerrainMaterialsInitializationOperation());
|
||||
_operations.Add(new TerrainGeneratorOperation());
|
||||
_operations.Add(new TreeResourcesGeneratorOperation());
|
||||
_operations.Add(new StoneResourcesGeneratorOperation());
|
||||
_operations.Add(new FreeProductStacksGeneratorOperation());
|
||||
_operations.Add(new CritterHerdsGeneratorOperation());
|
||||
|
||||
foreach (var operation in _operations) ServiceLocator.Inject(operation);
|
||||
|
||||
_signalBus.Subscribe<GameStartedSignal>(OnGameStarted);
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_signalBus.Unsubscribe<GameStartedSignal>(OnGameStarted);
|
||||
}
|
||||
|
||||
private void OnGameStarted(GameStartedSignal signal)
|
||||
{
|
||||
ExecuteOperationsAsync().Forget(Debug.LogException);
|
||||
}
|
||||
|
||||
private async UniTask ExecuteOperationsAsync()
|
||||
{
|
||||
_world.Seed = Environment.TickCount;
|
||||
|
||||
foreach (var operation in _operations) await operation.Execute(_world);
|
||||
|
||||
var generationCompletedCallbacks = new List<IOnWorldGenerationCompletedCallback>();
|
||||
_signalBus.Raise(new WorldGenerationCompletedSignal(generationCompletedCallbacks));
|
||||
foreach (var callback in generationCompletedCallbacks) await callback.OnWorldGenerationCompletedAsync(_world);
|
||||
|
||||
_signalBus.Raise(new WorldReadySignal());
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Source/Riversong/Game/WorldGen/WorldReadySignal.cs
Normal file
6
Source/Riversong/Game/WorldGen/WorldReadySignal.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct WorldReadySignal
|
||||
{
|
||||
}
|
||||
}
|
||||
58
Source/Riversong/Game/WorldGen/WorldReadyUpdateFilter.cs
Normal file
58
Source/Riversong/Game/WorldGen/WorldReadyUpdateFilter.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class WorldReadyUpdateFilter : GameSystem, IInitializable, IDisposable, IUpdateFilter
|
||||
{
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
[InjectService]
|
||||
private IEngine _engine;
|
||||
|
||||
private readonly HashSet<Type> _filteredTypes = new();
|
||||
|
||||
private bool _worldReady;
|
||||
|
||||
public WorldReadyUpdateFilter(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync()
|
||||
{
|
||||
CacheFilteredTypes();
|
||||
|
||||
_signalBus.Subscribe<WorldReadySignal>(OnWorldReady);
|
||||
|
||||
_engine.RegisterUpdateFilter(this);
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_signalBus.Unsubscribe<WorldReadySignal>(OnWorldReady);
|
||||
}
|
||||
|
||||
private void CacheFilteredTypes()
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
foreach (var type in assembly.GetTypes())
|
||||
if (type.GetCustomAttribute<RequiresWorldReadyForUpdateAttribute>() != null)
|
||||
_filteredTypes.Add(type);
|
||||
}
|
||||
|
||||
private void OnWorldReady(WorldReadySignal _)
|
||||
{
|
||||
_worldReady = true;
|
||||
}
|
||||
|
||||
public bool CanUpdate(IUpdatable updatable)
|
||||
{
|
||||
return _worldReady || !_filteredTypes.Contains(updatable.GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user