code of the Ninja();

// in search of swift, efficient, and invisible code

2011-08-24

Trigger Happy

When Game Maker 8.0 introduced triggers, I can't say that I was all that impressed with the feature, and I've hardly used it since. This is because, as a matter of personal preference, I tend to eschew such shortcuts in favour of handling things manually; it's far easier for me to debug code when I'm personally responsible for each line, rather than goofing around with automatic functions whose technical nature is hidden in order to make things more palatable to beginners. Sometimes I forget that Game Maker even has things like room transitions and timelines, since I habitually code custom, flexible alternatives for each of my projects.

Recently, though, I've discovered two great uses for triggers that solve problems that are otherwise either very difficult to overcome or flat-out insuperable. I'll talk about the first one of these in this post, and the second, next time.

Code Execution Order

Games (and pretty much all programs) have what's commonly called a "main program loop" which controls all the behaviour in the game. A basic platformer might have one that looks something like this:

  1. Check for press of the pause key to toggle pause on and off
  2. If paused, branch directly to (8) drawing the screen
  3. Handle player input
  4. Move all objects
  5. Check for collisions
  6. Update the camera
  7. Handle animations
  8. Draw the screen

Obviously, the above is rather simplified; for instance, there's no mention of handling the game's audio. But you get the idea.

Having started experimenting with programming long before helpful game creation suites with user-friendly GUIs like Game Maker came on the scene, I'm very used to starting a project by writing the main loop. So used to it, in fact, that when I first used Game Maker, with its event system, I found it very counter-intuitive and confusing. Buried in the help file one can find a brief explanation of the event order, but otherwise there's very little explanation given of when and how your code is being run. Without going through the effort to manually test the system and gain a detailed understanding of it, one can easily be led to make games with egregious timing issues; for example, the player juttering uncomfortably while riding moving platforms.

In the above example loop, steps 4, 5, 7, and 8 are actually loops in and of themselves. To move all objects, for instance, one must use some kind of loop (for, while, or whichever of their kin is most suitable) that moves each object in turn (usually by adding the speed variable to the position variable).

Since the code for each object must be performed sequentially in some sort order, it's natural to wonder what that order happens to be. In a classic game like Sonic the Hedghog, it is the order in which the objects are actually stored in RAM. In Game Maker, it is the order in which the objects and instances were created.

Because this, especially the object/instance distinction, can be confusing, I will explain it in greater detail.

"Object" is the natural word one wants to use when discussing "things" in a video game: the player, pickups, enemies, platforms, etc. In fact, I can't think of a single non-clumsy alternative. Game Maker, though - in one of its myriad maddening quirks - has a different, technical meaning for "object" which is much closer to something like "class" or "template". The layman's object is referred to in GM parlance as an "instance". The player never interacts with objects, only ever instances of objects.

But if you're reading this, you're probably already passingly familiar with GM's system. Anyway, every instance has an "id" (in the id variable) as well as an "object index" (in the object_index variable). The id is a unique identifying number by which the instance can be referred to. Every time an instance is made, it is given an id number that is 1 greater than the last instance created. Barring some horrible bug in GM (or gap in my knowledge), it's impossible to have two instances with the same id, or to create an instance with an id that is lesser than an existing instance's id. (Note that objects placed in GM's room editor have fixed ids; if you restart a room their ids remain constant.)

So every instance has a unique id, creating a perfect sequence that mirrors the order in which they were created. (Yes, there will be gaps introduced when objects are destroyed, but this hardly matters.) Is this the order in which their code is executed? In short, no.

If it were, one would expect the object index to be irrevelant, causing the following pseudo-code program:

to output the string "ABAB". However, in reality, it will generate the string "AABB". What this means is that all instances of an object are performed in id sequence, and then all instances of the next object, and so on. The order of objects is determined by their object index, which works much like instance ids: the first object created for a game (using the resource tree in GM itself or by using the object_add() function in a running game) will be 0, the second will be 1, and so on.

Knowing this, we can predict issues that many games will encounter. Imagine the player is object 0 and a moving platform is object 26. The player will do its motion code, during which it will align to the platform instance that it's on (if it happens to be on one) -> Then the platform will do its motion code -> Then the screen will be drawn. What will be seen when playing the game is the player always lagging one step behind the platform, because it can't catch up until the next step.

