j3m

Ludum Dare 59

Full illustrations from Many Wearing Rapiers

Our game for Ludum Dare 59 is done! Sharing the full illustrations made by the talented @StealingPandas for this project.

Her Majesty the Queen Queenie-W.jpg

Princess Catherine Princess-W.jpg

Doctor Montague Doctor-W.jpg

Word Puzzles Are Hard

For our game this year, we decided quickly we'd go with the idea of steganography. It was going to be about Elizabethan courtiers sending hidden messages embedded inside innocuous ones. So I came up with the idea that:

  1. Each message would contain a reference to an ordinal number, like "third" or "fourth".
  2. That would mean that you would take every e.g. third or fourth letter of the message to get the hidden meaning.

This turned out to be INCREDIBLY HARD to design. I suck at word puzzles at the best of times, but after hours and hours Saturday, the best I could come up with was this stilted, barely grammatical "poem" using the dubiously real word "sallyeth":

How can I ever express

How my love sallyeth?

Jewelry ever t'was

A seventh bit thy equal

A dream to I

The like of a kiss

...which should spell out HERMAJESTYDIES if read correctly. Saturday night I ditched the idea and went with a much simpler form of steganography.

The lesson: I don't know a lot about anagrams, language puzzles and so on, and that extends to having a really poor read on what kinds of puzzles are going to be hard or easy to design. It sounded so doable at first!

Making a web game engine when time is measured in hours

For LD59, I knew we were going to make a web game. I don't like frameworks and engines, so I knew I'd be making it from scratch. So once we settled on our game idea Saturday morning, I put an HTML5 canvas on the easel, installed a Typescript compiler and got to work.

Our game was going to be just a matter of clicking on buttons to navigate between screens. Mouse-only input. It could probably have been as a regular HTML page, no canvas, but I anticipated some basic animation which seemed like it would be at least equally annoying to code in CSS. So I opted for the direct control of a canvas.

The asset loader

I knew from experience that the easiest way to get images into your canvas game is by embedding them into the HTML page as <img /> tags, and then drawing them with drawImage(). The problem is, those img tags need to be finished loading by the time the call to drawImage() happens. So you always need some kind of "loader" code to wait for them to load before you actually run anything.

What we want is a little object called, say, the image manager that we can tell about all the images in the game. It'll keep track of which ones are loaded, and only let the game start once they're all ready. Usage will look something like this:

typescript const images = new ImageManager(); for (let image of document.querySelectorAll("img")) { images.add(image as HTMLImageElement); } images.onComplete = startTheGame; // set a callback images.finish(); // that's all the images!

The logic needed in the class is pretty obvious, and mine looks like this:

```typescript class ImageManager { nLoading : number = 0; complete : boolean = false; onComplete : Function;

add(image : HTMLImageElement)
{
    this[image.id] = image;

    if (image.complete)
    {
        return;
    }

    this.nLoading += 1;

    image.addEventListener("load", this.onLoad.bind(this));
}

finish()
{
    if (this.nLoading == 0)
    {
        this.complete = true;
        this.onComplete();
    }
}

onLoad()
{
    this.nLoading -= 1;

    if (this.nLoading == 0)
    {
        this.complete = true;
        this.onComplete();
    }
}

} ```

The one extra little flourish in there is the line

typescript this[image.id] = image;

This just provides a convenient way for me to get at my assets in the game code. All I have to do is give my image tags an ID, like <img id="princess" src="princess.png" />, and I can get them from code via images["princess"].

Dimensions and coordinates

One thing you gotta know about HTML canvases is that they have two sets of dimensions. They have the dimensions of the actual HTML element, and then they have the resolution of the canvas itself. The latter is set using the width and height properties on the canvas. So for example:

html <style> canvas { width: 800px; height: 600px; } </style> <canvas width="100" height="100"></canvas>

