Search code examples
c#unity-game-enginedynamicgrid

Moveable grid in Unity C#


I am working on a moveable grid system for my voxel game, I implemented most of it, but I keep getting stuck at making the actual Move function. This is the code without the Move function implemented:

using UnityEngine;
using System;

public class Grid<T>
{
    private T[] data;
    public Vector3Int position;
    public Vector3Int size;
    public int iSize;

    public Grid(Vector3Int size)
    {
        this.position = new Vector3Int(0, 0, 0);
        this.size = size;
        this.iSize = size.x * size.y * size.z;
        this.data = new T[size.x * size.y * size.z];
    }

    public T this[int x, int y, int z]
    {
        get
        {
            Vector3Int position = new Vector3Int(x, y, z);
            if (!IsValidIndex(position - this.position))
            {
                throw new IndexOutOfRangeException("Invalid position");
            }

            Vector3Int positionRelative = PositionToRelative(position);
            return this.data[VectorToIndex(positionRelative)];
        }
        set
        {
            Vector3Int position = new Vector3Int(x, y, z);
            if (!IsValidIndex(position - this.position))
            {
                throw new IndexOutOfRangeException("Invalid position");
            }

            Vector3Int positionRelative = PositionToRelative(position);
            this.data[VectorToIndex(positionRelative)] = value;
        }
    }

    public void Move(Vector3Int newPosition)
    {
        Vector3Int offset = PositionToRelative(newPosition);
        T[] newData = new T[data.Length];

        this.data = newData;
        this.position = newPosition;
    }

    private bool IsPositionInRange(Vector3Int position, Vector3Int start, Vector3Int end)
    {
        int minX = Mathf.Min(start.x, end.x);
        int maxX = Mathf.Max(start.x, end.x);
        int minY = Mathf.Min(start.y, end.y);
        int maxY = Mathf.Max(start.y, end.y);
        int minZ = Mathf.Min(start.z, end.z);
        int maxZ = Mathf.Max(start.z, end.z);
        return position.x >= minX
            && position.x <= maxX
            && position.y >= minY
            && position.y <= maxY
            && position.z >= minZ
            && position.z <= maxZ;
    }

    private Vector3Int PositionToRelative(Vector3Int position)
    {
        return position - this.position;
    }

    private bool IsValidIndex(Vector3Int position)
    {
        int index = VectorToIndex(position);

        return index >= 0 && index < this.iSize;
    }

    private bool IsValidIndex(int index)
    {
        return index >= 0 && index < this.iSize;
    }

    private int VectorToIndex(Vector3Int position)
    {
        return position.x + this.size.x * (position.y + this.size.y * position.z);
    }
}

This is the code Im using to test it:

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

public class Tester : MonoBehaviour
{
    void PrintMatrix<T>(Grid<T> grid)
    {
        for (int x = 0; x < grid.size.x; x++)
        {
            string output = "\n";

            for (int y = 0; y < grid.size.y; y++)
            {
                Vector3Int position = new Vector3Int(x, y, 0) + grid.position;
                output += grid[position.x, position.y, position.z];

                if (y < grid.size.y - 1)
                {
                    output += " ";
                }
            }

            Debug.Log(output);
        }
    }

    void Start()
    {
        Vector3Int size = new Vector3Int(4, 4, 1);
        Grid<int> grid = new Grid<int>(size);

        for (int x = 0; x < size.x; x++)
        {
            for (int y = 0; y < size.y; y++)
            {
                grid[x, y, 0] = x + size.y * y + 1;
            }
        }

        PrintMatrix<int>(grid);

        Debug.Log("-------- Amogus ------------");

        grid.Move(new Vector3Int(-1, 0, 0));

        PrintMatrix<int>(grid);
    }

    void Update() { }
}

The example shifts the grid in the left.

First time it logs the matrix correctly:

1 5 9 13

2 6 10 14

3 7 11 15

4 8 12 16

However the second time its incorrect. What it prints with the current move function:

What it's supposed to print:

0 1 5 9

0 2 6 10

0 3 7 11

0 4 8 12

What it prints:

2 6 10 14

3 7 11 15

4 8 12 16

0 9 13 0

