Wednesday, January 10, 2018

Isometrish: Automatic walls

Obviously, when you're building a game level (and especially if, like I hope to, you're building it procedurally), you can't spend ages making sure you've put the correct-facing wall object down at every part of the level, or fixing them every time you move a bit of floor around.

Well, you could, but wouldn't it be much better to just have a single wall object that could automatically detect which sides it needed to match to the neighbouring floor tiles?

Yes! And luckily, this is really easy to do!

There is some heavy lifting involved, though: you'll still need a sprite with a separate frame for each possible combination of walls - north on its own, east on its own, north and east together, south on its own, north and south together…

(If you want a shorter notation, you need 2n-1 sprites, where n is the number of sides you need to consider. For most cases, this'll be four, so you need 24-1, or 15 images - plus one with no walls.)

But how do you know which image to draw based on the combination of faces required? Binary!

Assign each face a binary number - for a square, this'll be 1 for North, 2 for East, 4 for South and 8 for West. The neat trick with binary numbers like this is, the sum of any subset of them is unique! Which means we just need to add up the numbers corresponding to each 'active' face, and that tells us which image to load.

In GameMaker Studio 2, sprites have subimages - this is most commonly used for animation frames, but we can also use it here to keep a collection of images together in a common object. You need to make sure the various subimages are added in the correct order (the easiest way to do this in GMS2, I found, was to save the images into a common folder numbered 1.png to 15.png, then use the 'Create sprite from image(s)' option).

A hideous example of a 16-frame wall sprite

Now that our sprite is set up, we can add the following to the object's create event:

iso_sprite = sWall;
var f = 0;
if (place_meeting(x, y - grid_size/2, oFloor)) f += 1;
if (place_meeting(x + grid_size/2, y, oFloor)) f += 2;
if (place_meeting(x, y + grid_size/2, oFloor)) f += 4;
if (place_meeting(x - grid_size/2, y, oFloor)) f += 8;
iso_subimg = f;

This checks the room for a floor on each side of our wall, and adds that face's value to the total, resulting in the correct subimage index that we need to draw, which is done in the draw event:

draw_sprite(iso_sprite, iso_subimg, screen_x, screen_y);

(In this example, screen_x and screen_y are the pre-converted grid-to-screen coordinates.)

And presto - a single wall object that you can drop into your room, which will automagically show the correct wall faces for any surrounding floor tiles!

Friday, January 05, 2018

Isometrish: Perspective

The top-down grid

Do you ever suddenly realise that you understand something that's been bugging you for days, and then find it impossible to see how you didn't get it in the first place?

GameMaker is great at 2D - side-scrolling and top-down games are a breeze to get working. There are dozens of video tutorials that can get you up and running with a platformer in fifteen minutes or less. But if you want to do anything slightly more complicated than that, well… time to do some maths.

Luckily for me - and you, if you're wanting to do this yourself - YellowAfterlife has a really good piece (aside from some confusing terminology) about translating grid coordinates to on-screen coordinates which, although it still took a lot of practical trial and error for me to really understand, gave me enough confidence to actually start trying things.

While drawing objects to the screen is the more technically challenging part of the process, it actually took me longer to understand moving around - even though it's really simple.

The short version is, I needed to realise that the character's not moving on the screen, but moving on a 2D plane (as if you're doing basic a top-down game); those 2D X and Y coordinates are then warped by a couple of simple functions to correspond to the on-screen, 'isometric' plane that's on the screen.

Basically, you don't need to care about the screen coordinates- the maths will take care of it for you! Just do your stuff as normal, and the maths around the drawing step takes care of the rest.

Mind: Blown

Which can use the exact same movement code I used in the top-down version, and results in this on the screen:

The screen

I had some other issues drawing things in the right place - like I said at the start, that's actually the technically complicated bit.

I'd assumed, incorrectly, that when you're drawing a sprite in code the X and Y coordinates you give it would be where it started drawing, with the top-left corner of the sprite. Turns out the origin you set in the Sprite window matters! For walls and floors to line up correctly, this means you need to set the origin in the same place on the 'floor', which took me some time to figure out (particularly since the sprites in the room editor aren't the same as the ones you'll see when playing).

I also hit some trouble with aspect ratios because my grid (and the isometric sprites) were larger than those used in YAL's example - and since I'm still not 100% on the maths involved it took a little bit of trial and error to iron that out. (Turns out you just need to calculate the ratio of the isometric tiles' dimensions to your grid squares'.)

The way GameMaker Studio 2 handles 'depth' for draw orders has changed slightly too, favouring its built-in layers which are, in practice, pretty incompatible with isometric drawing (but are still really handy for organising objects in the room editor). The documentation suggests you should still be able to arbitrarily assign depth to your objects, but in practice I found that doing so prevented them from rendering at all, until I added the following in an init script:

layer_force_draw_depth(true, 0);

So now I've got a map, and the ability to move around it! And, as it turns out, right through the walls…

Thursday, January 04, 2018

Isometrish

First rendering walls

Right, fuck it. I'm going to write about something that terrifies me: my own code projects.

About the only thing I've programmed 'publicly' is a 140-character random level generation script (in Ruby). I'd written it in JavaScript originally, but decided for no particular reason to try and shorten it until it fit in a tweet. (This was before tweet length got doubled.)

def l w,h;g=(1..h).map{[]};y=rand h;x=g[y][0]=0;(g[y][x]||=1;n=y+rand(-1..1);n>=0&&n<h&&g[n][x].nil?? y=n : x+=1)while x<w;g[y][x-1]=2;g;end

But I've started something a bit more ambitious: I'm trying to make a game. Or at least, most of a game's systems.

I worked at YoYo Games for over six years, and it's only since I left in November that I've really started to use the product, GameMaker. I've thrown together little proofs of concept - platformers, a tank driving thing, some experiments with shooters - but never really had a specific goal in mind.

Having something to aim for means having something to miss; it's easy to just try out hitscan shooting in a tiny demo level, but make a game?

I'm now a couple dozen man-hours into a clone of Crusader: No Remorse. I don't know how often, if ever, I'm going to write updates on my progress, but I want to try and get less defensive about my code, and exposing some of it to the air might help.

(It helps that I don't expect anyone to read this, though.)

I've been meaning to find something to make me blog more this year, and maybe this is it.