Cheese talks to himself (about the SLUDGE engine)

During April I participated in Adventure Jam and created a short point and click adventure called Above The Waves using the Free/Open Source SLUDGE engine.

In this article, we'll look at the tools contained within the SLUDGE Development Kit, the SLUDGE language, and some example code techniques. Alongside the article, I've also published an example game with sources.


Picking An Engine

Any time I'm about to start a new project, I like to have a look around and see what tech there is out there that fits that project's needs. With Above The Waves being my first proper attempt at a point and click adventure game, it was time to dive in and experiment with some new tools.

Beyond aligning with the specific needs of a project, my requirements are usually a mix of one or more of the following:

For Above The Waves, I knew that I wanted something capable of providing a point and click adventure style experience. To keep the amount of tech development time down during Adventure Jam's two week window, convenience methods and tools for common 2D adventure game style functionality (automatic nav mesh traversal, perspective scaling, sprite animation, depth handling, etc.) felt essential.

Based on previous research, I had narrowed down four engine possibilities that seemed to fit most of my requirements.

Narrowing down options here was pretty straightforward. ASG's lack of native Linux tools made it easy to cross it off the list. Likewise, ScummVM doesn't have much in the way of available tools either (there is a Qt based Linux compatible editor for the AGI engine, but its last code update was in 2012), and the ScummVM developers frame targeting it for new projects as "not advisable".

Godot was a serious consideration. With it being used for The Interactive Mendonça Adventures of Dog & Pizzaboy, Godot has been getting a bunch of 2D point and click adventure specific enhancements. Some of these had made their way in by the time Adventure Jam rolled around, whilst others have come into the codebase more recently. Godot's level of complexity and learning curve looked to be steeper than SLUDGE, and with the window I had available, I decided to put Godot aside for later use.

SLUDGE seemed to map fairly well against what I was looking for - it's LGPL licenced, runs on the three major desktop platforms, and consists of an engine (a common binary that's the "player" for a SLUDGE data file, much as AGS or Unity might have), a spritesheet builder, a nav polygon editor and a tool for creating depth masks, and a utility for managing localisations. The engine and toolset also compile on Linux, Mac and Windows.

At the core of SLUDGE is the scripting language itself. This language is fairly straightforward, but has a number of idiosyncrasies that I'll go into later. The SLUDGE compiler builds scripts into some kind of bytecode that the SLUDGE engine can run.

I'd been aware of SLUDGE for some time and had had some recommendations from friends, but hadn't really had a project suitable to use as a testbed. I also have a couple of acquaintances who've previously used it that I felt I could nudge for assistance if I needed it. In addition to being able to look at friends' projects (thanks Rusty!), I had access to several example projects on the SLUDGE website that looked like they would cover most of the functionality I would need if I wasn't able to figure it out myself.

SLUDGE was originally created by Hungry Software for their own games (there's not a lot of solid information about timeframes that I can find, but so far as I can tell, that started with Out of Order), and has since been released under open licences (the engine itself is LGPL, and the suite of editing tools is GPL).

In hindsight, I'm glad I checked out SLUDGE. It was a good opportunity to mess around with the tools, and it was enjoyably fast to put things together.

↑return to top↑

The SLUDGE Toolset

The SLUDGE Project Manager serves as a launcher for the other tools and for compiling SLUDGE projects. It doesn't include any sort of integrated development environment (IDE), but instead allows you to configure which applications you use to open the assorted files you can add to your project.

The SLUDGE Project Manager.

The SLUDGE Project Manager

Since Above The Waves was pretty small, I'm used Gedit for code editing. There's no reason beyond that choice than it being installed and me being lazy. For the backgrounds, I used Inkscape, exporting images to PNG for use in-game. For animations, Mim also used Inkscape, creating each frame as a separate layer and exporting those as individual PNGs which would then be assembled with SLUDGE's spritebank builder.

For music, we sourced a number of tracks from Kevin MacLeod's library of CC licenced music.

All files to be included in a game must be added to the project via the Project Manager (the project file is plain text and you could add it there if you really, really wanted).

The SLUDGE Sprite Bank Editor.

The SLUDGE Sprite Bank Editor

The Sprite Bank Editor allows you to load TGA or PNG images to be compiled together into SLUDGE's .duc sprite bank format (which was originally used for one of Hungry Software's earlier games, Ducks). The Sprite Bank Editor allows you to reposition the 0,0 point of each frame manually, allowing for centering and a small amount of animation tweaking in some cases.

