4
\$\begingroup\$

I wrote a infinite terrain script which works! Saddly everytime the player moves a chunk it lags for a moment. I know my code isn't great but I'm here to learn why and get this working too :D

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GEN_InfiniteTerrain : MonoBehaviour
{
    public GameObject targetObject;
    public GameObject chunkObject;
    public int chunkSize;
    public float unitSize;

    public int renderDistance;
    Dictionary<Vector2, GameObject> gridOfChunks = new Dictionary<Vector2, GameObject>();
    List<Vector2> expectedChunkGridPositions = new List<Vector2>();

    public float noiseScale;

    // Infinite terrain values
    float absoluteChunkSize;

    private void Start()
    {
        // Calculate absolute chunk size
        GetAbsoluteChunkSize();

        // Generate base world
        GenerateBase();
    }

    Vector2 lastTargetGridPosition = Vector2.zero;
    private void LateUpdate()
    {
        // Get the targets position in world space
        Vector3 targetAbsolutePosition = targetObject.transform.position;

        // Convert the targets world position to grid position (/ 10 * 10 is just rounding to 10)
        Vector2 targetGridPosition = new Vector2();
        targetGridPosition.x = Mathf.RoundToInt(targetAbsolutePosition.x / 10) * 10 / absoluteChunkSize;
        targetGridPosition.y = Mathf.RoundToInt(targetAbsolutePosition.z / 10) * 10 / absoluteChunkSize;

        if (targetGridPosition - lastTargetGridPosition != Vector2.zero)
        {
            GenerateExpectedChunkAreas(targetGridPosition);
            UpdateChunkPositions(targetGridPosition);
        }

        lastTargetGridPosition = targetGridPosition;
    }

    void GenerateBase()
    {
        for (int x = -renderDistance / 2; x < renderDistance / 2; x++)
        {
            for (int z = -renderDistance / 2; z < renderDistance / 2; z++)
            {
                Vector2 gridPosition = new Vector2(x, z);
                Vector3 worldPosition = new Vector3(x * (unitSize * chunkSize), 0, z * (unitSize * chunkSize));
                GameObject chunk = Instantiate(chunkObject, worldPosition, Quaternion.identity);
                chunk.GetComponent<GEN_Chunk>().gridPosition = gridPosition;
                gridOfChunks.Add(gridPosition, chunk);
            }
        }
        GenerateExpectedChunkAreas(Vector2.zero);
    }

    void GenerateExpectedChunkAreas(Vector2 targetGridPosition)
    {
        expectedChunkGridPositions.Clear();
        for (int x = -renderDistance / 2; x < renderDistance / 2; x++)
        {
            for (int z = -renderDistance / 2; z < renderDistance / 2; z++)
            {
                Vector2 gridPosition = new Vector2(x, z) + targetGridPosition;
                expectedChunkGridPositions.Add(gridPosition);
            }
        }
    }

    void UpdateChunkPositions(Vector2 targetGridPosition)
    {
        List<Vector2> positionsWithoutChunks = new List<Vector2>();
        List<Vector2> positionsWithOldChunks = new List<Vector2>();

        for (int chunkCount = 0, x = -renderDistance / 2; x < renderDistance / 2; x++)
        {
            for (int z = -renderDistance / 2; z < renderDistance / 2; z++)
            {
                Vector2 gridPosition = new Vector2(x, z) + targetGridPosition;

                if(!gridOfChunks.ContainsKey(gridPosition))
                {
                    positionsWithoutChunks.Add(gridPosition);
                }
                chunkCount++;
            }
        }

        foreach (GameObject chunk in gridOfChunks.Values)
        {
            if(!expectedChunkGridPositions.Contains(chunk.GetComponent<GEN_Chunk>().gridPosition))
            {
                positionsWithOldChunks.Add(chunk.GetComponent<GEN_Chunk>().gridPosition);
            }
        }

        for (int i = 0; i < positionsWithOldChunks.Count; i++)
        {
            Vector3 worldPosition = new Vector3(positionsWithoutChunks[i].x * absoluteChunkSize, 0, positionsWithoutChunks[i].y * absoluteChunkSize);
            gridOfChunks[positionsWithOldChunks[i]].transform.position = worldPosition;

            // Recalculating noise for chunk based on its new position does lag more but even WITHOUT this it still stutters when player moves around. ( plan to learn threading just to calculate noise on seperate threads )
           // gridOfChunks[positionsWithOldChunks[i]].GetComponent<GEN_Chunk>().ApplyNoise();
        }
    }

    void GetAbsoluteChunkSize()
    {
        absoluteChunkSize = unitSize * chunkSize;
    }
}
\$\endgroup\$
1
\$\begingroup\$

I can only give you some general tips; I'm afraid Unity is outside my expertise.

  • I would consider redefining what renderDistance means. Currently it seems like a diameter; the rendered terrain consists of renderDistance2 chunks. What that means in practice is that every time you reference renderDistance, you divide it by 2. What happens if it is odd? It may be easier to treat this value as a radius instead (and just use values that are about twice as large).

  • You have a list of expectedGridChunkPositions, and it's not clear to me why it exists. It may just be that I'm not understanding the work done in the latter third of UpdateChunkPositions. What is troubling is that you have a lookup on this list (expectedChunkGridPositions.Contains). I doubt that your performance problems are coming from this alone... but if it's not possible to remove this List, you might consider changed it to a HashSet.

  • The latter two thirds of UpdateChunkPositions could be combined, in fact. You spend one foreach-loop building up positionsWithOldChunks, just to iterate through it and do work in another for-loop. Why not do the work right away?

  • It's possible that, as you examine the behavior of your game more closely, you will find opportunities for broad optimizations, and you will be able to keep the game running smoothly on a single thread. I recommend1 that you treat multithreading as a last resort, due to the inherent complexity increase it will cause in your code. You may find Visual Studio's performance analysis tooling very helpful here, especially its capability to show which functions are consuming the most resources.

  • It's also possible that, as the player crosses a chunk boundary, there's just too much terrain generation work to handle in the next dozen milliseconds, and lag will be unavoidable. In that case, good luck with your multithreading adventure. One thing you'll want to avoid as you explore that space is the use of Systems.Collections.Generic for anything that might be shared across threads. Instead, you'll need to use specialized collections like ConcurrentDictionary and ConcurrentBag, which have very handy functions with built-in locking mechanisms (such as GetOrAdd, and AddOrUpdate).

1On the other hand, I've never programmed a game before. Maybe multithreading is considered a best practice for Unity3d games? If so, then my recommendation isn't worth much.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.