If I draw a 100x100 test image on this canvas, it won't sit in the top left corner of an 800x600 canvas, as you might expect. It will fill the 100x100 canvas that you've requested in HTML, and CSS will then scale that to 800x600.

To avoid this, you need some code like this, to make the resolution of the canvas match its true on screen dimensions at all times:

```typescript function onResize() { canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; }

addEventListener("resize", onResize); ```

The game is going to be 16x9 aspect ratio. So if the user resizes the window, the canvas needs to scale to be the largest 16x9 box it can within that window. Probably the simplest way to do this by far is just to use CSS, I did something much, much more complicated. I told CSS to make the canvas fill the screen, and then computed the largest 16x9 box I could within that. So now my onResize looks like this:

```typescript function onResize() { canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;

let widthRatio = canvas.width / 1920;
let heightRatio = canvas.height / 1080;
let ratio = Math.min(1, widthRatio, heightRatio);

width = Math.floor(ratio * 1920);
height = Math.floor(ratio * 1080);

x0 = (canvas.width - width) / 2;
y0 = (canvas.height - height) / 2;

} ```

This code is setting the global variables x0, y0, width, height which define a little box within the canvas that the actual graphics code will draw into. I think I did this because I imagined I might want to draw a border around the game if there was extra room, sort of like what the Super Gameboy used to do. But it adds a bunch of complexity. Not really worth it.

The simplest thing for a short game jam is to set up your code so that your actual graphics and gameplay code can simply assume that the drawing area starts at (0, 0) and is always exactly 1920 by 1080 pixels. So if you want to draw something at, say, the middle bottom of the screen, you can just hard code the coordinates 1920 / 2 and 1080. To make this possible, you need a piece of code responsible for resizing the canvas, like above, and then a layer wrapping the drawing API that basically converts the gameplay code's "virtual coordinates" into physical coordinates.

The "engine"

Now we need to get our event loop set up, kind of like if we were making a Raylib or SDL game. So the top level code of our game looks like this:

```typescript function onResize() { // same as before }

function onMouse(event : MouseEvent) { let mouse = {}; mouse.clicked = false; if (event !== undefined) { if (event.type == "mousemove") { let canvasX = event.clientX; let canvasY = event.clientY; mouse.x = (canvasX - x0) * (1920 / width); // convert physical coordinates into "virtual" coordinates for the gameplay code mouse.y = (canvasY - y0) * (1080 / height); }

    if (event.type == "click")
    {
        mouse.clicked = true;
    }
}

gameSpecificOnMouse(mouse);

}

function render() { gameSpecificRender(); }

let x0, y0, width, height : number; // globals to store canvas dimensions

addEventListener("resize", onResize); addEventListener("mousemove", onMouse); addEventListener("click", onMouse); setInterval(render, 1000 / 60); ```

Substructure: individual screens

Since the game involves moving between several different individual "screens", it's nice to be able able to code each screen with its own, separate render and mouse event handler functions. So we introduce some types to represent this:

``` enum ScreenType { LETTERSCREEN, PERSONSCREEN, DEATHSCREEN, FAILURESCREEN, VICTORYSCREEN, TITLESCREEN, TUTORIAL_SCREEN }

interface Screen { enter() : void onMouseEvent(event : Mouse) : ScreenType | null render(context : CanvasRenderingContext2D, x, y, w, h : number) : ScreenType | null } ```

The purpose of the enter() function is that it'll get called when we first enter the screen. After that, render() will get called once per frame. Notice that the screen's onMouseEvent and render functions return a ScreenType enum. This lets the gameplay logic in those functions decide whether or not it's time to go to a different screen, and report that back to the top-level engine code:

```typescript // in the top level render function

let nextScreen = screen.render(artist, x0, y0, width, height);

if (nextScreen != null) { switch (nextScreen) { case ScreenType.LETTER_SCREEN: screen = letterScreen; break;

    case ScreenType.PERSON_SCREEN:
        screen = personScreen;
        break;

    // etc
}

screen.enter()

} ```

And that's basically it!