I had some small problems with the Sprite Bank Editor crashing when opening certain existing .duc files I'd created. These were rendered correctly in the game, and with the time I had available, I opted to just re-create the spritebanks entirely when updating a single frame rather than debugging it.

The SLUDGE Floor Maker.

The SLUDGE Floor Maker

The Floor Maker tool allows you to define polygonal regions which can be used to limit character movement (the "floor" on which your character may move). Polygons must be convex and contiguous if characters are to be moving between them. The tool allows for splitting polygons, removing corners (this icon looks like it is meant to be removing lines, but can only be used on corners) and adding corners.

Early on, I had planned to allow for zero gravity style movement, which would have negated the need for this tool, but for better or worse, SLUDGE didn't seem to expose any ability to rotating sprites and I ended up sticking with a more traditional style of movement with the main character positioned to be floating above the floor.

The SLUDGE Z-Buffer Maker.

The SLUDGE Z-Buffer Maker

The Z-Buffer Maker is used for creating masks that will obscure sprites, making them appear to have passed behind elements in the background image. It takes a limited palette (16 colour max) TGA file and breaks that into regions based on colour variation. It allows you to define a "y-value" for each colour, which represents the coordinates a sprite's 0,0 point must be above to be obscured.

This kind of tool feels like it's most relevant where traditionally painted backgrounds are concerned. The amount of time saved by using this compared to using sprites for this sort of thing is low, and without any kind of support for anti-aliasing, masks' jagged edges become visible on characters as they move past. It may have been faster to have bitmap masks than do blending on slower machines, but right now, it doesn't seem like it has much impact. I ended up using this for several areas in Above The Waves when I was having some hiccoughs with scaling sprites (with the benefit of sleep and hindsight, and I can see that the problems I was having were overcomeable, but at the time, I hadn't discovered where the relevant information was hidden in the documentation).

The SLUDGE Translation Editor.

The SLUDGE Translation Editor

The Translation Editor looks through the scripts referenced in your project file for strings, and lists each one with a checkbox to indicate whether the string should be translated, and a text field for a translation to be added. Once a translation ID and name has been added, this can then be saved off as a .tra translation file (one .tra per localisation so far as I can tell). When editing existing translation files, strings can be re-loaded from the project and the translation can be filtered to only show strings that currently have no existing translation.

For Above The Waves, I opted to go for a textless presentation (although we fell back on text screens to fill in for missing cutscenes), so I didn't manage to get any hands-on time with the Translation Editor.

↑return to top↑

The SLUDGE Language

The SLUDGE language is an interesting beast. It's not too hard to learn, and many of the problems I had were the result of parts of the language documentation being difficult to find. For example, the pages related to scaling include no references to setCharacterExtra() where the scaling override FIXEDSIZE flag can be set, and there isn't any reason to look for setCharacterExtra() unless you already know it exists, so discovering this was frustratingly difficult. There were several other things that I had trouble finding, but for better or worse, I wasn't taking notes.

Another aspect that caused much head scratching is that else if conditionals don't seem to evaluate correctly for statements containing not operators. I hope to find time to write out repro cases and file a bug report[1], as this is a fairly significant issue. With the Adventure Jam clock ticking, I opted to place extra if statements inside else statements rather than work with else ifs.

