Introducing: POOL [of doom!]


Game's title screen

This week a One Script Games jam was held on GameMaker forums. In short, the rules are that the entire game must be done inside a single script (function), which is then called once per frame, and must make use of built-in functions to store and process all needed information.

While potentially a little quirky, this seemed like an interesting challenge, so I made a game for it.

The result is a mini-FPS that is a mix of Doom, Quake, and a particular cue sport.

You can download it right now or read the full post for technical details.

The idea

The idea for the game emerged a little spontaneously.

One of my ideas was to do some sort of first-person game (given that I was messing with such prior), but I was not sure about to actually do, since I had about 4 days to work on the game at most. I've asked for suggestions in one of the discussions that I participate in. Jobo jokingly asked whether this would be related to the "pool pool" mockup I posted earlier. Following that, AlexV suggested "DOOM-style pool". That seemed like a nice idea so I did just that.

Technical approach

Perhaps the most concerning part of the process of making a single-script game is code duplication - since you can't make additional scripts/functions, you are extremely likely to end up with a an amount of duplicate code. That is obviously no good, since you could forget to update some snippet of copied code when editing all of them, and end up with game working incorrectly.

My first thought was to make a small program that would search for specifically marked (with //{ comments) sections of code and replace specifically made comments around the code with them. You know, the kind of thing for which you make a regular expression that you won't be able to easily read later, such as /\/\/{\s*(\w+)\n([^]+?)\n\/\/}/g.

So I've whipped up around half of a small Haxe program for this before getting thought about this resembling a very basic version of what Haxe does with macros and inline functions. "Speaking of which", a thought continued, "couldn't this just be done in Haxe?".

And that would seem like a pretty reasonable question - as you may know, I've done my share of poking on Haxe's JS generator, making experimental targets for concise JavaScript, compact Lua (for PICO-8) and GameMaker's GML.

That to say, however, using the previously made generator "as-is" would not be possible, since a decent number of tricks that I would use to map Haxe syntax to GML usually require a script.

For example, given a Haxe array declaration,

var arr = [1, 2, 3];

I would usually have the generator output a special script called array_decl and have it call it like so:

var arr = array_decl(1, 2, 3);

In a single-script game, of course, this is not possible, so instead the generator has to expand the script contents contextually, like so:

var arr = undefined; arr[2] = 0; // [re-]allocation
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;

The [re-]allocation bit is needed to avoid accidental array reuse when creating multiple ones inside a loop (see "Mike's problem" part of a tech blog post). It is said than an upcoming version of the program will introduce a function specifically for allocating arrays of given size, so that should help too.

Another things is handling Haxe structures. Say, you have

typedef Player = { x:Float, y:Float, z:Float };
var player:Player = { x: 16, y: 16, z: 0 };

GameMaker does not have support for compact objects\structures (yet?), so the options here would be to either compile them to ds_map structures (associative arrays) and deal with needing to deallocate them explicitly, or to index the fields and store them as arrays. The second option is a better idea, and thus the code was made to compile as

enum Player { x, y, z }
// ...
var player = undefined; player[2] = 0; // [re-]allocation
player[Player.x] = 16;
player[Player.y] = 16;
player[Player.z] = 0;

Obviously, this means that your structures cannot be "anonymous" (you need to know what it is to pick the correct enum for indexes), but otherwise this is about the best option.

So, after doing these and a few other tweaks, I was able to get compiler to generate compliant GML code. Then I wrote a couple of extern definitions for the needed GM functions, wired a few via a macros that just outputs GML code, and so was able to write the entire game in Haxe.

Being able to use Haxe was pretty helpful in this particular case - Haxe's compile-time error checking meant that I've encountered extremely few runtime errors, while being able to use FlashDevelop for code meant being able to make use of code folding to organize everything. And, of course, no duplicate code.

Gameplay

As far as the game mechanics go, the game is relatively simple:

  • The player runs around the table area.
  • Balls drop in from above in waves and will attack the player by hopping towards them.
  • The player's goal is to knock balls into the "pockets" (holes) for score.
  • The player loses if they run out of energy (which renegerates) or are knocked into a pocket.

For collision checking, the player is assumed to be a cylinder. The balls are assumed to be spheres for ball-ball\ball-player collisions and cylinders for ball-table collisions. Movement as whole is organized through a function that finds the maximum altitude if an object was to be placed at the given spot. It works as following:

result = (default altitude)
if object exceeds bounds of table' rectangle:
	result = (border height)
for each pocket:
	if object is contained within the pocket' circle:
		result = (hole depth)

So the altitude is assumed to be even inside the table' play area, elevated outside of that, and overriden for holes/pockets.

If the new location's altitude is found to be less or equal than the object's altitude, it can move there.

Player's movement needs to be relatively precise, so the game will first attempt to offset the player by actual velocity, and if that does not work, will instead make several smaller steps to move them as close as possible to the wall.

Ball movement works similarly, except with bounces instead of closing the distance towards collision.

Ball-ball bounces follow a common formula, transferring momentum as they should.

I believe that the largest thing I've overlooked is that the corner pockets are supposed to have small "pathways" leading up towards them instead of just cutting into a corner of the table. Given the pocket:ball size ratio, it is still easy enough to send balls into them, but means that it is noticeably easier to send balls into the central pockets.
Perhaps for the better though, since the process of knocking balls into the central pockets requires standing between the ball and the other pocket, meaning that the player has an increased risk of getting knocked into a pocket themselves.

Graphics

For the game's visuals I wanted to go with something that would be somewhat minimalistic but would not fall into the already-overused "blocky with pixels" category.

As the result, I've settled on a "low-poly" style with mild colors and simple patterns - detailed enough to be pleasing to an eye and simple enough to work even devices with integrated graphics.

Level geometry is produced through series of relatively simple mathematical formulas (pictured).

Balls are slightly cut spheres with similarly tweaked circles and rectangles for "face details".

Font and the healthbar are drawn from series of triangles to achieve the clean "slant" that they have.

The logo is a bunch of polygons too:

For purposes of "weight distribution", giving the "L" a fat tail would have probably been a better idea, but that looked weird to me, so the logo spots conventional lettering and slightly off-center balance.

Overall I'm pretty happy with how things turned out.

Other considerations

  • Jam rules did not forbid use of recursion. This meant that you could, for example, have the script call itself with a name of action to do, and act based on that. While this would have permitted to cut duplicate code, it would also largely defeat the purpose of the jam, so I've decided to not make use of this.
  • Again, as per jam rules, it be allowed to pack assets as text (e.g. in Base64 format).
    With no restrictions on what this could be used for, you could load graphics, data, or audio alike.
    Heck, you could even dynamically assemble an extension (DLL) this way and write most of your game code in C++ if you wanted.
    Overall this would seem like slight cheating so I did not make use of this either.
  • Ideally there should have been some sounds and/or music, but as per jam rules this would have also meant writing the related software during the jam timeframe [on top of the game itself].
  • I was intending to also put up OSX and HTML5 versions of the game, but, as it usually goes, I tend to break software when experimenting, and this was no exception - in HTML5 version I have managed to break model texturing, while the OSX version revealed to have some unusual issues with 3d mode as whole, as well as issues with "mouselock" needed for camera rotation.

In conclusion

Overall I'm pleased with how things turned out.

As per usual, some things could have gone better, but the game works, does what was intended, and appears to deliver the entertainment value thaat it is expected to.

For anyone further interested in technical details, complete source code is made available for download.

Download Source code

Have fun!

Related posts:

2 thoughts on “Introducing: POOL [of doom!]

  1. This is crazy cool. Would of loved to see the script though. Perhaps you posted it (pool ball eyebrow thing)? For real though love this will have to come back to this! You have insane skills my friend, very admirable ones. I love the art for this game, the from everything you do I can see you like minimalism. For a minimalist game this has very good graphics. I also love the camera movement! Overall 12/10 this is awesome, however the game play was a bit dull, but from an engine perspective this is incredible.

    • Source code is linked in the end of the post – Haxe source is in “Game.hx” while the generated GML is in “scripts/main.gml”.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.