(Quick readers will notice that you can easily solve this problem by simply having the platform move the player if they're on it, as opposed to letting the player take care of their own motion, but this is not always an optimal solution depending on how you've set up your physics. For instance, if the player does take care of their own motion, it's sometimes possible to optimise by removing collision checks.)

It's certainly things like this that explain why GM has the "Begin Step" and "End Step" events. Every game creation engine I've tried out has some form of this; it gives the user the ability to make absolutely sure that certain code is done before/after other code. It works for 99% of situations, and was probably easier on the creators of the program than giving the user full control over the main loop (which might confuse beginners).

But what about that other 1% of the time? Very advanced users might be out of luck, if it weren't for triggers (you knew I had to get back to the actual subject of the post eventually, right?).

Using Triggers to Solve Order Issues

First I'll need to paint a picture of a specific order issue so that I can explain how triggers can be employed in solving it:

It's fun to be able to put cool visual effects in your game, like motion blur, lighting bloom, or ripple distortion effects when underwater. But these are incredibly difficult, if at all possible, to achieve in GM without using surfaces. By drawing the game view to a surface, and then drawing that surface to the screen, the possibilities for effects are endless. Once the view has been rendered to a surface, it can be handled like any other image resource, manipulated in a myriad of ways - even used as a texture!

(Rendering to a small surface and then drawing the surface upscaled without any texture interpolation is a great way to make low-res classic-style games รก la Cave Story without that nasty blur that GM normally inflicts upon you. The only other ways to do this aren't really viable - storing all graphics as 2x not only takes up more memory and CPU power, but it's only the graphics - the physics won't be "low-res" without major tweaking.)

The simplest way to convert a game to use a surface instead of drawing directly to the screen is to a) switch off automatic drawing at game start; and b) in a control object's end step event, set the drawing target to a surface, call screen_redraw(), reset the target to the screen, and draw the surface with whatever effects are desired (make sure to call screen_refresh() afterward, or the frame the player sees will always be one frame out-of-date!)

But wait... that all sounds great, but there's a hidden problem lurking amongst those otherwise reasonable instructions: "In a control object's end step event..." According to what we've learnt above, unless the control object is the last object you ever created in the game (which is almost the exact opposite of what will be the case in the vast majority of situations), some objects won't have executed their code when the screen is rendered to the surface! Normally in GM, the screen is drawn last thing, full stop. But our brilliant surface method has just borked that, pushing the drawing back to the end step event. If any object's appearance or position changes in its end step event, then it will drawn out-of-sync, which is unacceptable (to all but the development team of Sonic Genesis).

Well, what's needed (but doesn't seem to exist) is an "End End (No, really this time!) Step Event" that can be used by the control object for handling the drawing instead. If one were lucky, there might be some kind of event type (like "outside of boundary" or some such) that's handled after the end step. One might, in a hackish way, abuse it for such a purpose. However, this is not the case. This is the event order:

  • Begin step events
  • Alarm events
  • Keyboard, Key press, and Key release events
  • Mouse events
  • Normal step events
  • (now all instances are set to their new positions)
  • Collision events
  • End step events
  • Draw events

There's nothing after end step that can be conveniently repurposed.

Here (finally) is where triggers come to our rescue. Take a look at the trigger window:

(When using Windows 98, one sometimes resorts to festive colour schemes for the sake of entertainment. =P)

The magic we're about to make is all because of that little phrase toward the bottom: "moment of checking". Trigger events can be made to happen at each of GM's step events. And if "at" seems a little vague, let me amend that - it's actually directly before. (If multiple triggers are set to the same moment of checking, they'll be evaluated in the order they were created, but they'll all still happen before the associated step event.)

Well, you might be thinking, that's great - we can add new events before the end step. Good going, genius - we needed one for after.

But here's the thing: it's entirely possible to swap out the end step event of all your objects for a trigger event (let's go ahead and call it "pre-end step") - well, all except for the control object, which will use the end step to draw the surface. A large amount of clicks, surely (depending on the size of your game), but a small price to pay for full control over the order in which things happen. By the way, the pre-end step trigger should have return true; as its condition code, so that it happens no matter what.

And I'm sure a clever Code Ninja like you can imagine ways, through the use of many triggers, to beat GM at its own game, essentially injecting all sorts of custom step events directly into the main program loop. (Imagine a trigger whose condition was a variable called exit_step; you could effectively make a custom step event which could be exited at any point!) With power like that you could - dare I say it? - take over the world! =P

No comments:

Post a Comment