The stack data type (which I'll cover below when looking at inventory code) is novel for providing queue functionality, but without non-destructive assessors, it feels awfully cumbersome to work with as a collection. It really feels like scripting languages (and compiled languages in general) should probably be abstracting away these kinds of limitations to make code simpler, more readable and more maintainable.

The language feels like it's C-ish, with braces for code blocks, semicolon line terminators, etc.. Its notions of objects and their members functions are unexpected, where member variables can be read, but not have values assigned to them without a fully qualified path. For example, the following code will return a compile time error.

objectType testObject("Test Object")
{
	var completed = FALSE;
	event useItem
	{
		completed = TRUE;
	}
}

This code, however won't.

objectType testObject("Test Object")
{
	var completed = FALSE;
	event useItem
	{
		testObject.completed = TRUE;
		say(completed);
	}
}

So, a call to the say() function (which outputs text to the screen and is generally used for dialogue) is fine because it only reads from the completed variable, but the assignment can't be made because the compiler doesn't understand scope when writing assigning values. This is in contradiction with the documentation and I hope to file it as a bug at some point (I'd do it right now, but writing this feels like it's more important).

It also highlights that objects behave more like global variables with members than classes that can be instantiated. You can add an object to a scene multiple times (At one point, I had a bug in Above The Waves where with some fancy mouse clicking, players could enter a scene twice, causing two Yok-yoks to appear), but I'm yet to find a way to programmatically identify/access one.

With one exception, SLUDGE doesn't allow you to access files outside of the compiled .slg file, which contains all of the scripts and assets that have been added to the project. This means that you have the advantage of a single, self contained data file, at the disadvantage of not being able to change any game files without recompiling the entire project (if, for example, you want to replace a sprite or an audio file, then you must go through the entire compilation process).

For many people, this isn't an issue, but for me, it's a hurdle to productivity, especially in a team environment. For this game, I would have loved to be able to tell Mim that she could dump images in a folder and then run the game without having to get her to build the game (not that she isn't capable, it's just that it's a time cost and focus cost). Similarly, when testing music tracks (which blew out the project size and therefore compile time significantly), I would've loved to have been able to just rename a file and relaunch the game (or even better, trigger an in-game reloading of assets).

The exception is several convenience methods for saving/loading game state, which are mostly geared towards saving the entire game's state or runtime values of variables rather than loading arbitrary files.

↑return to top↑

Code Patterns

I'm a long way from being an expert on SLUDGE, but there were some approaches to working with the platform that I felt saved me time. In case it's helpful to anybody else, here's a rundown!

Costumes, Characters and In-game Objects

SLUDGE uses a notion of "costumes" to represent collections (multiples of three) of sprite based animations that can be applied to a character. If a costume has the right number of animations, a costume will automatically show an appropriate animation for stationary, moving and talking states in multiple directions. For example, if you have three animations (and an animation may be a single frame or many frames long), then the first one will be played whenever the character is stationary, the second for when the character is walking, and the third for when the character is talking. If there are six animations, the first animation will represent the character facing north (facing away from the screen) while stationary, the second south (facing toward the screen) while stationary, and so forth. Stationary objects within scenes have costumes applied to them as well (usually with one animation and NULL for the remaining two needed by the function).

For most of Yok-yok's animations, we had twelve animations representing north, east, south and west, although for top down perspectives, we chose to go with sixteen.

With Above The Waves, we kept our character object definitions in a single file called characters.slu, which looked like this.

objectType cTestCharacter("Test Character")
{
	walkSpeed 10;

	var defaultCostume = costume(
		anim('sprites/test_wait_south.duc', 0, 1, 2, 3),
		anim('sprites/test_wait_west.duc', 0, 1, 2, 3),
		anim('sprites/test_wait_north.duc', 0, 1, 2, 3),
		anim('sprites/test_wait_west.duc', -1, -2, -3, -4),

		anim('sprites/test_move_south.duc', 0, 1, 2, 3),
		anim('sprites/test_move_west.duc', 0, 1, 2, 3),
		anim('sprites/test_move_north.duc', 0, 1, 2, 3),
		anim('sprites/test_move_west.duc', -1, -2, -3, -4),

		NULL,
		NULL,
		NULL,
		NULL
		);

	var grabEast = anim('sprites/test_grab_east.duc', wait(1, 6), wait(2, 15), wait(3, 3));
	var grabWest = anim('sprites/test_grab_east.duc', wait(-2, 6), wait(-3, 15), wait(-4, 3));
	var grabNorth = anim('sprites/test_grab_north.duc', wait(0, 3), wait(1, 6), wait(2, 15), wait(3, 3), wait(4, 3));
	var grabSouth = anim('sprites/test_grab_south.duc', wait(0, 3), wait(1, 6), wait(2, 15), wait(3, 3), wait(4, 3));

	sub getDefaultCostume()
	{
		return defaultCostume;
	}

	sub getGrabEast()
	{
		return grabEast;
	}

	sub getGrabWest()
	{
		return grabWest;
	}

	sub getGrabNorth()
	{
		return grabNorth;
	}

	sub getGrabSouth()
	{
		return grabSouth;
	}
}

This pattern is something I came up with on my own. the c prefix on the object name denotes a character. getDefaultCostume() means that for any given character, I can rely on that function existing, and in case I ever I need to, I've got a function I can stick some processing in before the default animation is returned. walkSpeed is used to determine the movement speed for the character, and having a variable here means that different characters can have different speeds without a lot of messing around (originally, the creatures you rescue were planned to follow you around as you moved from scene to scene). grabEast(), grabWest(), grabNorth() and grabSouth() are character specific functions that I only call when I know that this character should be grabbing things.

The negative numbers for the east or west facing animations mean that they will be a horizontally flipped version of the animation frames defined in that .duc file, and the wait() function means that that animation frame (first parameter) will be held for multiple game frames (second parameter).

Since our inventory items are also characters (and since they have the same "costume" thing going on), we included them in costume.slu. They looked like this.

objectType iTestItem("Test Item")
{
	var defaultCostume = costume(anim('sprites/test_item.duc', 0), NULL, NULL);
	var iconCostume = costume(anim('sprites/test_item.duc', 0), NULL, NULL);
	var icon = anim('sprites/test_item.duc', 0);
	var iconHighlight = anim('sprites/test_item.duc', 1);

	event actionUse
	{
		inventoryAddItem(iTestItem);
		removeCharacter(iTestItem);
	}

	sub getDefaultCostume()
	{
		return defaultCostume;
	}

	event getInventoryIconCostume
	{
		inventoryTempIcon = iconCostume;
	}

	event getInventoryIcon
	{
		inventoryTempIcon = icon;
	}

	event getInventoryIconHighlight
	{
		inventoryTempIcon = iconHighlight;
	}
}

Again, this pattern is of my own devising. Since inventory items exist in the environment, we use getDefaultCostume() for that. inventoryTempIcon is a global variable I created to contains the animation to be used for the mouse cursor when an inventory item is selected. icon is the animation used when displaying the inventory item in the inventory view, and iconHighlight is the animation to be played when the mouse cursor is overing over an item in the inventory view, or when the mouse cursor is over an interactive object and an inventory item is selected.

For the game that I ended up making, this created a bunch of unnecessary "icon" variables. This stems from early decisions to have the inventory creature mouse cursors animate as you moved them across interactive elements within a scene which were abandoned as the Adventure Jam deadline loomed.

Hotspots & Default Events

I kept most of my definitions for interactivity hotspots in hotspots.slu, using the following pattern.

objectType sTestHotspot("Hotspot Test")
{
	var completed = FALSE;

	event actionUseInventory
	{
		if (inventoryCurrent == iTestItem)
		{
			addScreenRegion(hTestSceneExitEast, 1060, 320, 1205, 629, 1280, 550, EAST);
			sTestHotspot.completed = TRUE;
			removeScreenRegion(sTestHotspot);
			inventoryRemoveItem(iTestItem);
			setFloor('rTestRoom/floorExit.flo');
		}
	}

	sub isComplete()
	{
		return completed;
	}
}

useActionInventory() is an event that's triggered when the player clicks on an in-world object with an inventory item selected. In this case, we check to see if inventoryCurrent (a global variable containing the currently selected inventory item) is iTestItem before performing any actions. When the correct inventory item is used, the hotspot is removed, a new hotspot representing an exit is added, and the room's polygonal floor map is updated to reflect the new exit. iTestItem is also removed from the player's inventory.

Since scene exits are more specifically tied to scenes (puzzles and pick-up-able items shifted between scenes a number of times during development), I included these hotspots in the relevant scene's .slu file.

objectType hTestSceneExitEast("Exit")
{
	flags EXIT;
	event actionUse
	{
		movingToExit = hTestSceneExitEast;
		transitionRoom(rTestScene2);
	}
}

I used the EXIT "flag" (flags are arbitrary and can be set to anything you like) to keep track of which hotspots were and weren't exits to, in conjunction with a global movingToExit variable, allow double click fast travel (more on that in a bit) to behave nicely. transitionRoom() performs a little housekeeping (removing objects, blanking the screen, disabling input, closing the inventory if it was left open, etc.) before triggering the new scene to load.

actionUse() is triggered when the player clicks on an in-world object without an inventory item selected (you may have noticed it above when looking at the example inventory item). actionUse is defined as an objectType as well as an event. Through some weird not-quite-fathomable design decision, SLUDGE allows objectTypes to behave as default events for other objectTypes that don't have them (so effectively, all objectTypes inherit actionUse(), so we'll always catch this for anything the player can click on - if it doesn't implement this event itself, then the default behaviour defined in the actionUse object occurs instead).

Here's a look at the code to make that kind of behaviour work.

objectType default("")
{
}

objectType actionUse("")
{
	event default
	{
		doDefaultAction();
	}
}

Both the empty default objectType and the actionUse objectType with the default event are needed for this to behave properly.

Scenes

For organising scenes themselves, I kept a subfolder for each which contained a scene.slu and background.png, as well as one or more floor definition files (.flo) or zbuffer definition files (.zbu). My thought here was that a template folder could contain everything needed to get a scene up and running with minimal tweaks. For the most part, that worked, but I think in retrospect, I'd have preferred to keep the background images in a backgrounds folder with explicit names to reduce any accidental chance of overwriting the wrong file. The scene.slu files also contained a bunch of duplicated code, and I think that I would have done better by using an objectType data structure to contain most of the relevant variables, and moving most of the scene instantiation code out to somewhere more generic.

sub rTestScene()
{
	setScale(350, 500);
	#setZBuffer('rTestScene/floor_mask.zbu');

	addOverlay('rTestScene/background.png', 0, 0);

	if (sTestHotspot.isComplete() == FALSE)
	{
		addScreenRegion(sTestHotspot, 1060, 320, 1205, 629, 1280, 550, EAST);
		setFloor('rTestScene/floor.flo');
	}
	else
	{
		addScreenRegion(hTestSceneExitEast, 1060, 320, 1205, 629, 1280, 550, EAST);
		setFloor('rTestScene/floorExit.flo');
	}

	addCharacter(cTestCharacter, 1280, 550, cTestCharacter.getDefaultCostume());
	moveCharacter(cTestCharacter, 500, 640);
}

rTestScene() (or whatever the objectType for the relevant scene is) is called when we want to enter that scene. It sets the perspective scaling behaviour via setScale(), sets the background image via addOverlay(), configures any puzzle specific content based on the state of hotspots and then finally adds the player character at an appropriate location and moves them to some central position to give a sense that they are moving into the scene.

addScreenRegion() is used to add interactivity hotspots to scenes. The first two parameters are top left x,y coordinates, the second two parameters are bottom right x,y coordinates, the third two parameters are the x,y coordinates that a character should move to when told to move to the location of the hotspot (handy for when you want a hotspot to be on a wall, but the character needs to stand in front of it rather than on it), and the final parameter indicates which direction a character should face when it finishes moving to the location of this hotspot.

Audio

Since SLUDGE doesn't expose much in the way of audio effects, I created my own fade functionality in audio.slu. It's far from elegant (and doesn't clear musicCurrent when a non-looping track is finished), but it worked well enough for my needs, and let me start, stop and fade music without worrying too much about the current state.

For playing music, I wanted to have a function I could call which took a file handle for a music track and booleans for looping and fading.

sub playMusic(music, loopMusic, fade)
{
	if (musicCurrent != NULL)
	{
		haltMusic(fade);
	}

	musicCurrent = music;

	if (fade)
	{
		setSoundVolume(musicCurrent, 0);
	}
	if (loopMusic)
	{
		loopSound(musicCurrent);
	}
	else
	{
		playSound(musicCurrent);
	}
	if (fade)
	{
		setSoundVolume(musicCurrent, (musicVolume * 10) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 20) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 30) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 40) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 50) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 60) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 70) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 80) / 100);
		pause(3);
		setSoundVolume(musicCurrent, (musicVolume * 90) / 100);
		pause(3);
	}
	setSoundVolume(musicCurrent, musicVolume);
}

