Phoenix’s tech blog


Sand And Below revealed


Date posted: January 17th, 2008 (checksum: CEAC5C4BCA327723)

Let’s go back in time - do you remember sand simulator thing I wrote several months ago? I decided to tell more about it, mostly about internals and physics solver (here, and from now on, solver means the program that actually calculates the physics).

In case you have missed the post earlier, Sand And Below (SAB) is a sand simulator, kind of like Falling Sand Game (FSG). Except this is totally different solver, and I didn’t add iteration between different elements, like in FSG. Also it is in a way similar to Burning Sand game.

picture51.jpg

I didn’t really get to finish it, but I’ve made several test versions, which allow to play around with walls, concrete, water, cement, acid, sand, and ground/dirt. Last one also adds air, but it acts wrong really.

First of all, what are these sand games are about? Of course they are not about simulating particles with real physics parameters, because doing so would lag hard starting with 3000 particles (in best case!). These games, on other hand, can easily sustain 500000 to 1000000 particles, without causing major lag. (SAB @ 800×600 and 400000 particles runs at 30 fps).

The solution is to use a kind of a hack, we don’t simulate actual pieces, we create two dimensional array, where each cell specifies if there is a sand in current spot, or there is none, and we simulate/iterate through the array using specific set of rules.

The solver used in Sand And Below is my own FastSAB solver, which I think was not used anywhere before (I mean the same, or look-alike solvers). If you see similar stuff, e-mail me.

