• 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!

Perspective Hexagon Grid

U

Ulrich6

Guest
Hello all.

I want to build a hexagon grid system that appears in perspective, like this:



Every hexagon can be clicked and mouse-hovered (with highlight) and all those regular actions.
I don't want to use real 3D perspective functions, because all the game will be in 2D. 2D-3D (and vice-versa) coordinates conversion would be unnecessarily hard for my goal.

The problem is I'm not sure how would someone approach this.
Do I have to create an object for every hexagon holding its coordinates and a unique sprite representing the clickable area?
Can I do it in a more efficient way?

Also, it may be worth noticing that the distance between two consecutive hexagons in a line is always the same. Other than that, I couldn't think of anything else.

I'd be gleeful if someone could help me.

Thanks in advance.
 
Last edited by a moderator:
U

Ulrich6

Guest
Thank you very much for replying.

The script you suggested looks really interesting; however, I don't know how to effectively use it to solve my problem. Do I still need to manually store the vertices, say, in an array and then use that script to check if the point is inside each polygon given its vertices?

I managed to solve it by using instances of hexagons, each with a different sprite assigned to it; the point checking is automatic now. It was a tedious task, though.

Thank you again.
 

MusNik

Member
Do I still need to manually store the vertices, say, in an array and then use that script to check if the point is inside each polygon given its vertices?
Yes.
In fact you can only configure the polygon once, and the use a code to correct its points based on perspective, but you need to write that code first :D
 
E

Ephemeral

Guest
Can I do it in a more efficient way?
The answer to this question is always yes. :p

The way I'd do this, is first, make your generic hexagon sprite / object.

Then use a controller object to spawn the hexagon instances into the room, relative to itself.

Next, make sure the hexagon object has a blank draw event. In the controller object's draw event, create a surface, then loop through all the instances spawned, with a with statement, and have them draw themselves to the created surface. After that, have the controller object draw to the room a textured four-vertex trianglestrip primitive, (there may be some necessary intermediate things you have to do to get a usable texture from the surface) textured with the contents of the surface, so you only have to adjust two points of one primitive to create the faux-3d perspective, and meanwhile "under the hood" everything's still running on flat 2d room coordinates.

You might be able to skip the primitive drawing entirely if you can get the effect you want just by scaling each row of hexagon instances relative to its y coordinate and then using draw_surface_stretched() to squish the height. Or skipping even the surfaces and doing it all with just scaling math.

After looking through the manual, it looks like my first idea might actually require you to dynamically create sprites...
Code:
surface_set_target(srf_perspective);
with (obj_hextile) draw_self(); // if you're using cameras this will be more complicated
surface_reset_target();

spr_perspective = sprite_create_from_surface(srf_perspective, 0, 0, WIDTH, HEIGHT, true, true, 0, 0);
tex_perspective = sprite_get_texture(spr_perspective, 0);

draw_primitive_begin_texture(pr_trianglestrip, tex_perspective);
draw_vertex(-offset, HEIGHT);
draw_vertex(WIDTH + offset, HEIGHT);
draw_vertex(offset, 0);
draw_vertex(WIDTH - offset, 0);
draw_primitive_end();

sprite_delete(spr_perspective);
In theory this could do what you want robustly, but it will suck up a vastly disproportionate amount of your CPU and Memory budget. Whether that's viable depends entirely on what you're gonna use it for. It seems like there should be a way to go straight from surface to texture, but there doesn't seem to be a function for it in the manual, so yeah.
 
U

Ulrich6

Guest
Wow, thanks a lot, Ephemeral. Although I'm not used to working with primitives and surfaces, I guess that task would turn out to be easy and I think I've understood what you meant. However, again, how can I detect mouse clicks on individual hexagons using this method? It looks like the hexagons are being drawn from computations whose intermediate values won't be available for later checking. Correct me if I'm wrong.

As the performance is really important for my game, maybe I'll just stick to fixed, pre-made instances instead of wasting memory and CPU in drawings.

Thanks again!
 
