• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Legacy GM What's the best way to optimize procedural terrain?

H

HalcyonFlux

Guest
I'm trying to develop a procedural engine that can change the terrain/weather on the fly. I'm currently using for loops in the draw and step event (nested loops, at that...I know 🙃) What is another technique to prevent from having to call on for loops to access an array and draw it?

I'm considering using data structures, but I'm not sure how much better that would be with performance. I've also considered surface drawing, but I don't know if it's a good implementation if the variables are constantly changing.

If anybody has some ideas that would be excellent! 😁

GML:
//change_terrain();

i=1; ii=1; //Offset by 1 to prevent array errors.
for(i=1;i-1<(room_width/xx);i+=1){
    for(ii=1;ii-1<(room_height/yy);ii+=1){
        if(terrain[i,ii]!=0){terrain[i,ii]=choose(terrain[i-1,ii-1],terrain[i-1,ii],terrain[i-1,ii+1],
                             terrain[i,ii-1],terrain[i,ii],terrain[i,ii+1],
                             terrain[i+1,ii-1],terrain[i+1,ii],terrain[i+1,ii+1])}



//I think this else statement is redundant but w/e.

        else{terrain[i,ii]=choose(terrain[i-1,ii-1],terrain[i-1,ii],terrain[i-1,ii+1],
                             terrain[i,ii-1],terrain[i,ii],terrain[i,ii+1],
                             terrain[i+1,ii-1],terrain[i+1,ii],terrain[i+1,ii+1])}
        }
    }

}
GML:
//Create Event

i=0; //x coord
ii=0; //y coord
xx=16; //x width (i*xx)
yy=16; //y width (ii*yy)

//Init the terrain
for(i=0;i-1<=room_width/xx;i+=1){
    for(ii=0;ii-1<=room_height/yy;ii+=1){
        terrain[i,ii]=choose(0,1); //Choose between A or B. You can add weight to either side
    }
}

//Change the terrain to add variety.
repeat(5){
    change_terrain();
}
GML:
//Step Event
change_terrain();
GML:
//Draw Event
for(i=1;(i-1)<room_width/xx;i+=1){
    for(ii=1;(ii-1)<room_height/yy;ii+=1){//((i/(room_width/xx)*255)+(ii/(room_height/yy)*255))/2

color1=make_color_hsv(i/(room_width/xx)*255,255,ii/(room_height/yy)*255);
color2=make_color_hsv(i/(room_width/xx)*255,255/2,ii/(room_height/yy)*255);

        if(terrain[i,ii]=0){draw_set_blend_mode(bm_normal);
        draw_rectangle_color((i-1)*xx,(ii-1)*yy,((i-1)*xx)+xx,((ii-1)*yy)+yy,c_black,c_black,c_black,c_black,false);}

        if(terrain[i,ii]=1){draw_set_blend_mode(bm_add);
        draw_rectangle_color((i-1)*xx,(ii-1)*yy,((i-1)*xx)+xx,((ii-1)*yy)+yy,color2,color2,color2,color2,false);}

    }

}

draw_text(4,4,fps);
 
Last edited by a moderator:

FrostyCat

Redemption Seeker
You'll be using a nested for loop in any case, so that's not where you should be targeting your optimizations.

If you are using a view, you should only loop through the cells that are visible in the view.

If your room size is less than 2048x2048, you should use the loop to draw to a surface for the first time, then draw that surface on subsequent steps.

Also, not using var for loop iterators and other temporary values is an atrocious rookie habit with insidious consequences. Use var for your i, ii, color1 and color2.
 

kburkhart84

Firehammer Games
I see you put the "Legacy GM" tag....its possible you could switch to custom vertex arrays for drawing instead of doing it with draw_rectangle....it depends on just how much you are drawing, and how old your version you are using is.

I'm also wondering if this isn't a case of premature optimization. Are you already having performance issues? If not, you may be better off not worrying about it.
 
H

HalcyonFlux

Guest
I see you put the "Legacy GM" tag....its possible you could switch to custom vertex arrays for drawing instead of doing it with draw_rectangle....it depends on just how much you are drawing, and how old your version you are using is.

I'm also wondering if this isn't a case of premature optimization. Are you already having performance issues? If not, you may be better off not worrying about it.
I'm using GM8. I suppose I could use vertices with a buffer that checks for adjacent and homogenic squares.

Performance hovers around 27fps with a 16x16 grid. Anything below 4x4 crashes though. It doesn't help that I'm running on 4GB RAM

I'm only drawing 640px x 480px right now with no views, but I'd like to make it take up the screen with at least 30 fps.

Also, not using var for loop iterators and other temporary values is an atrocious rookie habit with insidious consequences. Use var for your i, ii, color1 and color2.
Oh, I didn't realize GML was finicky about initializing variables. I usually use var in C# and Java, but I get a bit lazy with syntax in GML
 

Simon Gust