musicCurrent is a global variable I used to keep track of which (if any) music track was currently playing. When playing a new track, any currently playing track is stopped beforehand using the fade method specified in this playMusic call (so, if we transitioned to a scene that warranted an abrupt music change to increase impact such as when Yok-yok is abducted, we can do that without worrying which fade method the preceding track was started with).

If when a track should be faded in, I incrementally increase the volume from 10% through to 100% of musicVolume. musicVolume is a global variable that is currently set to 64 in the Above The Waves codebase. I had hoped to expose this via some in-game configuration menu eventually otherwise I would have reduced the amount of calculation being done here and set the volume to static values.

When stopping music, we just fade out if required (using a reversed version of the fade in code from the playMusic() function), and then call stopSound() before setting musicCurrent to NULL.

sub haltMusic(fade)
{
	fade = TRUE;
	if (musicCurrent != NULL)
	{
		if (fade)
		{
			setSoundVolume(musicCurrent, (musicVolume * 90) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 80) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 70) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 60) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 50) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 40) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 30) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 20) / 100);
			pause(3);
			setSoundVolume(musicCurrent, (musicVolume * 10) / 100);
			pause(3);
		}
		stopSound(musicCurrent);
		musicCurrent = NULL;
	}
}

Inventory System

In spite of having convenience methods for many other common point and click adventure functionality, SLUDGE doesn't include anything specifically for handling inventory. Its only collection data type is a stack/queue hybrid type that implements queue and dequeue methods as well as push and pop methods. To keep track of inventory items, I had a global stack variable called inventory, but the first thing I needed for actual inventory functionality was the ability to check whether an item was in a given stack.

