Or, the canvas is our enemy

This is the first day of my 30 Day “Game Jam” challenge series.

Today I’ve spent ten minutes getting a pipeline set up, and then about an hour or so trying to write a basic game loop along with a simple resource manager, using object-oriented TypeScript.

Hoo boy. Let’s break down the work.

GitLab

This one is easy. As part of my day job, I manage a production GitLab pipeline for building a fairly complex, modern ASP.NET application with an Angular front end. Spinning up a simple GitLab Pages pipeline that builds a TypeScript-based web page is cake. I’ve done it quite a bit.

Really, you only need a few lines of YAML:

stages:
  - build
  - publish

build:
  stage: build
  image: "node:latest"
  artifacts:
    paths:
      - dist
  script:
    - npm ci
    - npm run build-prod

pages:
  stage: publish
  needs:
    - build
  script:
    - mkdir public
    - cp -r dist/* public
  artifacts:
    paths:
      - public
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

That’s it. Of course, you need to have a proper webpack file written, which is where the bulk of the complexity comes in when it comes to something like this. But that’s not what this post is about. That’s the “easy win” for the day.

Dependency Injection for Dummies

I’m a big fan of dependency injection. I’m building a whole emulation platform off of the .NET application builder pattern (a blog series for a later time). So, I set out to make a couple big dependencies that the game is going to need:

  • The GameController, which, you know, controls the game, manipulates game objects (that’s a generic term, Unity lovers) and queues things up to be drawn by the
  • CanvasController, which handles all the minutiae of rendering to the HTML canvas. It’s the GameController’s interface (thats’s a generic term, OOP lovers) to the canvas.

The GameController depends upon a CanvasController. So, in theory, you could write your own canvas controller that uses, say, p5.js, or use another 2D game engine, and the GameController’s logic need not change. This is great! All in all, it doesn’t take long to define a couple of methods that an interface (no longer the generic term) might need:

  • DrawImage: way to draw an image on the screen
  • ClearFrame: A way to clear the canvas, to make way for a new frame.

We’ll ignore for a second that this means that the GameController will be requesting animation frames. I’m not quite ready to separate that concern yet.

Resource Management

Right now, the GameManager is also our resource manager. It takes care of loading resources like sprites, sounds, or blobs of JSON. I decided to use a simple, non-spritesheet-based system, where every frame of animation will be a discrete image. This will make managing our resources a bit more complicated, but it means I can just pass around individual images and not worry about calculating offsets or anything like that. This might change later, we’ll see!

When you want to load a resource, you tell the GameManager to loadResource, and you pass it a path to that resource alongside a ResourceType. It’ll go out and fetch that resource and put it into an array of similar resources.

This is where I found my first bit of complexity: how to you tell how big an image is?

I tried to make this dynamic. I really did:

private addImage(path: string){
  const image = new Image();
  const images = this.images;
  image.addEventListener('load', function () {
    images.push({ image, width: this.naturalWidth, height: this.naturalHeight });
  })
  image.src = path;
}

However, I can’t quite figure out how to get the naturalWidth and naturalHeight populated into my ImageResource type.

But, hey, it turns out: You can access those later, and you really don’t need to have a type that stores those values. Lesson learned: don’t overcomplicate things. I learn that lesson nearly every day, you would think it’d be ingrained by now.

this.ctx.drawImage(img.image, dx, dy, img.image.naturalWidth * scale, img.image.naturalHeight*scale);

Moving Around the Screen

Okay, we have a (very simple) CanvasController and we’ve passed it to our (very simple) GameController. Now we just have to do the rest of the owl.

I spent a bit too much time futzing about with this resource loading paradigm this morning, so I don’t really have time to get started on moving the “player” around right now. Maybe we’ll get back to that later today, but it might end up being the beginning task for the next “workday”.

UPDATE: I decided to ago ahead and, at the very least, add the concept of positions and vectors to the game.

So, I included a (very simple) class, Vector2. It does all of the basic things you’d expect a vector class to do:

export class Vector2 {
  constructor(public x: number, public y: number){
  }

  Normalize(){
    const length = Math.sqrt((this.x * this.x) + (this.y * this.y) );
    this.x = this.x / length;
    this.y = this.y / length;
  }
  Multiply(magnitude: number){
    this.x *= magnitude;
    this.y *= magnitude
  }
  Add(vec: Vector2){
    this.x += vec.x;
    this.y += vec.y;
  }
  Dot(vec: Vector2): number{
    return (this.x * vec.x) + (this.y * vec.y);
  }
}

Vectors are a lot simpler than I think a lot of people assume they are. If you struggle with vectors, I urge you (strongly) to checkout this video by Freya Holmér. It is, by far, the most thorough and, importantly, easy-to-grok rundown on vectors ever to grace the halls of YouTube. Did you watch it? No, it’s three hours long, you say? That’s okay, I guess. I’ll run down the methods here:

  • Normalize: Sets the magnitude of the vector to 1. It does this by determining the length of the vector (the hypotenuse of the triangle made up by the coordinates from the origin) and then scaling x and y by that number. This way, if we tell our player to go up and to the right, we won’t end up with a greater-than-speed velocity.
  • Multiply: I made the decision to keep to pure magnitude multiplication. That is, we only change the magnitude of the vector, we can’t multiply a vector by another vector. Given the use case for this, namely scaling a normalized vector to a “speed” modifier, I don’t foresee any issues here.
  • Add: This is a straightforward additon of two vectors. This will, mostly, be used to add a unit’s velocity to their position to get a new position.
  • Dot: The dot product is probably the most “complicated” of these, but you’ll notice that the actual calculation is relatively straightforward. As I said before, I strongly urge you to check out Freya’s video on the topic (here’s a link directly to the section on the dot product). I’ll be using the dot product in order to check whether a unit in the game can see another unit, and to check whether effects affect other units (an attack, for example).

That’s it! That’s all we need out of a vector class for this little game. We need a way to store where a unit is, and we need a way to store whiere that unit is moving. Given I have yet to define what a “unit” is, or even what sort of gameplay we’re going to have, this will do for now.

I’ll have to circle back around to this on the next work day.