I want to build a 3D grid where I can assign an object to every single node in that grid. Imagine the DS9 mine field for example.

Right now I am building the grid by using a dictionary where the key is a string in the format of "x_y_z" so for a 3x3x3 grid I have values like

0_0_0
1_2_3
3_1_2

To be honest, this feels very wrong to me πŸ˜ƒ Is there a more efficient way to solve this? Maybe storing an array inside an array inside an array?

Max

    max_godot Yeah use a three dimensional array i.e. array of arrays of arrays. Depending on what you need this for, you may find the GridMap node to be of some use.

    Toxe in theory it could be thousands of cells, realistically it’s going to be between 1-1000.

    • Toxe replied to this.
      var grid_size = Vector3(10, 4, 5)
      var grid_data = {}
      
      func load_grid():
      	for x in range(grid_size.x):
      	    for y in range(grid_size.y):
                         for z in range(grid_size.z):
      
      			grid_data[Vector2(x, y, z)] = "" #your object reference 

        Loxyrak thx! I assume

        grid_data[Vector2(x, y, z)] = "" #your object reference 

        should be

        grid_data[Vector3(x, y, z)] = "" #your object reference 

        instead?

        Might wanna go for Vector3i instead, since x, y and z are integer (assuming you are using Godot 4).

        @max_godot @Loxyrak I made a quick benchmark.

        I tested three different methods of creating and iterating through a 3-dimensional int array of size 10x10x10. I chose integers for simplicity.

        What we measure in each test is creating the array and then iterating through it 1000 times and increasing each value by one. For testing I had a couple of asserts at the end of each test but those were for size 3 x 3 x 3 but I left them in here to show how to access the array values. I did not measure the asserts.

        const size_x := 10
        const size_y := 10
        const size_z := 10
        
        
        func value(x: int, y: int, z: int) -> int:
            return size_z * size_y * z + size_x * y + x

        The value() helper function returns the initial value at position x/y/z.

        Method 1: Multidimensional array

        Access values with data[z][y][x].

        func bench01(iterations: int):
            var data := []
        
            for z in size_z:
                var grid := []
                for y in size_y:
                    var row: Array[int] = []
                    for x in size_x:
                        row.append(value(x, y, z))
                    grid.append(row)
                data.append(grid)
        
            for i in iterations:
                for z in size_z:
                    for y in size_y:
                        for x in size_x:
                            data[z][y][x] += 1
        
            # assert(data[0][0][0] == 1000)
            # assert(data[0][0][2] == 1002)
            # assert(data[1][0][1] == 1010)
            # assert(data[1][1][1] == 1013)
            # assert(data[2][1][0] == 1021)
            # assert(data[2][2][2] == 1026)

        Method 2: Dictionary with Vector3i keys

        Access values with data[Vector3i(x, y, z)].

        func bench02(iterations: int):
            var data := {}
        
            for x in size_x:
                for y in size_y:
                    for z in size_z:
                        data[Vector3i(x, y, z)] = value(x, y, z)
        
            for i in iterations:
                for x in size_x:
                    for y in size_y:
                        for z in size_z:
                            data[Vector3i(x, y, z)] += 1
        
            # assert(data[Vector3i(0, 0, 0)] == 1000)
            # assert(data[Vector3i(2, 0, 0)] == 1002)
            # assert(data[Vector3i(1, 0, 1)] == 1010)
            # assert(data[Vector3i(1, 1, 1)] == 1013)
            # assert(data[Vector3i(0, 1, 2)] == 1021)
            # assert(data[Vector3i(2, 2, 2)] == 1026)

        Method 3: Linear array and we calculate the index by hand

        Access values with data[size_z * size_y * z + size_x * y + x].

        func bench03(iterations: int):
            var data: Array[int] = []
            data.resize(size_z * size_y * size_x)
        
            for z in size_z:
                for y in size_y:
                    for x in size_x:
                        data[size_z * size_y * z + size_x * y + x] = value(x, y, z)
        
            for i in iterations:
                for z in size_z:
                    for y in size_y:
                        for x in size_x:
                            data[size_z * size_y * z + size_x * y + x] += 1
        
            # assert(data[size_z * size_y * 0 + size_x * 0 + 0] == 1000)
            # assert(data[size_z * size_y * 0 + size_x * 0 + 2] == 1002)
            # assert(data[size_z * size_y * 1 + size_x * 0 + 1] == 1010)
            # assert(data[size_z * size_y * 1 + size_x * 1 + 1] == 1013)
            # assert(data[size_z * size_y * 2 + size_x * 1 + 0] == 1021)
            # assert(data[size_z * size_y * 2 + size_x * 2 + 2] == 1026)

        Results

        • Method 1: 191 ms
        • Method 2: 122 ms
        • Method 3: 90 ms

        Method 3 (one linear array) is the fastest which isn't surprising but only under the condition that we always calculate the index on the spot by hand and don't use some kind of index(x, y, z) function that returns the array index as one would usually do to reduce errors. This would completely kill performance because every function call in GDScript is expensive. Calculating the index by hand makes this method faster but also more clunky.

        I was surprised to see that using a dictionary with Vector3i as keys is faster than using a multidimensional array. So in this case this might actually be the best tradeoff between usability and performance.

        Using a 3-dimensional array was surprisingly slow but on the other hand we access the array three times whereas the other three methods only make one array access and each [] operator is probably a function call which as I said are slow in GDScript.

        BTW I know that we can optimize the loops (or use only one for method 3) but I wanted to check the most common use case which would probably be calling some kind of please_get_me_my_array_value_at(x, y, z).

          Toxe thank you so much for the detailed analysis! So dict[Vector3i] is gonna be my way forward then! πŸ™‚

          Toxe You should benchmark fully random access for each as well.

          • Toxe replied to this.

            I feel like I remember someone testing strings vs vec3s as keys and strings won... but that might not have even been Godot. Could be worth checking though

            • Toxe replied to this.

              award I feel like I remember someone testing strings vs vec3s as keys and strings won... but that might not have even been Godot. Could be worth checking though

              I thought about it but I assumed that it would be slower so I left it out but sure, it's easy enough to do.

              Method 4: Dictionary with String keys

              func bench04(iterations: int):
                  var data := {}
              
                  for x in size_x:
                      for y in size_y:
                          for z in size_z:
                              data["%d,%d,%d" % [x, y, z]] = value(x, y, z)
                              # data[str(x) + "," + str(y) + "," + str(z)] = value(x, y, z)
              
                  for i in iterations:
                      for x in size_x:
                          for y in size_y:
                              for z in size_z:
                                  data["%d,%d,%d" % [x, y, z]] += 1
                                  # data[str(x) + "," + str(y) + "," + str(z)] += 1

              I checked two different ways of creating the String keys:

              1. data["%d,%d,%d" % [x, y, z]]: 1136 ms
              2. data[str(x) + "," + str(y) + "," + str(z)]: 1356 ms

              My preferred way to create formatted Strings is option 1 so it's nice to see that it is also faster. But that is also what I would have expected.

              Now in comparison to the other benchmarks using String keys is roughly 5 to 10 times slower. I expected it to be slower but not by that much. πŸ˜†

              xyz You should benchmark fully random access for each as well.

              True but I was too lazy. πŸ˜‰

              But I have this one small project that I made as a benchmark for... um, I don't remember. Something. But I always butcher it for some small ad-hoc tests like these and I thought about expanding it to have a nice, pleasant "Godot GDScript Benchmarks" project where I can collect all those small tests that I make over time in one comprehensive project. And also make usability a bit simpler and most importantly run more measurements and then calculate the 50% and 95% median and maybe show a nice graph or something.

              So there is a good chance that I will come back to this. πŸ˜‰