sub checkStack(stack, item)
{
	var count = 0;
	var tempStack = copyStack(stack);
	while (tempStack)
	{
		if (popFromStack(tempStack) == item)
		{
			count = count + 1;
		}
	}

	if (count > 0)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

The only options available for doing such a check are to create an entire copy of the stack and pop off every item and made note when the popped item matches the one we're looking for. I used checkStack() when loading scenes to determine whether pick-up-able items should be placed in the scene for any inventory items which the player would keep until the end of the game (if the player already has the item, then there's no value in letting them pick it up again).

For adding and removing inventory items, I used the following code.

sub inventoryAddItem(item)
{
	pushToStack(inventory, item);
	inventoryUpdate();
}

sub inventoryRemoveItem(item)
{
	#TODO: Make this a little more elegant
	if (inventoryVisible)
	{
		deleteFromStack(inventory, item);
		inventoryUpdate();
	}
	else
	{
		deleteFromStack(inventory, item);
	}
	pushToStack(inventoryUsed, item);
}

sub inventoryUpdate()
{
	if (inventoryVisible)
	{
		inventoryHide();
		inventoryShow();
	}
}

Since the rendered inventory doesn't update automatically when the stack it's generated from changes, I chose to hide and then show the inventory again whenever an item is removed. pushToStack() and deleteFromStack() are provided by the SLUDGE API.

