• Hey! Guest! The 39th GMC Jam will take place between November 26th, 12:00 UTC and November 30th, 12:00 UTC. Why not join in! Click here to find out more!

GMS 2.3+ Water and sand with a ds_grid

Hello everyone,
I am on a little project where you can create differents elements (water, sand....)
I am inspired by the amazing "Powder Game" (a Physics simulation game)


https://dan-ball.jp/en/javagame/dust/

So for now i just start my projet and maybe you can have ideas to optimise my projet, advices...
You can see my project actually (i am on the very begining).
I just implemented water and sand.
fdsfazfza.png

This is my step event

GML:
f mouse_check_button(mb_left) {ds_grid_set(grid,xxx,yyy,1)}
if mouse_check_button(mb_right) {ds_grid_set(grid,xxx,yyy,2)}

for(var xx = 0; xx < ds_grid_width(grid); xx++) {
    for(var yy = 0; yy < ds_grid_height(grid); yy++) {
#region//--------------------------SAND--------------------------//
        if ds_grid_get(grid, xx, yy) = 1 {
             if ds_grid_get(grid, xx, yy + 1) = 0 {//Set gravity (nothing under)
                ds_grid_set(grid, xx, yy + 1, 1)
                ds_grid_set(grid, xx, yy,0)
                }
            if ds_grid_get(grid, xx, yy + 1) = 2 {//water under
                ds_grid_set(grid, xx, yy + 1, 1)
                ds_grid_set(grid, xx, yy, 2)
                }
            else if ds_grid_get(grid, xx - 1, yy + 1) = 0 {//nothing left + down
                ds_grid_set(grid, xx - 1, yy + 1, 1)
                ds_grid_set(grid, xx, yy,0)
                }
            else if ds_grid_get(grid, xx + 1, yy + 1) = 0 {//nothing right + down
                ds_grid_set(grid, xx + 1, yy + 1, 1)
                ds_grid_set(grid, xx, yy,0)
                }
        }
#endregion

#region//--------------------------WATER--------------------------//
            if ds_grid_get(grid, xx, yy) = 2 {
             if ds_grid_get(grid, xx, yy + 1) = 0 {//Set gravity (nothing under)
                ds_grid_set(grid, xx, yy + 1, 2)
                ds_grid_set(grid, xx, yy,0)
                }
            else if ds_grid_get(grid, xx - 1, yy + 1) = 0 {//nothing left + down
                ds_grid_set(grid, xx - 1, yy + 1, 2)
                ds_grid_set(grid, xx, yy,0)
                }
            else if ds_grid_get(grid, xx + 1, yy + 1) = 0 {//nothing right + down
                ds_grid_set(grid, xx + 1, yy + 1, 2)
                ds_grid_set(grid, xx, yy,0)
                }
            else if ds_grid_get(grid, xx + 1, yy) = 0 {//nothing left
                ds_grid_set(grid, xx + 1, yy, 2)
                ds_grid_set(grid, xx, yy,0)
                }
            else if ds_grid_get(grid, xx - 1, yy) = 0 {//nothing right
                ds_grid_set(grid, xx - 1, yy, 2)
                ds_grid_set(grid, xx, yy,0)
                }
            }
#endregion
    }
}
In the draw event

Code:
for(var xx = 0; xx < ds_grid_width(grid); xx++){
    for(var yy = 0; yy < ds_grid_height(grid); yy++){
        if ds_grid_get(grid, xx, yy) = 1 {
            //draw_rectangle_color(xx*cellsize, yy*cellsize, xx*cellsize+8, yy*cellsize+8, c_yellow, c_yellow, c_yellow, c_yellow, false); 
            draw_sprite(spr_ground,0,xx*cellsize,yy*cellsize)
        }
        if ds_grid_get(grid, xx, yy) = 2 {
            //draw_rectangle_color(xx*cellsize, yy*cellsize, xx*cellsize+8, yy*cellsize+8, c_blue, c_blue, c_blue, c_blue, false); 
            draw_sprite(spr_water,0,xx*cellsize,yy*cellsize)
        }
    }
}

PS : i also tried to do the same things with 2d arrays but i don't see any difference with the performances maybe i am wrong???
 
Last edited:
I think the best way to optimize my project is to not loop in the whole grid and rather create a list (or an array or a struct????).
In this list i i will put all the "active" cells. I am searching the best waay to do it, for now without success
 

Simon Gust

