While developing a game for studying in Unity I realized that many things I do are helpful for others. So here is another post about mapping indices to indices in other chunks.

Let's assume you have a chunk class for storing needed data like this:

public class Chunk
{
    private Vector2 index;
    private int chunkSize;
    private float[] heightmap;

    public float this[Vector2 v]
    {
        get { return this[CalculateIndex((int)v.x, (int)v.y, chunkSize)]; }
        set { this[CalculateIndex((int)v.y, (int)v.y, chunkSize)] = value; }
    }

    public Vector2 Index
    {
        get { return index; }
    }

    public int ChunkSize
    {
        get { return chunkSize; }
    }

    public Chunk(int chunkSize)
    {
        this.chunkSize = chunkSize;
        heightmap = new float[chunkSize * chunkSize];
    }

    private static int CalculateIndex(int x, int y, int range)
    {
        return y * range + x;
    }
}

I won't explain how to get this heightmap data into Unity because here and here is everything essential to get started.

Next you have a ChunkBehaviour setting up everything visible.

using UnityEngine;
using AliveDevil.Data;

namespace AliveDevil.Behaviour
{
    public class ChunkBehaviour : MonoBehaviour
    {
        private Chunk chunk;

        private void BuildMesh()
        {
            // Prepare for modifying meshes/vertices/triangles/uvs/stuff.

            for (int x = 0; x < chunk.ChunkSize; x++)
            {
                for (int z = 0; z < chunk.ChunkSize; z++)
                {
                    DrawRectangleMinMax(new Vector2(x, z), new Vector2(x + 1, z + 1));
                }
            }

            // Do everything else for building Meshes.
        }

        private void DrawRectangleMinMax(Vector2 min, Vector2 max)
        {
            Vector2 lowerLeftRectangle = min;
            Vector2 upperLeftRectangle = new Vector2(min.x, max.y);
            Vector2 upperRightRectangle = max;
            Vector2 lowerRightRectangle = new Vector2(max.x, min.y);

            Vector3 lowerLeft = new Vector3(lowerLeftRectangle.x, chunk[lowerLeftRectangle], lowerLeftRectangle.y);
            Vector3 upperLeft = new Vector3(upperLeftRectangle.x, chunk[upperLeftRectangle], upperLeftRectangle.y);
            Vector3 upperRight = new Vector3(upperRightRectangle.x, chunk[upperRightRectangle], upperRightRectangle.y);
            Vector3 lowerRight  = new Vector3(lowerRightRectangle.x, chunk[lowerRightRectangle], lowerRightRectangle.y);

            AddQuad(lowerLeft, upperLeft, upperRight, lowerRight);
        }

        private void AddQuad(Vector3 lowerLeft, Vector3 upperLeft, Vector3 upperRight, Vector3 lowerRight)
        {
            // Do whatever you need to build up the mesh.
            // Basically create two triangles from
            // lowerLeft, upperLeft, upperRight
            // and
            // upperRight, lowerRight, lowerLeft
        }
    }
}

First thing you'll notice is that you will get an IndexOutOfRangeException because this[(chunk.ChunkSize - 1) + 1, 0] is out of heightmap-Array. Now you could modify the two for-loops in ChunkBehaviour to fit the ChunkSize with chunk.ChunkSize - 1 but this will result in a visible blank between each chunk. Another solution would be to extend the heightmap with one index but this will result in a) duplicates of data and b) visible artifacts when changing one side of the chunk but not the adjacent. So I thought of a real solution to this: moving out of the chunk will result in reevaluating the index and creating another chunk if it's not existing. The ChunkBehaviour won't be touched anymore because the logic for changing the chunk is handled in the chunk itself. One thing you will need though is a ChunkManager (or anything else that comes into your mind that would fit).

using System.Collections.Generic;
using UnityEngine;

namespace AliveDevil.Data
{
    public class ChunkManager
    {
        const int ChunkSize = 64;
        private Dictionary<Vector2, Chunk> chunks = new Dictionary<Vector2, Chunk>;

        public GetChunk(Vector2 chunkIndex)
        {
            Chunk chunk;
            if (!chunk.TryGetValue(chunkIndex, out chunk))
                chunk = CreateChunk(chunkIndex);
            return chunk;
        }

        private Chunk CreateChunk(vector2 chunkIndex)
        {
            Chunk chunk = new Chunk(this, ChunkSize);

            // setup heightmap-values here!

            chunks.Add(chunkIndex, chunk);
            return chunk;
        }
    }
}

There are some modifications left for Chunk which are mapping the index to another chunk and determining which chunk should be indexed.

namespace AliveDevil.Data
{
    public class Chunk
    {
        private Vector2 index;
        private int chunkSize;
        private float[] heightmap;
        private ChunkManager chunkManager;

        public float this[Vector2 v]
        {
            get
            {
                return this[(int)v.x, (int)v.y];
            }
            set
            {
                this[(int)v.x, (int)v.y] = value;
            }
        }

        public float this[int x, int y]
        {
            get
            {
                if (InRange(x, y, chunkSize))
                    return this[CalculateIndex(x, y, chunkSize)];
                else
                    return ChunkManager.GetChunk(FindChnk(x, y, chunkSize, index))[x & (ChunkSize - 1), y & (ChunkSize - 1)];
            }
            set
            {
                if (InRange(x, y, chunkSize))
                    this[CalculateIndex(x, y, chunkSize)] = value;
            }
        }

        public Vector2 Index
        {
            get { return index; }
        }

        public int ChunkSize
        {
            get { return chunkSize; }
        }

        public ChunkManager ChunkManager
        {
            get { return chunkManager; }
        }

        public Chunk(ChunkManager chunkManager, int chunkSize)
        {
            this.chunkManager = chunkManager;
            this.chunkSize = chunkSize;
            heightmap = new float[chunkSize * chunkSize];
        }

        private static Vector2 FindChunk(int x, int y, int range, Vector2 currentChunk)
        {
            return new Vector2(currentChunk.x + x / range, currentChunk.y + y / range);
        }

        private static bool InRange(int x, int y, int range)
        {
            return !(x < 0 || x >= range || y < 0 || y >= range);
        }

        private static int CalculateIndex(int x, int y, int range)
        {
            return y * range + x;
        }
    }
}

If ChunkBehaviour is now trying to access X < 0 || X > 31 or Y < 0 || Y > 31 it is now out of range. This results in a FindChunk-Call which does nothing more than integer division and adding this to currentIndex. For X=32 this would be currentIndex.x + 32 / chunkSize = 1 with currentIndex = {0|0} and chunkSize=32. Now the ChunkManager checks if this chunk exists and creates one if necessary. The last thing to mention is the mapping: x & (ChunkSize - 1). x should be in range 0-31 which is done by applying binary arithmetic. If X=32 this would result in a term like 32 & 31 which is 0010.0000b & 0001.1111b returning the value 0000.0000b because both values have to be 1 at the same position. Same thing is valid for negative values. I'm using a 8-Bit-Signed Integer to demonstrate this here: -1 is 1111.1111b because the first bit determines whether the last seven bits should be subtracted by the first. So: 1000.0000b - 0111.1111b = 0000.0001. The application now has to add a negative sign infront of the result which then becomes -1. This is the answer for why this simple expression works in both ways. 1111.1111b & 0001.1111b = 0001.1111b = 31.

I hope this helps someone trying to figure this out.

Previous Post Next Post