For showing inventory items, I used the following code, which would create rows of inventory ten items wide. I had planned to add some scrolling functionality if the player's inventory ever exceeded more than ten items, but since it didn't, that was low priority and fell by the wayside.

sub inventoryShow()
{
	var tempStack = copyStack(inventory);
	var xoffset = 0;
	var yoffset = 0;
	while (tempStack)
	{
		if (xoffset > 680)
		{
			yoffset += 64;
			xoffset = 0;
		}
		var i = popFromStack(tempStack);
		callEvent(getInventoryIconCostume, i);
		addCharacter(i, inventoryXPos + xoffset, 752, inventoryTempIcon);
		setCharacterExtra(i, ICON + RECTANGULAR);
		setCharacterWalkSpeed(i, 80);
		forceCharacter(i, inventoryXPos + xoffset, inventoryYPos + yoffset);
		xoffset += 64;
	}
	callEvent(getInventoryIconCostume, inventoryCloseButton);
	addCharacter(inventoryCloseButton, 1248, 752, inventoryTempIcon);
	setCharacterExtra(inventoryCloseButton, ICON + RECTANGULAR);
	setCharacterWalkSpeed(inventoryCloseButton, 80);
	forceCharacter(inventoryCloseButton, 1248, inventoryYPos + yoffset);
	inventoryVisible = TRUE;
}

Again, we have to make a copy of the entire stack to be able to work with its contents, and we loop through, popping the top item off and increasing a horizontal offset (xoffset) as we go so that the icons are appropriately spaced.

inventoryCloseButton is an inventory item as described above, which calls inventoryHide() in its actionUse() event.

You may notice that we're referring to inventory items as characters here. So far as SLUDGE is concerned, any sprite that we animate onscreen is done so as a character. This allows us to use all of the same kind of functionality that we'd have for the player character, or any other more dynamic object in a scene. In this case, we add the inventory items below the bottom screen boundary and then after setting the inventory icon to be displayed without scaling and to receive click events on transparent areas (the ICON and RECTANGULAR flags for setCharacterExtra()) and an appropriate speed via setCharacterMovementSpeed(), we use forceCharacter() to move the icon to its final position whilst ignoring any floor region constraints.