Member
In general choose() is very slow, and you should always try to use irandom() instead.
In your change_terrain script, you could get away with irandom and then choosing a direction you want the center tile to be replaced with
Code:
var w = room_width / xx;
var h = room_height / yy;
var i, j, dir, len, i_new, j_new;

for (i = 1; i < w - 1; i++) {
for (j = 1; j < h - 1; j++) {
    
    dir = irandom(8) * 45;
    len = sign(dir);
    i_new = i + lengthdir_x(len, dir);
    j_new = j + lengthdir_y(len, dir);
    
    terrain[i, j] = terrain[i_new, j_new];
    
}}
The chance should be 1/9 for every variation.
I changed some of the variable names because they are the ugliest things I have every seen, no offense.
 
H

HalcyonFlux

Guest
In general choose() is very slow, and you should always try to use irandom() instead.
You're right, it saved a few frames. I also apologize for my syntax. I think I was pulling an all nighter when I originally wrote this lol

I tried the vertex method mentioned before with a triangle strip, and it made things run slower by about 8-10 fps. I was kinda surprised tbh
 

Nidoking

Member
dir = irandom(8) * 45;
Seems to me this should be irandom(7) because you're getting both 0 and 360, unless you add a special case for the central tile. This will never give you the central tile as far as I can see.

I'd just grab two irandom_range(i-1,i+1) and use those as indices, myself. Seems a bit silly to delve into trigonometry and fractional array indices, and no special case needed for hitting tile i,j.
 

Simon Gust

Member
Seems to me this should be irandom(7) because you're getting both 0 and 360, unless you add a special case for the central tile. This will never give you the central tile as far as I can see.

I'd just grab two irandom_range(i-1,i+1) and use those as indices, myself. Seems a bit silly to delve into trigonometry and fractional array indices, and no special case needed for hitting tile i,j.
dir 0 is reserved for central tile if you follow the later steps where len is sign(dir). sign(0) is 0. and dir 8 replaces dir 0 for the 0° angle.
 

Simon Gust

Member
Seems to me this should be irandom(7) because you're getting both 0 and 360, unless you add a special case for the central tile. This will never give you the central tile as far as I can see.

I'd just grab two irandom_range(i-1,i+1) and use those as indices, myself. Seems a bit silly to delve into trigonometry and fractional array indices, and no special case needed for hitting tile i,j.
Actually, I do remember having had problems using my method. The fractional indexes do tend to mess up and bug things out completely.
When I test it, only 5 of the 9 spaces are actually accessed. Odd is that some non-diagonal spaces which shouldn't get fractional indexes aren't reached somehow.
1597426044282.png
 
H

HalcyonFlux

Guest
I'm not sure if this is the issue, but I had to modify
GML:
for(i = 1; i < w-1; i+=1){
for(j = 1; j < h-1; j+=1){}
}
I changed it to this because it wasn't updating the bottom right corner:

GML:
for(i = 1; i <=w; i+=1){
for(j = 1; j<= h; j+=1){}
}
 

Joe Ellis

Member
I was going to say that the terrain shouldn't be being drawn using a 2d loop to process every cell, the terrain should be built into a grid of static vertex buffers, each one being something like 10x10 cells. That way, you can edit the terrain in game and it can rebuild the small vertex buffer it's dealing with no problem. This is how minecraft works, and it's basically the same thing, dynamic geometry in a grid.
It's called "chunking", well I think that's normally the term used when discussing how to render the very large world. The geometry is split into groups\chunks per certain amount of distance, so that only the parts within the camera's viewing distance\radius are rendered.
This will probably still involve groups of hundreds of vertex buffers being submitted, as the vertex buffers are made small enough to rebuild during playing without any lag, so probably about 500 of these vertex buffers are submitted per frame or more, but the computer can handle this, the slower part is building them, so it must weight up how many vertices it can build without lag (how big each vertex buffer can be), and then how many it can draw per frame.

Also for editing the terrain, the height coordinates should ideally be held in a grid array, (possibly a grid for each vertex buffer, which are then held in a larger(spatially) grid.) and editing each cell\vertex can be done by simply editing the value in the array, here's some code for this as an example:
GML:
///terrain_change_height(chunk_x, chunk_y, vertex_x, vertex_y, value)

var chunk = global.terrain_chunks[argument0, argument1];
var chunk_data = chunk[_chunk.data];

chunk_data[@ argument2, argument3] = argument4

global.update_terrain = true

var l = global.vbuffers_to_update;
l[@ array_length1d(l)] = chunk

///main object\engine step

if global.update_terrain
{
global.update_terrain = false
var l = global.vbuffers_to_update, i = 0, chunk, vb;
global.vbuffers_to_update = 0
global.vbuffers_to_update[0] = 0
repeat array_length1d(l) - 1
{
chunk = l[++i]
vb = chunk[_chunk.vbuffer]
vertex_delete_buffer(vb)
/////run script to rebuild vbuffer
}

}
 
Top