I'm having trouble figuring out how why this script won't generate chunks seamlessly next to each over based on a Perlin noise
I am quite new to procedural generation but not new to Unity and C#.
I have made this script here, It will generate chunks of terrains around the main camera and have each chunk randomly be affected by a procedurally generated height map, a similar system that can be seen in the likes of Minecraft.
Obviously, I made it this way in hopes of a system where the player could move around the world forever and generate the world around them depending on where they were with a chunk system to reduce lag and such.
The problem is that each different chunk doesn't seem to tile seamlessly the adjacent chunks. I assume this is because of my lack of understanding of Perlin noise but I can't crack down on the issue even though I swear that it should work properly when I go through the code myself.
It DOES however correctly generate the same height map with the same seed and for the same chunks in the same positions.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Generator : MonoBehaviour
{
[SerializeField] private int seed; // Seed for random generation
[SerializeField] private int chunkSize; // Size of chunks in Unity units
[SerializeField] private int chunkLoadRadius; // Number of chunks to load around target
[SerializeField] private GameObject[] objectPrefabs; // Prefabs of objects to generate
[SerializeField] private int maxItemsPerChunk;
private Dictionary<Vector2Int, GameObject[]> loadedChunks = new Dictionary<Vector2Int, GameObject[]>(); // Dictionary to store loaded chunks
private Vector2Int lastChunkCoord; // Coordinates of last loaded chunk
private Transform targetTransform; // Transform of target
[SerializeField] private Transform cityParent;
[SerializeField] private float depth;
[SerializeField] private float scale;
private void Start()
{
targetTransform = Camera.main.transform;
lastChunkCoord = GetChunkCoord(targetTransform.position);
GenerateChunksAroundTarget();
}
private void Update()
{
// Check if target has moved into a new chunk
Vector2Int currentChunkCoord = GetChunkCoord(targetTransform.position);
if (currentChunkCoord != lastChunkCoord)
{
lastChunkCoord = currentChunkCoord;
GenerateChunksAroundTarget();
}
}
private void GenerateChunksAroundTarget()
{
// Unload any previously loaded chunks that are outside of the chunk load radius
List<Vector2Int> chunksToUnload = new List<Vector2Int>();
foreach (KeyValuePair<Vector2Int, GameObject[]> loadedChunk in loadedChunks)
{
if (Vector2Int.Distance(loadedChunk.Key, lastChunkCoord) > chunkLoadRadius)
{
chunksToUnload.Add(loadedChunk.Key);
foreach (GameObject obj in loadedChunk.Value)
{
Destroy(obj);
}
}
}
foreach (Vector2Int chunkCoord in chunksToUnload)
{
loadedChunks.Remove(chunkCoord);
}
// Load any new chunks that are within the chunk load radius
for (int x = -chunkLoadRadius; x <= chunkLoadRadius; x++)
{
for (int y = -chunkLoadRadius; y <= chunkLoadRadius; y++)
{
Vector2Int chunkCoord = lastChunkCoord + new Vector2Int(x, y);
if (!loadedChunks.ContainsKey(chunkCoord))
{
loadedChunks[chunkCoord] = GenerateChunk(chunkCoord);
}
}
}
}
private Vector2Int GetChunkCoord(Vector3 position)
{
return new Vector2Int(Mathf.FloorToInt(position.x / chunkSize), Mathf.FloorToInt(position.z / chunkSize));
}
private GameObject[] GenerateChunk(Vector2Int chunkCoord)
{
Random.InitState(seed + chunkCoord.GetHashCode());
GameObject[] objects = new GameObject[Random.Range(1, maxItemsPerChunk)]; // Generate between 1 and 10 objects per chunk
Vector3 BasePos = new Vector3(chunkCoord.x * chunkSize, 0, chunkCoord.y * chunkSize);
TerrainData terrainData = new TerrainData();
terrainData.heightmapResolution = chunkSize;
terrainData.size = new Vector3(chunkSize, depth, chunkSize);
terrainData.SetHeights(0, 0, GenerateHeights(chunkCoord.x * chunkSize, chunkCoord.y * chunkSize, chunkCoord.GetHashCode()));
objects[0] = Terrain.CreateTerrainGameObject(terrainData);
objects[0].transform.position = BasePos;
objects[0].transform.parent = cityParent;
for (int i = 1; i < objects.Length; i++)
{
Vector3 objectPosition = new Vector3(
Random.Range(chunkCoord.x * chunkSize, (chunkCoord.x + 1) * chunkSize),
0,
Random.Range(chunkCoord.y * chunkSize, (chunkCoord.y + 1) * chunkSize)
);
GameObject objectPrefab = objectPrefabs[Random.Range(0, objectPrefabs.Length)];
objects[i] = Instantiate(objectPrefab, objectPosition, Quaternion.identity, cityParent);
}
return objects;
}
float[,] GenerateHeights(float offsetX, float offsetY, int hash)
{
float[,] heights = new float[chunkSize, chunkSize];
for (int x = 0; x < chunkSize; x++)
{
for (int y = 0; y < chunkSize; y++)
{
heights[x, y] = CalculateHeight(x, y, offsetX, offsetY, hash);
}
}
return heights;
}
float CalculateHeight(int x, int y, float offsetX, float offsetY, int hash)
{
float xCoord = (float) x / chunkSize * scale;
float yCoord = (float) y / chunkSize * scale;
//Random.InitState(seed + hash);
return Mathf.PerlinNoise(offsetX + xCoord, offsetY + yCoord);
}
}
In order to use this script
- Open a new unity3d project
- apply this script to an empty
- depth 10, scale, 10, chunk size 129, radius 2
- add a "cityParent" transform
- press play and move the camera around to see how the chunks load and unload properly but the Perlin noise in each doesn't quite match up at all
I was hoping somebody who's probably much better at this than me could give me some insight on what it is that I'm doing wrong? and ideally with a modified script or a re-write of the area I had screwed up.