Last edited by a moderator:
Now if the hexagons are fixed I offer an alternative to find out which hexagon the mouse is on (not for drawing in perspective):

  • Create an array or ds where you assign each hexagon a unique colour
  • Draw each hexagon to a second surface through a shader that draws every opaque fragment with that unique colour. You'll only need to do this once at the start or when the surface is lost.
  • On each frame check the colour of the surface where the mouse is. Black would be no hexagon and any other colour is a hexagon.
  • Compare the colour with your datastructure to find out which hexagon the mouse is on, store that in a global.
  • Now in step event of any object you can check the global to see on what object the mouse is when you hover, press, keep pressing, release ...

if you worry about performance and one surface_get_pixel per frame worries (which I doubt it should), create a buffer and get the pixel colour from there.
 
U

Ulrich6

Guest
The Reverend, nice approach. How do you check only the color on the surface? I mean, there may be sprites which have the same color that I stored in the array/ds_list.
 
Well, the second surface would look close to this:

upload_2018-1-26_1-5-33.png
and the structure would store the id of the hexagon and the unique colour of that hexagon.

To create a unique colour you'd use the make_colour_ functions and make sure that no colour is the same. So either by incrementing colour channels each time or by creating random colours and comparing them with what you had assigned already.

If you dont need the vertex colour to draw the hexagon to the application surface you can use the vertex colour to pass the colour into the shader for the second surface.

And if you need the vertex colour you'd pass in a vec3 uniform with the unique colour instead.

The shader would be something similar to this (a bit simplified of course):
gl_FragColor = vec4(unique_colour, texel_colour.a);
 
Last edited:
E

Ephemeral

Guest
That seems completely unnecessary.

However, again, how can I detect mouse clicks on individual hexagons using this method?
With the method I outlined, you can just use standard Mouse Events with the sprite set to precise collision. To get everything to match up visually, you'd probably want to hide the system cursor and draw the mouse to the surface in the same code as everything else, which will cause your custom cursor to move in room coordinates but appear to move on the slanted plane with everything else.

It looks like the hexagons are being drawn from computations whose intermediate values won't be available for later checking.
Not if you're spawning them correctly. You just have to set and store the relevant information in the spawning code.
Code:
grid_width = 4;
grid_height = 4;
x_offset = sprite_get_width(spr_hextile) / 2;

col_size = sprite_get_width(spr_hextile);
row_size = sprite_get_height(spr_hextile) * 0.75 ; // guessing these numbers

do_offset = false;
var hx, hy;

for (var y_hex = 0; y_hex < grid_height; y_hex += 1)
{
    for (var x_hex = 0; x_hex < grid_width; x_hex += 1)
    {
        if (do_offset) hx = x + (col_size * x_hex) + x_offset;
        else  hx = x + (col_size * x_hex);
        hy = y + (row_size * y_hex);

        hextile[x_hex, y_hex] = instance_create_layer(hx, hy, layer, obj_hextile);
        with (hextile[x_hex, y_hex])
        {
            grid_coord_x = x_hex;
            grid_coord_y = y_hex;
        }
    }
    if (do_offset) do_offset = false;
    else  do_offset = true;
}
As the performance is really important for my game, maybe I'll just stick to fixed, pre-made instances instead of wasting memory and CPU in drawings.
I really think you might not need any of the surface/primitive stuff and could get something close enough just doing everything in the spawn code with some more complicated math, so that the first row's image_xscale and image_yscale are lower than 1, and the distance between them is equally reduced, while the bottom row's are larger by the same amount.

I'm not as good with math as I am with logic, so this is probably cringe-inducingly sloppy, but you could do:
Code:
scale = 0.8 + ((0.4 / 3) * y_hex);
There is probably a better way, but.
Then, you just multiply everything by scale. You'll also need to figure out centering, 'cause if you just plug scale into the above code the grid will be left-justified instead of centered, but that's a whole separate math problem.
 
U

Ulrich6

Guest
Alright, I've solved the problem!
Thank you very, very much, everyone, for all the help, efforts and explanations you gave me.
We can consider this thread closed! :)
 
E

Ephemeral

Guest
surface_get_texture() - GMS1 - GMS2
Oh. So there is. How did I miss that...

Well, good news, it turns out my first idea will work splendidly after all.:)

It has also belatedly occurred to me that it may be necessary to use draw_vertex_texture() instead of draw_vertex() to get a distortion instead of a cropping effect, but I have no idea if it actually works that way.
 
Top