There are several versions of SAB available, you might want to try them all (each has some advantages):

  • SAB 0.7b (Very fast, but lacks features)
  • SAB 0.7b with 4 cores support, and 1024×768 screen
  • SAB 0.8 (Slower than 0.7, but more features)
  • SAB 0.8+ (No frame limiter here)
  • Game thread on PHXGames forum
  • FastSAB has these features:

  • Simulates solid and liquid substances
  • Simulates density
  • Supports static walls
  • Supports friction of substance
  • Supports freezing of non-simulated sand, which makes calculations faster
  • Minuses:

  • High-friction substances may go through thing walls because of how it is calculated
  • Liquids are actually solids, and are not proper liquids. This is same in all of other sand solvers I saw, it’s an issue you can’t really fix (There is idea to use realtime fluid dynamics with sand, but no idea if it would work out, more on this in next posts)
  • Advantages over some other solvers (for example FSG):

  • Liquids are simulated a bit better, and they reach stable state faster
  • There is variable density (because of how solver works), there is full density, half density, and no density (empty space)
  • There is potential for adding all of the features other engines have
  • There is potential of making it 3d (but that does not work out really, it looks ugly in 3d. I could try using voxels though…)
  • So, lets start with the solver itself! I wrote SAB in Delphi, so all of the code here will be in Delphi. Don’t worry, it’s not hard, think of it as of psuedo code. (Small cheat-sheet: boolean = bool; integer = int; record = struct; array[…] = […])

    First of all, there are 2 static arrays, used as source of data for the solver, and they fully define current state (apart from substance data):
    //Sand array - the heart of engine. A = Type; Active - Physics enabled (dynamic! this is changed in realtime, to lighten up calculations)
    Sand : array[0..W,0..H] of record A : Integer; Active : Boolean; end;
    //If this is a solid wall
    Solid : array[0..W,0..H] of Boolean;

    This should speak for itself.

    There is also helper array that shows whether sand was processed during the frame (you will see why you need it later)
    //Was this sand cell processed already?
    ID : array[0..W,0..H] of Boolean;

    Then we define substances. First version had them in a static array, so for simplicity we are looking on first version.
    //Everything is in special units
    //Friction is 0..8, everything else makes it go crazy
    //Void Sand Water Concr. Cement Acid Dirt
    const Colors : array[0..6] of Integer = ($000000, $AFAF00, $0000AF,$4F4F4F,$AFAFAF,$00AF00,$7F3F00);
    Friction : array[0..6] of Integer = (0 , 2 , 8 , 0 , 0 , 8 , 1 );
    Density : array[0..6] of Integer = (0 , 200 , 100 , 50 , 150 , 50 , 250 );
    Liquid : array[0..6] of Boolean = (False , False , True , False , False , True , False );
     
    const DissolveTable : array[0..6,0..6] of Integer =
    //Void Sand Water Concr. Cement Acid Dirt
    ((0 ,0 ,0 ,0 ,0 ,0 ,0 ), //Void
    (0 ,0 ,0 ,0 ,0 ,0 ,0 ), //Sand
    (0 ,500 ,0 ,0 ,100 ,0 ,300 ), //Water
    (0 ,0 ,0 ,0 ,0 ,0 ,0 ), //Concrete
    (0 ,0 ,0 ,0 ,0 ,0 ,0 ), //Cement
    (0 ,100 ,50 ,100 ,20 ,0 ,150 ), //Acid
    (0 ,0 ,0 ,0 ,0 ,0 ,0 ));//Dirt

    Dissolve table is used when comparing elements for dissolving. The number is literally equal to chance of dissolving, the more - the less chance. It is used as DissolveTable[dissolver,other_substance].
    Liquid - invokes extra calculations.
    Density - compared against each other, things with lower density stay “on top” of other substance
    Friction - explained below
    Colors - nice colors =)

    There is single function that processes the data, I called it PhysicsStep0 =D. It goes in 6 steps, and is executed 4 times per frame (original version had one frame each 25ms, fixed framerate). 6 steps are:

  • Initialize and determine active and non-active sands
  • If particle is active, then apply gravity and liquid physics…
  • …check for dissolving by neighbour elements (sands)..
  • …apply fake friction…
  • …apply some random movement…
  • …finish processing, and process border walls, if they are enabled
  • Let’s see source code of each step (added comments on most of lines):
    Step 1 (determine active sands)

    //Iterate through all sand
    for x := 1 to High(Sand)-1 do
    for y := 1 to High(Sand[0])-1 do begin
    //Initially inactive
    ID[x,y] := False;
    //First check, if there are different sands around (not same type)
    Sand[x,y].Active := not((Sand[x-1,y].A = Sand[x,y].A) and (Sand[x,y+1].A = Sand[x,y].A) and
    (Sand[x+1,y].A = Sand[x,y].A) and (Sand[x,y-1].A = Sand[x,y].A));
    //Second check, if there is something that cant dissolve it around, and not air
    if Sand[x,y].Active and ((DissolveTable[Sand[x,y].A,Sand[x-1,y].A] = 0) and
    (DissolveTable[Sand[x,y].A,Sand[x+1,y].A] = 0) and
    (DissolveTable[Sand[x,y].A,Sand[x,y-1].A] = 0) and
    (DissolveTable[Sand[x,y].A,Sand[x,y+1].A] = 0) and
    (Sand[x-1,y].A > 0) and
    (Sand[x+1,y].A > 0) and
    (Sand[x,y-1].A > 0) and
    (Sand[x,y+1].A > 0)) then
    Sand[x,y].Active := False;
    end;

    Step 2 - gravity & liquid physics

    //If there is where to fall
    if (not ID[x,y]) and (not ID[x,y+1]) and (Sand[x,y+1].A = 0) then begin
    Sand[x,y+1] := Sand[x,y];
    Sand[x,y].A := 0;
    ID[x,y] := True;
    ID[x,y+1] := True;
    end;
     
    //If it's liquid
    if Liquid[Sand[x,y+1].A] and (not ID[x,y]) and (not ID[x,y+1]) and (Sand[x,y+1].A > 0) then begin //Liquid physics
    //Go through the sand, by comparing density
    if (Density[Sand[x,y].A] > Density[Sand[x,y+1].A]) and (Random(5) = 0) then begin
    K := Sand[x,y+1].A;
    Sand[x,y+1] := Sand[x,y];
    Sand[x,y].A := K;
    ID[x,y] := True;
    ID[x,y+1] := True;
    end;
    end;

    Step 3 - dissolving

    if (DissolveTable[Sand[x,y].A,Sand[x,y+1].A]) > 0 then begin
    if (Random(DissolveTable[Sand[x,y].A,Sand[x,y+1].A]) = 0) then begin
    Sand[x,y+1].A := 0;
    end;
    end;
     
    if (DissolveTable[Sand[x,y].A,Sand[x,y-1].A]) > 0 then begin
    if (Random(DissolveTable[Sand[x,y].A,Sand[x,y-1].A]) = 0) then begin
    Sand[x,y-1].A := 0;
    end;
    end;
     
    if (DissolveTable[Sand[x,y].A,Sand[x+1,y].A]) > 0 then begin
    if (Random(DissolveTable[Sand[x,y].A,Sand[x+1,y].A]) = 0) then begin
    Sand[x+1,y].A := 0;
    end;
    end;
     
    if (DissolveTable[Sand[x,y].A,Sand[x-1,y].A]) > 0 then begin
    if (Random(DissolveTable[Sand[x,y].A,Sand[x-1,y].A]) = 0) then begin
    Sand[x-1,y].A := 0;
    end;
    end;

    Pretty self-explaining - if any of neighbour pixels can dissolve us, throw a “dice” (random), and see if we need to remove this sand.

    Step 4 - friction

    //Friction
    F := Friction[Sand[x,y].A] - Random(Friction[Sand[x,y].A] div 4);

    Step 5 - movement
    The longest step

    if (not ID[x,y]) then begin
    //If this sand was not processed. Select random direction of movement
    K := Random(3); //Move left or right or dont move at all
    L := (x-f >= 0) and (not ID[x-F,y]) and (Sand[x-F,y].A = 0); //Can move left?
    R := (x+f <= High(Sand)) and (not ID[x+F,y]) and (Sand[x+F,y].A = 0); //Can move right?
     
    if (K = 0) then begin //Fix direction 0 (left)
    if (not L) and (R) then K := 1;
    if (not L) and (not R) then K := -1; //Cant move
    end;
    if (K = 1) then begin //Fix direction 1 (right)
    if (not R) and (L) then K := 0;
    if (not R) and (not L) then K := -1; //Cant move
    end;
     
    //Priorities - sometimes sand has priority of moving into one direction than other. This happens on edges (as checked here)
    if (x-f >= 0) and (Random(2) = 0) and (not ID[x-F,y+1]) and (Sand[x-F,y+1].A = 0) and (R) then begin //Priority to move left/down
    K := 0;
    end;
    if (x+f <= High(Sand)) and (Random(2) = 0) and (not ID[x+F,y+1]) and (Sand[x+F,y+1].A = 0) and (L) then begin //Priority to move right/down
    K := 1;
    end;
     
    if (x-f >= 0) and (K = 0) and (Sand[x-F,y].A = 0) then begin //Move left
    Sand[x-F,y] := Sand[x,y];
    Sand[x,y].A := 0;
    ID[x-F,y] := True;
    ID[x,y] := True;
    end;
    if (x+f <= High(Sand)) and (K = 1) and (Sand[x+F,y].A = 0) then begin //Move right
    Sand[x+F,y] := Sand[x,y];
    Sand[x,y].A := 0;
    ID[x+F,y] := True;
    ID[x,y] := True;
    end;
    if (K = -1) then begin //If we did not move, still process
    ID[x,y] := True;
    end;
    end;

    Step 6 - boundary wall

    //By default there is one. This code "removes" it.
    if not Borders then begin //Borders disabled?
    for X := 1 to High(Sand)-1 do begin
    Sand[x,High(Sand[0])-1].A := 0;
    Sand[x,1].A := 0;
    end;
    for Y := 1 to High(Sand[0])-1 do begin
    Sand[High(Sand)-1,y].A := 0;
    Sand[1,y].A := 0;
    end;
    end;

    You can find full source code here: http://fileserver.forest-tm.com/sand/FastSAB.txt

    Rendering
    Nothing much to say here, except I used blur in all versions, and differently colored pixels in newer version (it’s easy, just put it in sand structure). There is also simple dynamic lighting, very slow too (because rendering is per-pixel, not using any hardware acceleration, except for outputting the pixel buffer). Well, jet the screenshots speak for themselves:
    picture44.jpgpicture55.jpgjdi24tmp.jpgpicture6.jpgpicture10.jpgpicture47.jpg

    More on rendering engine later, if you want (because it has nothing to do with the solver, it’s just working with 2d graphics, per-pixel =) )

    2 Responses to 'Sand And Below revealed'

    Subscribe to comments with RSS or TrackBack to 'Sand And Below revealed'.

    1. Suslik said,

      on January 20th, 2008 at 3:39 am

      Hello there, Black Phoenix. I’m Suslik from gamedev.ru. Your articles are really interesting, and I’m here to express my respect to work you’ve done - i’ve never seen such an elegant solver like yours. Your blog decoration is really nice too.

      Best regards, ??????.


    2. on February 18th, 2008 at 1:00 pm

      Very nice! Thanks!

    Leave a Reply