camden carr
camden carr

Reputation: 1

Problem creating seamless chunk generation

I am working on creating infinite chunks of terrain, with variation in the height using perlin noise in unity, and am encountering problems getting the seams to align to create a tileable environment Screenshot of problem

TerrainChunk.cs

using UnityEngine;
using System.Linq; // Add this line

public class TerrainChunk
{
    public GameObject chunkObject;
    private Terrain terrain;
    private TerrainData terrainData;
    private int width, height, depth;
    private float scale;
    private int seed;
    private Vector3 position;
    private TerrainLayer[] terrainLayers;
    private Material terrainMaterial;

    
    private GameObject npcPrefab;  // Add this line to hold the NPC prefab.

    private NPCSpawner npcSpawner; // Reference to the NPCSpawner component

    public TerrainChunk(int width, int height, int depth, float scale, int seed, Vector3 position, TerrainLayer[] terrainLayers, Material terrainMaterial, GameObject npcPrefab) // Add npcPrefab parameter
    {
        this.width = width;
        this.height = height;
        this.depth = depth;
        this.scale = scale;
        this.seed = seed;
        this.position = position;
        this.terrainLayers = terrainLayers;
        this.terrainMaterial = terrainMaterial;
        this.npcPrefab = npcPrefab; // Assign to field


        CreateTerrainChunk();
    }

    private void CreateTerrainChunk()
    {
        // Create a new GameObject for the terrain chunk
        chunkObject = new GameObject("TerrainChunk");
        chunkObject.transform.position = position;

        // Add Terrain component and create TerrainData
        terrain = chunkObject.AddComponent<Terrain>();
        terrainData = new TerrainData();
        terrain.terrainData = terrainData;

        // Set terrain data properties
        terrainData.heightmapResolution = width + 1;
        terrainData.size = new Vector3(width, depth, height);
        terrainData.SetHeights(0, 0, GenerateHeights());

        // Set terrain layers
        if (terrainLayers != null && terrainLayers.Length > 0)
        {
            terrainData.terrainLayers = terrainLayers;
        }

        // Generate and apply the splatmap
        GenerateAndApplySplatmap();

        // Assign the material to the terrain
        if (terrainMaterial != null)
        {
            terrain.materialTemplate = terrainMaterial;
        }
        else
        {
            Debug.LogError("No material assigned to terrain");
        }

        // Optionally, add a TerrainCollider
        chunkObject.AddComponent<TerrainCollider>().terrainData = terrainData;

        // Existing code for setting up terrain...

        // Find or Add an NPCSpawner component
        npcSpawner = chunkObject.GetComponent<NPCSpawner>() ?? chunkObject.AddComponent<NPCSpawner>();
        npcSpawner.npcPrefab = this.npcPrefab;  // Ensure the NPC spawner knows which prefab to use
        npcSpawner.TrySpawnNPCs(position, width, height);
    }

    private float[,] GenerateHeights()
    {
        float[,] heights = new float[width, height];
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                heights[x, y] = CalculateHeight(x, y);
            }
        }
        return heights;
    }
    // This is the issue 
    private float CalculateHeight(int x, int y)
    {
        float xCoord = (position.x + x) / width * scale + seed * 0.0001f;
        float yCoord = (position.z + y) / height * scale + seed * 0.0001f;

        // Base layer
        float baseHeight = Mathf.PerlinNoise(xCoord, yCoord);
        return baseHeight;
    }

    private void GenerateAndApplySplatmap()
    {
        // Get the terrain size and heightmap
        int terrainWidth = terrainData.alphamapWidth;
        int terrainHeight = terrainData.alphamapHeight;
        int numTextures = terrainLayers.Length;

        // Create a new splatmap array
        float[,,] splatmap = new float[terrainWidth, terrainHeight, numTextures];

        // Loop through all terrain points
        for (int x = 0; x < terrainWidth; x++)
        {
            for (int y = 0; y < terrainHeight; y++)
            {
                // Normalized coordinates
                float xCoord = (float)x / (terrainWidth - 1);
                float yCoord = (float)y / (terrainHeight - 1);

                // Get the height at this point
                float heightAtPoint = terrainData.GetHeight((int)(xCoord * terrainData.heightmapResolution), (int)(yCoord * terrainData.heightmapResolution));

                // Calculate weights based on height
                float[] weights = new float[numTextures];
                for (int i = 0; i < numTextures; i++)
                {
                    // Simple gradient based on height for this example
                    if (i == 0)
                    {
                        weights[i] = Mathf.Clamp01(1 - heightAtPoint / depth); // Low areas (e.g., grass)
                    }
                    else if (i == 1)
                    {
                        weights[i] = Mathf.Clamp01((heightAtPoint / depth - 0.3f) * 2); // Mid areas (e.g., rock)
                    }
                    else if (i == 2)
                    {
                        weights[i] = Mathf.Clamp01((heightAtPoint / depth - 0.6f) * 3); // High areas (e.g., snow)
                    }
                }

                // Normalize weights
                float totalWeight = weights.Sum();
                for (int i = 0; i < numTextures; i++)
                {
                    weights[i] /= totalWeight;
                    splatmap[x, y, i] = weights[i];
                }
            }
        }

        // Assign the splatmap to the terrain
        terrainData.SetAlphamaps(0, 0, splatmap);
    }

    public void Unload()
    {
        // Destroy the GameObject and cleanup
        GameObject.Destroy(chunkObject);
        terrain = null;
        terrainData = null;
    }

    public bool IsWithinViewDistance(Vector2Int playerChunkCoord, int viewDistance)
    {
        Vector2Int chunkCoord = new Vector2Int(
            Mathf.FloorToInt(position.x / width),
            Mathf.FloorToInt(position.z / height)
        );

        int distance = Mathf.Max(Mathf.Abs(playerChunkCoord.x - chunkCoord.x), Mathf.Abs(playerChunkCoord.y - chunkCoord.y));
        return distance <= viewDistance;
    }
}