Member
The first thing that causes unneccesary slowdown is having the ds_grid_width / height inside the for-loops, they will reevaluate every iteration.
If you don't resize the grid mids physics, you can just pre-initialize them to variables.

I notice with your code, is that it's possible for a cell to perform both iterations of physics (And even more iterations of sub-physics).
Unless this effect is desired, you need to put an else-statement everywhere you have an if-statement. This should also eliminate the problem of water / sand duplicating themselves.

When flowing to a side, you could choose a random side to flow to instead of always left and then right.

I end up with this code
Code:
if mouse_check_button(mb_left) {ds_grid_set(grid,xxx,yyy,1)}
if mouse_check_button(mb_right) {ds_grid_set(grid,xxx,yyy,2)}

var side_array = [-1, 1]; // array
var w = ds_grid_width(grid);
var h = ds_grid_height(grid);

for(var xx = 1; xx < w-1; xx++) {
for(var yy = 1; yy < h-1; yy++) {

    var tile = ds_grid_get(grid, xx, yy);
    if (tile == 1) //--------------------------SAND---------------------------//
    {
        // gravity
        if (ds_grid_get(grid, xx, yy + 1) != 1) // tile below not sand
        {
            ds_grid_set(grid, xx, yy + 1, 1);
            ds_grid_set(grid, xx, yy    , 0);
        }
        else
        {
            // choose random side to go to
            var side = irandom(1);
            if (ds_grid_get(grid, xx + side_array[side], yy + 1) == 0) {
                ds_grid_set(grid, xx + side_array[side], yy + 1, 1);
                ds_grid_set(grid, xx, yy, 0);
            }
        }
    }
    else
    if (tile == 2) //--------------------------WATER--------------------------//
    {
        if (ds_grid_get(grid, xx, yy + 1) == 0) //Set gravity (nothing under)
        {
            ds_grid_set(grid, xx, yy + 1, 2);
            ds_grid_set(grid, xx, yy    , 0);
        }
        else
        {
            // choose random side to flow to
            var side = irandom(1);
            if (ds_grid_get(grid, xx + side_array[side], yy + 1) == 0) {
                ds_grid_set(grid, xx + side_array[side], yy + 1, 2);
                ds_grid_set(grid, xx, yy, 0);
            }
            else
            if (ds_grid_get(grid, xx + side_array[side], yy) == 0) {
                ds_grid_set(grid, xx + side_array[side], yy, 2);
                ds_grid_set(grid, xx, yy, 0);
            }
        }
    }

}}
Then, for the drawing part, you can do the same with the double-for loop where you pre-initialize the width and height.

For the drawing itself, it is much faster to draw a pixel than a sprite. Draw pixels and then upscale them using surface tranformations or display transformations.
To get the correct color, you can pre-initialize an array with colors where the cell index ends up representing the color inside the array.

I end up with this
Code:
var w = ds_grid_width(grid);
var h = ds_grid_height(grid);

var colors = [c_black, c_yellow, c_blue];

for(var xx = 0; xx < w; xx++) {
for(var yy = 0; yy < h; yy++) {
    var tile = ds_grid_get(grid, xx, yy);
    if (tile != 0)
    {
        var col = colors[tile];
        draw_point_colour(xx, yy, col);
    }
}}
EDIT:
I also really suggest using enums instead of numbers for sand, water and the like. It gets much more readable.
 
Last edited:
The first thing that causes unneccesary slowdown is having the ds_grid_width / height inside the for-loops, they will reevaluate every iteration.
If you don't resize the grid mids physics, you can just pre-initialize them to variables.

I notice with your code, is that it's possible for a cell to perform both iterations of physics (And even more iterations of sub-physics).
Unless this effect is desired, you need to put an else-statement everywhere you have an if-statement. This should also eliminate the problem of water / sand duplicating themselves.

When flowing to a side, you could choose a random side to flow to instead of always left and then right.

I end up with this code
Code:
if mouse_check_button(mb_left) {ds_grid_set(grid,xxx,yyy,1)}
if mouse_check_button(mb_right) {ds_grid_set(grid,xxx,yyy,2)}

var side_array = [-1, 1]; // array
var w = ds_grid_width(grid);
var h = ds_grid_height(grid);