I've tried this Move function, but its not working, I could not figure out how to get good startPosition and endPosition.

    public void Move(Vector3Int position)
    {
        T[] newData = new T[data.Length];
        Vector3Int positionRelative = PositionToRelative(position);

        Vector3Int startPosition = new Vector3Int(
            positionRelative.x >= 0 ? positionRelative.x : this.size.x + positionRelative.x,
            positionRelative.y >= 0 ? positionRelative.y : this.size.y + positionRelative.y,
            positionRelative.z >= 0 ? positionRelative.z : this.size.z + positionRelative.z
        );
        Vector3Int endPosition = new Vector3Int(
            positionRelative.x < 0 ? this.size.x : positionRelative.x,
            positionRelative.y < 0 ? this.size.y : positionRelative.y,
            positionRelative.z < 0 ? this.size.z : positionRelative.z
        );

        Debug.Log(positionRelative + " " + startPosition + " " + endPosition);

        for (int x = 0; x < size.x; x++)
        {
            for (int y = 0; y < size.y; y++)
            {
                for (int z = 0; z < size.z; z++)
                {
                    Vector3Int currentPosition = new Vector3Int(x, y, z);
                    int index = VectorToIndex(currentPosition);
                    int translationIndex = VectorToIndex(currentPosition - positionRelative);

                    if (
                        IsPositionInRange(currentPosition, startPosition, endPosition)
                        || !IsValidIndex(translationIndex)
                    )
                    {
                        newData[index] = default(T);
                        continue;
                    }

                    newData[index] = data[translationIndex];
                }
            }
        }

        this.position = position;
        data = newData;
    }

How it should look like

The first grid with position 0, 0, 0 shows how a 2x2 part of it would look like, and the second grid with position -1, 0, 0 shows how a 2x2 part of it should look shifted, and under them are the small 2x2 grids shown. THe one which is 0 6 0 7 is how it should look in code, 0 because the code cannot guess that its supposed to be 2 and 3.


Solution

  • The biggest issue seems to be your PrintMatrix.

    It prints the values the following way

    • For each X prints a line

      => X goes top to bottom

    • In each line prints 4 consecutive Y values

      => Y goes left to right

    Basically your What it prints is closer to what I would expect as it correctl shifts everything one step upwards (negative X direction)

    It should probably rather be e.g.

    public void PrintMatrix(Grid<T> grid)
    {
        var output = new StringBuilder();
    
        for (var y = 0; y < grid.size.y; y++)
        {
            for (var x = 0; x < grid.size.x; x++)
            {
                var value = grid[x, y, 0];
                if(value < 10) output.Append(' ')
                output.Append(value);
    
                output.Append(' ');
            }
    
            output.Append('\n');
        }
    
        Debug.Log(output.ToString());
    }
    

    which would print out a line for each Y (=> top to bottom) and then within the line X (=> left to right).


    Actually I would probably go for a wrap-around of the indices and have something like e.g.

    private int VectorToIndex(Vector3Int index) => VectorToIndex(index.x, index.y, index.z);
    
    private int VectorToIndex(int x, int y, int z)
    {
        x = x.WrapAround(size.x);
        y = y.WrapAround(size.y);
        z = z.WrapAround(size.z);
    
        return x + size.x * (y + size.y * z);
    }
    

    with extension method

    public static class IntExtensions
    {
        public static int WrapAround(this int value, int maxValue)
        {
            return ((value % maxValue) + maxValue) % maxValue;
        }
    }
    

    for wrapping the index at the borders

    =>

    first print

    1  2   3  4 
    5  6   7  8 
    9  10 11 12 
    13 14 15 16
    

    print after moving (-1 ,0 ,0)

     2  3  4  1 
     6  7  8  5 
    10 11 12  9 
    14 15 16 13
    

    => all values shifted one to the left (= negative X direction)


    And just a sidenote you know you can also directly use the vector as indexer and do

    public T this[Vector3Int index]
    {
        get
        {
            var positionRelative = PositionToRelative(position);
            return data[VectorToIndex(positionRelative)];
        }
        set
        {
            var positionRelative = PositionToRelative(position);
            data[VectorToIndex(positionRelative)] = value;
        }
    }
    

    and/or even keep both for convenience

    public T this[int x, int y, int z]
    {
        get => this[new Vector3Int(x, y, z)];
        set => this[new Vector3Int(x, y, z)] = value;
    }