TerrainManager.cs

using System.Collections.Generic;
using UnityEngine;


public class TerrainManager : MonoBehaviour
{
    public int chunkSize = 256;
    public int chunkDepth = 20;
    public float scale = 20f;
    public int seed;
    public TerrainLayer[] terrainLayers;
    public Material terrainMaterial;

    private Dictionary<Vector2Int, TerrainChunk> terrainChunks = new Dictionary<Vector2Int, TerrainChunk>();
    private Transform player;
    private int viewDistance = 2; // Number of chunks to load around the player

    [SerializeField] private GameObject npcPrefab; // Add this line

    void Start()
    {
        // Set the seed for the terrain
        if (seed == 0)
        {
            seed = Random.Range(0, int.MaxValue);
        }

        // Get player transform
        player = GameObject.FindGameObjectWithTag("Player").transform;

        // Generate initial terrain
        UpdateTerrainChunks();
    }

    void Update()
    {
        // Update terrain chunks based on player position
        UpdateTerrainChunks();
    }

    private void UpdateTerrainChunks()
    {
        Vector2Int playerChunkCoord = new Vector2Int(
            Mathf.FloorToInt(player.position.x / chunkSize),
            Mathf.FloorToInt(player.position.z / chunkSize)
        );

        GameObject npcPrefab = Resources.Load<GameObject>("NPC"); // Replace "NameOfYourPrefab" with the actual name

        List<Vector2Int> chunksToRemove = new List<Vector2Int>();

        // Check and unload distant chunks
        foreach (var chunkCoord in terrainChunks.Keys)
        {
            if (!IsChunkWithinViewDistance(chunkCoord, playerChunkCoord))
            {
                terrainChunks[chunkCoord].Unload();
                chunksToRemove.Add(chunkCoord);
            }
        }

        // Remove unloaded chunks from dictionary
        foreach (var chunkCoord in chunksToRemove)
        {
            terrainChunks.Remove(chunkCoord);
        }

        // Load new chunks around the player
        for (int xOffset = -viewDistance; xOffset <= viewDistance; xOffset++)
        {
            for (int yOffset = -viewDistance; yOffset <= viewDistance; yOffset++)
            {
                Vector2Int chunkCoord = new Vector2Int(playerChunkCoord.x + xOffset, playerChunkCoord.y + yOffset);

                if (!terrainChunks.ContainsKey(chunkCoord))
                {
                    Vector3 chunkPosition = new Vector3(chunkCoord.x * chunkSize, 0, chunkCoord.y * chunkSize);
                    TerrainChunk newChunk = new TerrainChunk(chunkSize, chunkSize, chunkDepth, scale, seed, chunkPosition, terrainLayers, terrainMaterial, npcPrefab);
                    terrainChunks.Add(chunkCoord, newChunk);
                }
            }
        }
    }

    private bool IsChunkWithinViewDistance(Vector2Int chunkCoord, Vector2Int playerChunkCoord)
    {
        int distance = Mathf.Max(Mathf.Abs(playerChunkCoord.x - chunkCoord.x), Mathf.Abs(playerChunkCoord.y - chunkCoord.y));
        return distance <= viewDistance;
    }
}

I have tried finding the source of the issue, and looking into other people's issues with seamless generation but have had a hard time implementing them into my code.

Upvotes: 0

Views: 56

Answers (1)

Bunny83
Bunny83

Reputation: 1119

It's hard to tell from the image. However are you sure that your x and y directions of your terrain actually match the x and y direction of the chunk / world? My guess is that you may have a rotated terrain (so the local origin x==0, y==0 is not the lower corner of your chunk). So make sure that the position of your heights[0,0] is actually at the lower position in the x-z world grid

Upvotes: 0

Related Questions