for(var xx = 1; xx < w-1; xx++) {
for(var yy = 1; yy < h-1; yy++) {

    var tile = ds_grid_get(grid, xx, yy);
    if (tile == 1) //--------------------------SAND---------------------------//
    {
        // gravity
        if (ds_grid_get(grid, xx, yy + 1) != 1) // tile below not sand
        {
            ds_grid_set(grid, xx, yy + 1, 1);
            ds_grid_set(grid, xx, yy    , 0);
        }
        else
        {
            // choose random side to go to
            var side = irandom(1);
            if (ds_grid_get(grid, xx + side_array[side], yy + 1) == 0) {
                ds_grid_set(grid, xx + side_array[side], yy + 1, 1);
                ds_grid_set(grid, xx, yy, 0);
            }
        }
    }
    else
    if (tile == 2) //--------------------------WATER--------------------------//
    {
        if (ds_grid_get(grid, xx, yy + 1) == 0) //Set gravity (nothing under)
        {
            ds_grid_set(grid, xx, yy + 1, 2);
            ds_grid_set(grid, xx, yy    , 0);
        }
        else
        {
            // choose random side to flow to
            var side = irandom(1);
            if (ds_grid_get(grid, xx + side_array[side], yy + 1) == 0) {
                ds_grid_set(grid, xx + side_array[side], yy + 1, 2);
                ds_grid_set(grid, xx, yy, 0);
            }
            else
            if (ds_grid_get(grid, xx + side_array[side], yy) == 0) {
                ds_grid_set(grid, xx + side_array[side], yy, 2);
                ds_grid_set(grid, xx, yy, 0);
            }
        }
    }

}}
Then, for the drawing part, you can do the same with the double-for loop where you pre-initialize the width and height.

For the drawing itself, it is much faster to draw a pixel than a sprite. Draw pixels and then upscale them using surface tranformations or display transformations.
To get the correct color, you can pre-initialize an array with colors where the cell index ends up representing the color inside the array.

I end up with this
Code:
var w = ds_grid_width(grid);
var h = ds_grid_height(grid);

var colors = [c_black, c_yellow, c_blue];

for(var xx = 0; xx < w; xx++) {
for(var yy = 0; yy < h; yy++) {
    var tile = ds_grid_get(grid, xx, yy);
    if (tile != 0)
    {
        var col = colors[tile];
        draw_point_colour(xx, yy, col);
    }
}}
EDIT:
I also really suggest using enums instead of numbers for sand, water and the like. It gets much more readable.
Ok thank you for your answer.
I will test all that you said.
I tested the ds_grid_width / height outise the for-loops and i won 5/7 fps
 

muki

Member
check out Powder Toy.
it's powder game on steroids.
 

NightFrost

Member
I think general consensus through testing says arrays are faster than ds_grids, so I'd switch to those in an intensive implementation like that. Also, the problem with trying to duplicate Noita is how it leverages multithreading, which is not possible with GMS. It also does some work to keep track of areas which need physics updates so that it can ignore the rest that remains static. See the GDC talk for some more details.
 
I think i can improve my drawing method i tested 2 things draw_sprite and draw_rectangle_color but i don't see a big difference

Simon gust advised me to draw pixel.
Actually i have my cells are 8 by 8 and i am searching how to do your idea "Draw pixels and then upscale them using surface tranformations or display transformations" can you explain me a bit more this idea?
 
Last edited:
I also have an other question : how can i count the number of the cells which are occupied? The number of cells which value != 0
Actually i only add 1 when i create a cell with the method bellow
GML:
if mouse_check_button(mb_left) {
    ds_grid_set(grid,xxx,yyy,1)
    cells_active+=1}
 
Hello o/

I've some loops like that to manage grid cells to make stuffs like cross words and more. The combo "for" + "switch" as well as "while" + "switch" is really efficient. set a mode list with enum to set your cases in the switch.

You could have :
target cells to update.
sort cells to update
update type 1
update type 2
clear cell
etc ...

hope it helps ;)
 
I think i can improve my drawing method i tested 2 things draw_sprite and draw_rectangle_color but i don't see a big difference

Simon gust advised me to draw pixel.
Actually i have my cells are 8 by 8 and i am searching how to do your idea "Draw pixels and then upscale them using surface tranformations or display transformations" can you explain me a bit more this idea?
create a sprite in the sprite asset 1px by 1px with color white.
then use draw_sprite_ext() whith scale = 8 to get an 8 by 8 rectangle. Set the collor to whatever you want.

An other approche would be to use "enum" maped with color code on a down scaled surface 8 times instead of a grid.

That way your surface is the grid to operate and you can draw it simply as a sprite scaled x8 on the screen. It would lower the size of whatever you draw and drasticaly decrease the amount of operation made.

