Hi everyone, I previously had some low-performance monobehaviour code for some procedural generation. However since then, I have optimised and used the following tools:
- ECS (Entity Component System)
- Burst Compiler
- Unity parallel jobs system
- Batching
Is there any other technologies anyone would recommend I try?
Here is the revised code btw, free for anyone to use:
using System.Collections.Generic;
using System.Linq;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;
using Material = UnityEngine.Material;
using Random = UnityEngine.Random;
public class ImprovedProcGen : MonoBehaviour
{
public GenerationSettings ChunkGen;
}
[System.Serializable]
public struct GenerationSettings : IComponentData
{
[Range(1, 254)]
public int MeshDetail;
public int ChunkSize, ViewRange;
public float NoiseScale, TesselationFactor, HeightMultiplier;
public bool InvertPlane;
public Color TerrainColour;
}
class ImrpovedProcGenBaker : Baker<ImprovedProcGen>
{
public override void Bake(ImprovedProcGen authoring)
{
Entity Base = GetEntity(flags: TransformUsageFlags.None);
AddComponent(Base, authoring.ChunkGen);
Debug.Log("Baked");
}
}
public partial class MySystem : SystemBase
{
GenerationSettings CS;
Mesh TerrainMesh;
int[] TriData;
Material TerrainMat;
float2 SeedOffset;
float2 PrevPlayerPos = float2.zero;
Transform PlaTra;
EntityManager EM;
Dictionary<float2, CombineInstance> Meshes = new Dictionary<float2, CombineInstance>();
ChunkConfig CC;
struct ChunkConfig
{
public int LOD, verticesPerSide, totalVertices, totalTriangles;
public float2 SeedOffset;
}
protected override void OnStartRunning()
{
base.OnStartRunning();
CS = SystemAPI.GetSingleton<GenerationSettings>();
PlaTra = GameObject.FindFirstObjectByType<Player>().transform;
int NLOD = math.max(CS.MeshDetail, 1);
int VPS = NLOD + 1;
int TV = VPS * VPS;
int TT = NLOD * NLOD * 6;
Random.InitState((int)(Random.value * math.pow(2, 32)));
SeedOffset = new float2(Random.Range(-10000, 10000), Random.Range(-10000, 10000));
CC = new ChunkConfig()
{
LOD = NLOD,
verticesPerSide = VPS,
totalVertices = TV,
totalTriangles = TT,
SeedOffset = new float2(Random.Range(-10000, 10000), Random.Range(-10000, 10000))
};
GenerateTrianglesJob TriJob = new GenerateTrianglesJob()
{
LOD = CC.LOD,
verticesPerSide = CC.LOD + 1,
triangles = new NativeArray<int>(CC.totalTriangles, Allocator.TempJob),
F = CS.InvertPlane
};
JobHandle TriJobHandle = TriJob.Schedule(CC.totalTriangles, CS.MeshDetail + 1);
TriJobHandle.Complete();
TriData = TriJob.triangles.ToArray();
TerrainMat = TesselatedTerrrain();
TerrainMesh = new Mesh();
CreateInitialChunks();
GenerateMesh();
}
void CheckIfMovement()
{
float2 CurPos = new float2(PlaTra.position.x, PlaTra.position.z);
int Spacing = CS.ChunkSize;
if (CurPos.x - PrevPlayerPos.x > Spacing)
{
PrevPlayerPos.x += Spacing;
GenerateChunks(true, false);
Debug.Log("Working");
}
if (CurPos.x - PrevPlayerPos.x < -Spacing)
{
PrevPlayerPos.x -= Spacing;
GenerateChunks(true, true);
Debug.Log("Working");
}
if (CurPos.y - PrevPlayerPos.y > Spacing)
{
PrevPlayerPos.y += Spacing;
GenerateChunks(false, false);
Debug.Log("Working");
}
if (CurPos.y - PrevPlayerPos.y < -Spacing)
{
PrevPlayerPos.y -= Spacing;
GenerateChunks(false, true);
Debug.Log("Working");
}
}
void GenerateChunks(bool XAxis, bool Negative)
{
int R = CS.ViewRange;
float S = CS.ChunkSize;
float SR = S * R * ((Negative) ? -1 : 1);
if (XAxis)
{
float PX = PrevPlayerPos.x + SR;
float NX = PrevPlayerPos.x - SR;
for (int i = (int)-math.floor(R); i < math.floor(R); i++)
{
float Y = PrevPlayerPos.y + (i * S);
CreateChunk(new float2(PX, Y));
Meshes.Remove(new float2(NX, Y));
}
}
else
{
float PY = PrevPlayerPos.y + SR;
float NY = PrevPlayerPos.y - SR;
for (int i = (int)-math.floor(R); i < math.floor(R); i++)
{
float X = PrevPlayerPos.x + (i * S);
CreateChunk(new float2(X, PY));
Meshes.Remove(new float2(X, NY));
}
}
GenerateMesh();
EM = World.DefaultGameObjectInjectionWorld.EntityManager;
CreateColliderEntity();
CreateCollider();
}
void GenerateMesh()
{
TerrainMesh.CombineMeshes(Meshes.Values.ToArray());
TerrainMesh.RecalculateNormals();
TerrainMesh.RecalculateTangents();
}
void CreateColliderEntity()
{
GameObject.CreatePrimitive(PrimitiveType.Cube);
//ColEnt.AddComponent<UnityEngine.MeshCollider>();
}
void CreateCollider()
{
//ColEnt.GetComponent<UnityEngine.MeshCollider>().sharedMesh = TerrainMesh;
}
void CreateInitialChunks()
{
int ViewRange = CS.ViewRange;
for (int x = (int) math.floor(-ViewRange); x < (int)math.floor(ViewRange); x++)
{
for (int y = (int)math.floor(-ViewRange); y < (int)math.floor(ViewRange); y++)
{
CreateChunk(new float2(x * CS.ChunkSize, y * CS.ChunkSize));
}
}
}
protected override void OnUpdate()
{
CheckIfMovement();
Graphics.DrawMesh(TerrainMesh, Vector3.zero, Quaternion.identity, TerrainMat, 0);
}
Material TesselatedTerrrain()
{
Material Result = new Material(Shader.Find("Shader Graphs/Terrain"));
Result.SetColor("_TerrainColour", CS.TerrainColour);
Result.SetFloat("_TesselationFactor", CS.TesselationFactor);
return Result;
}
CombineInstance CreateChunk(float2 pos)
{
LocalTransform NT = new LocalTransform()
{
Position = float3.zero,
Rotation = quaternion.identity,
Scale = 1
};
CombineInstance Instance = new CombineInstance()
{
mesh = GeneratePlane(pos), transform = NT.ToMatrix()
};
if (Instance.mesh != null)
{
Meshes.Add(pos, Instance);
}
return Instance;
}
public Mesh GeneratePlane(float2 pos)
{
int TVs = CC.totalVertices;
int VPSs = CC.verticesPerSide;
GetVerticesJob VertexJob = new GetVerticesJob()
{
VerticesPerSide = VPSs,
Steps = CS.ChunkSize / (VPSs - 1f),
NewVertices = new NativeArray<float3>(TVs, Allocator.TempJob),
ChunkSize = CS.ChunkSize / VPSs,
ChunkOffset = pos,
SeedOffset = SeedOffset,
NoiseScale = CS.NoiseScale,
HeightMultiplier = CS.HeightMultiplier
};
JobHandle VertexJobHandle = VertexJob.Schedule(TVs, VPSs);
VertexJobHandle.Complete();
Mesh Result = new Mesh();
Result.vertices = ConvertToVector3Array(VertexJob.NewVertices);
VertexJob.NewVertices.Dispose();
Result.triangles = TriData;
return Result;
}
[BurstCompile]
public struct GetVerticesJob : IJobParallelFor
{
public int VerticesPerSide;
public float ChunkSize, Steps, NoiseScale, HeightMultiplier;
public float2 ChunkOffset, SeedOffset;
public NativeArray<float3> NewVertices;
public void Execute(int index)
{
int x = index % VerticesPerSide;
int y = index / VerticesPerSide;
float2 Pos = new float2(x, y) * Steps;
float2 GlobalPos = Pos + ChunkOffset;
float2 NoisePos = GlobalPos * NoiseScale + SeedOffset;
float Height = Mathf.PerlinNoise(NoisePos.x, NoisePos.y) * HeightMultiplier;
NewVertices[index] = new float3(GlobalPos.x, Height, GlobalPos.y);
}
}
public static Vector3[] ConvertToVector3Array(NativeArray<float3> float3Array)
{
int length = float3Array.Length;
Vector3[] vector3Array = new Vector3[length];
for (int i = 0; i < length; i++)
{
vector3Array[i] = new Vector3(float3Array[i].x, float3Array[i].y, float3Array[i].z);
}
return vector3Array;
}
[BurstCompile]
public struct GenerateTrianglesJob : IJobParallelFor
{
[ReadOnly] public int LOD, verticesPerSide;
[ReadOnly] public bool F; //Flipped triangles?
[NativeDisableParallelForRestriction, WriteOnly] public NativeArray<int> triangles;
public void Execute(int y)
{
int triIndex = y * LOD * 6; // Each row of quads generates LOD * 6 triangle indices (2 triangles per quad, 3 vertices per triangle)
for (int x = 0; x < LOD; x++)
{
// Get the indices of the square's vertices
int bottomLeft = y * verticesPerSide + x;
int bottomRight = bottomLeft + 1;
int topLeft = bottomLeft + verticesPerSide;
int topRight = topLeft + 1;
// First triangle (bottom-left to top-left to top-right)
triangles[triIndex++] = bottomLeft;
triangles[triIndex++] = (F) ? topRight : topLeft;
triangles[triIndex++] = (F) ? topLeft : topRight;
// Second triangle (bottom-left to top-right to bottom-right)
triangles[triIndex++] = bottomLeft;
triangles[triIndex++] = (F) ? bottomRight : topRight;
triangles[triIndex++] = (F) ? topRight : bottomRight;
}
}
}
}