sub inventoryHide()
{
	var tempStack = copyStack(inventory);
	while (tempStack)
	{
		var i = popFromStack(tempStack);
		removeCharacter(i);
	}
	removeCharacter(inventoryCloseButton);
	inventoryVisible = FALSE;
}

To close the inventory, we must loop through the stack contents (by first making a copy again) and call removeCharacter() for each one.

Cutscenes

Cutscenes came in at the very last second (we were a few hours late with our submission, and I think I'd only just started to look at cutscene functionality when the deadline hit) and didn't quite have the kind of implementation I was hoping for. Since we didn't actually have the cutscenes as animations and were supplementing missing cutscenes with title cards, I needed something that would allow us to wait for mouse input before continuing (giving players a chance to read before moving on).

The abduction cutscene seen in Above The Waves consisted of two functions which looked like this.

sub cutsceneAbduction()
{
	playMusic(musicAbduction, TRUE, FALSE);
	addOverlay('titles/suddenly.png', 0, 0);
	onLeftMouse(runAbductionCutscene);
}

This function is called when the player enters the first underwater scene after solving the pearl puzzle and exiting the cave, shortly after the scene is loaded, but before the player regains control, so as to increase the unexpectedness of something happening.

We play the relevant music track, set the title, and then wait for the user to click to initiate the rest of the cutscene.

sub runAbductionCutscene()
{
	var set1 = newStack('cutscenes/abduction1.png',
						'cutscenes/abduction2.png',
						'cutscenes/abduction3.png',
						'cutscenes/abduction4.png',
						'cutscenes/abduction5.png',
						'cutscenes/abduction6.png',
						'cutscenes/abduction7.png',
						'cutscenes/black.png',
						'cutscenes/abduction8.png',
						'cutscenes/abduction9.png',
						'cutscenes/abduction10.png',
						'cutscenes/abduction11.png',
						'cutscenes/abduction12.png',
						'cutscenes/abduction13.png');

	var speed = 60;

	onLeftMouse(handleCutsceneSkip);

	while (set1)
	{
		if (cutsceneSkip)
		{
			inputEnable();
			transitionRoom(rStorage1);
			return;
		}
		var i = dequeue(set1);
		addOverlay(i, 0, 0);
		pause(speed);
	}
	inputEnable();
	transitionRoom(rStorage1);
}

I would have preferred to use a data structure for this type of cutscene and have a generic method for working through frames (I would also have preferred to have had cutscenes as videos and played them via SLUDGE's playMovie() function, but this is what we had and we made the best of it :) ), but instead, this proved to be fairly functional. Looking back, I should probably have worked on a copy of set1 rather than the stack itself so that if needed, the cutscene could be resumed or restarted without having to call runAbductionCutscene() again, but since I didn't have that functionality in place, nothing was lost by taking this approach.

The ending titles were handled in a similar way to cutsceneAbduction() and looked like this.

sub cutsceneHome()
{
	playMusic(musicMenu, TRUE, FALSE);
	addOverlay('titles/home.png', 0, 0);
	onLeftMouse(cutsceneYay);
}

sub cutsceneYay()
{
	addOverlay('titles/yay.png', 0, 0);
	onLeftMouse(cutsceneMore);
}

sub cutsceneMore()
{
	addOverlay('titles/more.png', 0, 0);
	onLeftMouse(cutsceneCredits);
}

sub cutsceneCredits()
{
	addOverlay('titles/credits.png', 0, 0);
	onLeftMouse(cutsceneSupporters);
}

sub cutsceneSupporters()
{
	addOverlay('titles/supporters.png', 0, 0);
	onLeftMouse(exitGame);
}

Again, we progress only when the user clicks, so that they've got time to read everything.

Everything Else

Above The Waves sidesteps a bunch of fairly common point and click adventure functionality. By being single verb, we sidestep the need for a verb palette, but something similar to the inventory rendering code could handle that fairly nicely. Without any text outside of the placeholder title screens, it's also a poor case for demonstrating SLUDGE's say(), think(), pasteText() or burnText() functionality (it is worth noting that as with inventory handling, SLUDGE has nothing built in for branching dialogue logic or UI). Above The Waves doesn't make use of SLUDGE's character tinting lightmap features or its saving/loading functionality. There's also a bunch of general game state stuff (knowing when the game has ended, etc. - currently Above The Waves exits upon completion), what transitionRoom() looks like, and a few other core bits and pieces that we glossed over here.

With this in mind, I created a second SLUDGE game to serve more as an example project that wasn't subject to the kind of deadline inspired sloppiness Above The Waves' codebase currently suffers from. You can find details of this below.

↑return to top↑

Resources

If the above has left you hankering for more, here are some URLs for documentation, games and more code that I hope will be helpful!

Example Project

With Above The Waves still under development (Mim and I are hoping to expand the game later this year), we spent a couple of additional days putting together a small example SLUDGE game called Robin's Rescue, which uses most of Above The Waves' codebase, but also implements a menu, saving/loading functionality, dialogue (though not use controllable branching dialogue trees) and a few other bits and pieces.

Robin's Rescue was made as a companion piece to this article so that interested readers and tinkerers can see how the code patterns covered above work in the context of a functional game.

Official SLUDGE Resources

When cruising through the SLUDGE docs, it's worth keeping in mind that even though menus expand out to show more options, each parent node in the navigation tree is an actual page that has important information that may not be mentioned/referenced elsewhere (like, for example, the optional objectType parameters for handling walk speed or speech colour which are only covered on the Object Types and Events parent page).

If you know what you're after, but can't find the page, sometimes a search engine limited to results found within "opensludge.github.io/opensludge/doc" can be helpful.

Gotchas

Whilst working on Above The Waves and Robin's Rescue, there were several unexpected behaviours that I came across which are worth keeping in mind when starting a SLUDGE project. With SLUDGE engine development less active at the moment, I don't expect these issues to be resolved any time soon, but I do hope to report them upstream and submit a patch or two if I have time[1]. As I did all of my development on Linux and only tested stuff on Mac OS and in Wine when I knew things were working properly, some of these might be Linux specific issues.

First up, the automatic windowed mode resolution calculations are a little bit funky (check footnote[2] for a workaround). Non-landscape screen orientations can lead windowed and fullscreen modes to be too large for any available screen. This was first reported to me by a Robin's Rescue player who had a 1920x1080 monitor as well as a 1080x1920 monitor.

When running in windowed mode, the game will automatically scale to a resolution that's intended to be a comfortable fit forthe current screen resolution. Aside from the issue above, this is mostly reliable and pleasant. Unfortunately, it can introduce scaling artifacts into your game (check footnote[2] for a workaround). So far as I've been able to find, SLUDGE does not offer the ability to switch assets for lower resolutions, and only has one anti-aliasing mode which may not suit all applications.

When building for Linux, I was keen to have a portable version of the game, but the path that SLUDGE looks for its libraries is baked in as part of the build process. To work around this, I patched the SLUDGE source so that it would first look for a SLUDGE_DATADIR environment variable (possibly not the best name, but it's what I went with).

The speechGap property for objectTypes seems to be ignored. This is noticeable in Robin's Rescue, which uses a custom font that is larger than the demo font included in some of the SLUDGE examples.

When running in the auto-scaled windowed mode, the darkBackground() function causes background images to become offset from the window.

Mentioned above in the The SLUDGE Language section, the documented behaviour for objectTypes' member variables is incorrect, and member variables can not be written to unless being accessed via their qualified path.

Most of this stuff feels like it's minor or at least an edge case. I don't think that any of these issues are particularly crippling, and I don't feel like they should necessarily put anybody off using the engine!

↑return to top↑

A note from cheese

A note from Cheese

Thanks for reading! This is the first tech/engine related article I've published on Cheese Talks, and the first published article since I launched a Patreon campaign to help support my writing (big thanks to all of my supporters!) and game development. I'm super interested to hear any feedback on this piece, particularly regarding its depth (to me, it feels a bit shallow).

Alongside this article, I've also published sources for an example SLUDGE game called Robin's Rescue. Which is built on the experience I'd gained working on Above The Waves.

If you have previously worked with SLUDGE or have been inspired by this article to have a try at making something, please get in touch. I'd love to hear about it!

[1] In the article, I mention several times a desire to lodge upstream issues, but at the time of writing that's still on my todo list.

[2] For windowed mode, at least, this can be worked around by setting FIXEDPIXELS=1 in the user's game specific ini file as described in this page of the SLUDGE documentation.

[3] A copy of the SLUDGE_DATADIR patch can be found bundled with Linux releases of Above The Waves and Robin's Rescue, as well as in the Robin's Rescue repository.

If you've got any thoughts on this article, you can email me at cheese@twolofbees.com.

This article was first published on the 9th of July 2015.