Hope it helps o/
 
create a sprite in the sprite asset 1px by 1px with color white.
then use draw_sprite_ext() whith scale = 8 to get an 8 by 8 rectangle. Set the collor to whatever you want.

An other approche would be to use "enum" maped with color code on a down scaled surface 8 times instead of a grid.

That way your surface is the grid to operate and you can draw it simply as a sprite scaled x8 on the screen. It would lower the size of whatever you draw and drasticaly decrease the amount of operation made.

Hope it helps o/
Can you explain me a bit more your idea with enum and surface please, it's very interesting
 

Simon Gust

Member
create a sprite in the sprite asset 1px by 1px with color white.
then use draw_sprite_ext() whith scale = 8 to get an 8 by 8 rectangle. Set the collor to whatever you want.

An other approche would be to use "enum" maped with color code on a down scaled surface 8 times instead of a grid.
As said before, draw_sprite, especially draw_sprite_ext is way slower than draw_point_ext. I think it's around 3 times slower.
You should always consider scaling your screen instead of your sprites first. Your game doesn't have every sprite scaled in themselves when the resolution changes does it?

That way your surface is the grid to operate and you can draw it simply as a sprite scaled x8 on the screen. It would lower the size of whatever you draw and drasticaly decrease the amount of operation made.
That's definetly not how it works. And you don't want to use a surface as your grid, surface-accesses require the cpu to reach to the gpu which is incredibly slow.

I think i can improve my drawing method i tested 2 things draw_sprite and draw_rectangle_color but i don't see a big difference

Simon gust advised me to draw pixel.
Actually i have my cells are 8 by 8 and i am searching how to do your idea "Draw pixels and then upscale them using surface tranformations or display transformations" can you explain me a bit more this idea?
It's important to use surfaces with primitive drawing as they won't get scaled automatically with display scale.
With this (assuming you have created the surface in the create event at grid size):
Code:
surface_set_target(surf);
draw_clear(0); // clear surface with black color


*** any draw code here ***


surface_reset_target();
draw_surface(surf, 0, 0);
you can draw everything the normal sized surface first, which will then be hopefully scaled up if you alter the display settings or the application surface settings.
I like to have these in my room creation code
Code:
surface_resize(application_surface, 480, 270); // scales drawGUI event
display_set_gui_size(480, 270); // scales draw event
Since my native resolution is 1920 x 1080, my game get's scaled up 4 times. Which is enough for this example to see everything.

I also have an other question : how can i count the number of the cells which are occupied? The number of cells which value != 0
Actually i only add 1 when i create a cell with the method bellow
GML:
if mouse_check_button(mb_left) {
    ds_grid_set(grid,xxx,yyy,1)
    cells_active+=1}
The code itself has a flaw, what if you set on an existing pixel, then you have the same amount of pixels but more cells that are active.
 
Can you explain me a bit more your idea with enum and surface please, it's very interesting
Yeah :

So you would have a surface, which would be your environment (in the memory) and since you wish to display squares of 8x8 your surface would be 8 times smaller than the final draw. Let's say :

display is 800x800
create surface 100x100
draw_sprite_ext with scale = 8;

The next step is about managing pixels.
You can get the color of a pixel with draw_getpixel. And you can make an "enum" called "color" to have "color.sand" "color.water"
pre-recorded instead of the color number which is hard to remember.

then it would go on a step event like :

GML:
surface_set_target(surf_environment);
for (each pixels X,Y) {
    var color = draw_getpixel(X,Y);

    switch(color) {
    case color.sand :     "code for sand" break;
    case color.water :     "code for water" break;
    default:              "default action"
    }
}
surface_reset_target();
delete old sprite
create sprite from the surface
surface reset target
(don't free the surface ! keep it for the next frame)
Remember however to add a clean-up for the sprite and the surface in case you change room.

Finaly draw the sprite in a draw event.



It makes me think about an other way if you just scroll throught each values linearily :
you could store the color numbers in a buffer that you could just manage the same way but with a repeat loop instead of a double for loop. Yet it's a bit more abstract. ;)

Hope it helps o/
 
I think general consensus through testing says arrays are faster than ds_grids, so I'd switch to those in an intensive implementation like that. Also, the problem with trying to duplicate Noita is how it leverages multithreading, which is not possible with GMS. It also does some work to keep track of areas which need physics updates so that it can ignore the rest that remains static. See the GDC talk for some more details.
Indeed i changed all my code to use arrays and the performances are much better.
 
Top