Build a tiny game for JS13K with Kontra.js
Tags: javascript, gamedev, webdev, js13k
Published: 2021-08-13 17:01:00

Do you think you can build a game using less than 13kB of JavaScript, CSS, and/or HTML in just 30 days? Do I have a challenge for _you_!

The [**2021 JS13K competition**](https://js13kgames.com/) organized by [GitHub Star @end3r](https://stars.github.com/profiles/end3r/) just kicked off with the announcement of the theme **SPACE**.

You can interpret that theme however you want - recreate classic Space Invaders or Asteroids-style games, make a game that's only controllable with the <kbd>SPACE</kbd> bar, build a game where you explore the space between two objects, or whatever else you can imagine. Just don't run out of _space_ - you only have 13kB to work with :wink:

If you've never done anything like this, or even coded much JavaScript before, it can be a little intimidating. Here's a quick little tutorial how to build this _suh-weeet_ game using [Kontra.js](https://github.com/straker/kontra) (a tiny game library made just for JS13K) plus a few lines of code:

<img width="40%" src="https://user-images.githubusercontent.com/121322/128929638-fc2dc1c9-83e7-4fd1-8447-e34fd5c6872c.gif">

[Play the game](https://leereilly.net/mish-lee-13k/), [view the source](https://github.com/leereilly/mish-lee-13k), or follow along with the steps and [corresponding diffs](https://github.com/leereilly/mish-lee-13k/commits/main) below.

## 1. Generate your HTML template

If you're a regular reader of DEV then it's likely you won't need much help with this, but let's start off with a super-simple HTML template:

```html
<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <canvas width="250" height="250" id="game" style="background-color: black;"></canvas>
  </body>
</html>
```

Looking at that in your browser, you should see a ⬛ - our play area. 

<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/9f22af75e3482d3ae1733f26b8b2b3221a0337af/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/9f22af75e3482d3ae1733f26b8b2b3221a0337af) for end of step 1</sup>

## 2. Include Kontra.js library

To keep things simple, we'll just pull the [latest version of Kontra from a CDN](https://www.jsdelivr.com/package/npm/kontra) and include the functions / helpers we know we'll be using after the `</canvas>` tag:

```html
<script src="https://cdn.jsdelivr.net/npm/kontra@7.1.3/kontra.min.js"></script>
<script>
  let { GameLoop, Sprite, bindKeys, collides, init, initKeys, keyPressed, randInt } = kontra;

  let { canvas } = init();
</script>
```

<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/464d86c183fa9cace8f58ddccfd1ecd8b2f9e939/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/464d86c183fa9cace8f58ddccfd1ecd8b2f9e939) for end of step 2</sup>

## 3. Ready player one!

First, let's define an image for player 1 after `let { canvas } = init();`. We'll use my GitHub avatar for quickness / ego boosting:

```javascript
let image1 = new Image();
image1.src = 'https://avatars.githubusercontent.com/u/121322?v=4'
image1.width = 40;
image1.height = 40;
```

Next, we'll create our sprite and position it on the top left of the screen:

```javascript
let sprite1 = Sprite({
  x: 40,
  y: 40,
  anchor: {
    x: 0.5,
    y: 0.5
  },
  image: image1
});
```

Now we'll define our game loop and start things ticking!

```javascript
let loop = GameLoop({
  render: function() {
    sprite1.render();
  }
});
```

If you view your game in the browser now, you should see my avatar in a big black square. Woo hoo - progress!

<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/41d97a4ece056b1a4b2f7a50cc406c8054b5cfd5/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/41d97a4ece056b1a4b2f7a50cc406c8054b5cfd5) for end of step 3</sup>


**Wait! Where did that image URL come from? How can I use my own?** You can grab that avatar URL easily from the GitHub API e.g.

```bash
$ curl -s https://api.github.com/users/leereilly | jq -r '.avatar_url' 
https://avatars.githubusercontent.com/u/121322?v=4
```

or

```bash
$ curl -s https://api.github.com/users/leereilly | grep -i avatar_url
  "avatar_url": "https://avatars.githubusercontent.com/u/121322?v=4",
```

Dunno about you, but here's what I feel like every time I run `curl` or `jq` commands against the GitHub API in a terminal:

<div align="center">
  <img src="https://chihacknight.org/images/blog/remote-hack-night-hackerman.gif">
</div>

Anyway, I digress. Looking at a static sprite on a black square isn't a whole heck of a lotta fun, so let's get moving!

## 4. Make player 1 move

Let's introduce an [`update()` function](https://straker.github.io/kontra/api/gameLoop#update) within our game loop that responds to <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> and moves our sprite appropriately:

```javascript
update: function() {
  if (keyPressed('left')) {
    sprite1.x = sprite1.x - 1;
  }

  if (keyPressed('right')) {
    sprite1.x = sprite1.x + 1;
  }

  if (keyPressed('up')) {
    sprite1.y = sprite1.y - 1;
  }

  if (keyPressed('down')) {
    sprite1.y = sprite1.y + 1;
  }
},
```

We also need to add a call to `initKeys();` just before `loop.start();`:

```javascript
initKeys();

loop.start();
```

You should now be able to move player 1 around the screen :joystick:

<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/9b55f0877a89b21a0bf75aa146f3bba4f07f888a/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/9b55f0877a89b21a0bf75aa146f3bba4f07f888a) for end of step 4</sup>

## 5. Introduce the enemy

We can definitely make this game more fun. Let's add our enemy player - my buddy @mishmanners* - somewhere randomly, but not outside the bounds of the screen.

<sup>* this has nothing to do with Michelle kicking my butt at Fornite, Magic The Gathering, and snake [building](https://dev.to/mishmanners/how-to-enable-github-actions-on-your-profile-readme-for-a-contribution-graph-4l66) / [battling](https://dev.to/mishmanners/take-your-python-skills-to-the-next-level-with-battlesnake-3f0f) amongst other things.

We'll start by defining the maximum X and Y values for our sprite (basically the canvas dimensions) and then make use of Kontra's [`randInt()` helper](https://straker.github.io/kontra/api/helpers#randInt) to set the sprite's location:

```javascript
let maxX = 250;
let maxY = 250;

let image2 = new Image();
image2.src = 'https://avatars.githubusercontent.com/u/36594527?v=4'
image2.width = 40;
image2.height = 40;

let sprite2 = Sprite({
  x: randInt(0, maxX),
  y: randInt(0, maxY),
  anchor: {
    x: 0.5,
    y: 0.5
  },
  image: image2
});
```
<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/afc59f8b7391dbf3a43b489fd49c105ebcc1b5b0/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/afc59f8b7391dbf3a43b489fd49c105ebcc1b5b0) for end of step 5</sup>

## 6. Add some collision detection

This is where your university-level math knowledge will come in handy. 

Just kidding. This sounds pretty intimidating, but thankfully Kontra does all of the hard work for us with the [`collides()` helper](https://straker.github.io/kontra/api/helpers#collides). Let's just move the player 2 sprite to a random position once there's a collision by adding the following at the end of the `update()` function:

```javascript
if (collides(sprite1, sprite2)) {
  sprite2.x = randInt(41, maxX - 40);
  sprite2.y = randInt(41, maxY - 40);
}
```

<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/0055acaefebdeb4592c6ec7403a325cb7e5f62ab/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/0055acaefebdeb4592c6ec7403a325cb7e5f62ab) for end of step 6</sup>

## 7. Make it pixelated/8-bit with this one neat trick!

This ~~tip~~ hack to make your sprites look pixelated is pretty easy. Since we're using the GitHub Avatar URL, we can change the query param from `v=4` to `s=10` to request a 10x10 pixel version.

```diff
- https://avatars.githubusercontent.com/u/121322?v=4
+ https://avatars.githubusercontent.com/u/121322?s=10
```

Since we're setting the image to 4 times that in the code, the browser will attempt to resize it making it look pixelated.

![Before and after](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l4c8qpwpul7xm1c8lg7d.png)

![MAGIC](https://c.tenor.com/bxHQ4KcM8eMAAAAC/magic-meme.gif)

**Note:** There are definitely more sophisticated techniques, and using images this big is a horrendous idea for JS13K. It's better to use something like [Aseprite](https://github.com/aseprite/aseprite) or [Piskel](https://github.com/piskelapp/piskel) to create your own pixel art.

<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/f03324d79d7c54fcb95dac35b74cc65a73fafb1a/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/f03324d79d7c54fcb95dac35b74cc65a73fafb1a) for end of step 7</sup>

## 8. Add some sounds effects

There isn't much room for OGGs and MP3s in JS13K. Thankfully, people smarter than I have developed some neat libraries and editors where you can create your sound effects and background music to include with just a few lines of code. 

Taking [@xem](https://github.com/xem)'s [MiniSoundEditor](https://xem.github.io/MiniSoundEditor/) as just one example, I can select from some predefined sounds and just copy and paste the JavaScript. 

I'll do just that and copy and paste this at the end of the `if (collides(sprite1, sprite2))` block:

```javascript
f = function(i){
  var n=2e4;
  if (i > n) return null;
  var q = t(i,n);
  i=i*0.7;
  return (Math.pow(i*50,0.8)&66)?q:-q;
}

t=(i,n)=>(n-i)/n;
A=new AudioContext()
m=A.createBuffer(1,96e3,48e3)
b=m.getChannelData(0)
for(i=96e3;i--;)b[i]=f(i)
s=A.createBufferSource()
s.buffer=m
s.connect(A.destination)
s.start()
```

I literally have no idea what that does, but I feel smarter having copied and pasted it. You will too. Try copying and pasting that (or your own sound) at the end of the collision detection code.

⚠️ Obviously don't blindly copy, paste and _use_ code blindly off the Internet if you don't know what it does. Thankfully, this is harmless.

By now, your code should look a little something like this:

```html
<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <canvas width="250" height="250" id="game" style="background-color: black;"></canvas>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/kontra@7.1.3/kontra.min.js"></script>
  <script>
    let { GameLoop, Sprite, bindKeys, collides, init, initKeys, keyPressed, randInt } = kontra;

    let { canvas } = init();

    let maxX = 250;
    let maxY = 250;

    let image1 = new Image();
    image1.src = 'https://avatars.githubusercontent.com/u/121322?s=10'
    image1.width = 40;
    image1.height = 40;

    let sprite1 = Sprite({
      x: 40,
      y: 40,
      anchor: {
        x: 0.5,
        y: 0.5
      },
      image: image1
    });

    let image2 = new Image();
    image2.src = 'https://avatars.githubusercontent.com/u/36594527?s=10'
    image2.width = 40;
    image2.height = 40;

    let sprite2 = Sprite({
      x: randInt(0, maxX),
      y: randInt(0, maxY),
      anchor: {
        x: 0.5,
        y: 0.5
      },
      image: image2
    });

    let loop = GameLoop({
      update: function() {
        if (keyPressed('left')) {
          sprite1.x = sprite1.x - 1;
        }

        if (keyPressed('right')) {
          sprite1.x = sprite1.x + 1;
        }

        if (keyPressed('up')) {
          sprite1.y = sprite1.y - 1;
        }

        if (keyPressed('down')) {
          sprite1.y = sprite1.y + 1;
        }

        if (collides(sprite1, sprite2)) {
          sprite2.x = randInt(41, maxX - 40);
          sprite2.y = randInt(41, maxY - 40);

          f = function(i) {
            var n = 1e4;
            var c = n / 3;
            if (i > n) return null;
            var q = Math.pow(t(i, n), 2.1);
            return (Math.pow(i, 3) & (i < c ? 16 : 99)) ? q : -q;
          }

          t = (i, n) => (n - i) / n;
          A = new AudioContext()
          m = A.createBuffer(1, 96e3, 48e3)
          b = m.getChannelData(0)
          for (i = 96e3; i--;) b[i] = f(i)
          s = A.createBufferSource()
          s.buffer = m
          s.connect(A.destination)
          s.start()
        }
      },
      render: function() {
        sprite1.render();
        sprite2.render();
      }
    });

    initKeys();

    loop.start();
  </script>
</html>
```

And it should look a little like this in your browser:

<img width="40%" src="https://user-images.githubusercontent.com/121322/128929638-fc2dc1c9-83e7-4fd1-8447-e34fd5c6872c.gif">

The sound in this GIF doesn't seem to be working, but you should hear a beep every time the sprites touch.

And there you have it. A game that will provide ~~hours~~ minutes of fun. Keep your eye out on Steam for the full release.

<sup>💾 [Source](https://github.com/leereilly/mish-lee-13k/blob/ba34c62da74de3ae937d1c5be9048e7823788b93/index.html) + [diff](https://github.com/leereilly/mish-lee-13k/commit/ba34c62da74de3ae937d1c5be9048e7823788b93) for end of step 8</sup>

## A step further

If you look at the file sizes, you'll see this is weighing a bit more than 13kB:

```
$ ls -lth
total 88
-rw-r--r--@ 1 leereilly  staff    28K Aug 13 09:50 kontra.min.js
-rw-r--r--@ 1 leereilly  staff   674B Aug 13 09:49 mishmanners.jpeg
-rw-r--r--@ 1 leereilly  staff   679B Aug 13 09:48 leereilly.jpeg
-rw-r--r--@ 1 leereilly  staff   2.2K Aug 13 08:07 index.html
```

We're using the minified version of Kontra, but that still includes a few things we don't need. See the Kontra website for details on [reducing the file size even further](https://straker.github.io/kontra/reducing-file-size)

## Join JS13K!!!

Please feel free to fork and expand upon this on for your own JS13K entry. There are lots of things you could improve...

* Make it a two-player game (player 2 could respond to <kbd>W</kbd>  <kbd>A</kbd>  <kbd>S</kbd>  <kbd>D</kbd> )?
* Add support for high scores?
* Introduce some more some sound effects?
* Add some actual gameplay LOL

Better yet, start from scratch and have some fun. Here are some other resources that might be useful:

* [Kontra.js tutorials](https://straker.github.io/kontra/tutorials)
* [Micro game engines and boilerplates](https://js13kgames.github.io/resources/#:~:text=Micro%20game%20engines%20and%20boilerplates)
* [Sound and music](https://js13kgames.github.io/resources/#:~:text=mouse%20/%20touch%20inputs-,Sound%20and%20music,-SoundBox%20-%20an%20HTML5)
* [Artwork and fonts](https://js13kgames.github.io/resources/#:~:text=that%20are%20%3C%2013kb)
* [Minification](https://js13kgames.github.io/resources/#:~:text=column-based%20raycasting-,Minification,-Try%20Terser%20-%20a)
* [Misc. tools](https://js13kgames.github.io/resources/#:~:text=all%20mobile%20devices-,Other%20tools,-Video%20Game%20Name)
* [Tutorials](https://js13kgames.github.io/resources/#:~:text=Hexadecimal%20viewer/editor-,Tutorials,-How%20to%20minify)
* [Post-mortems](https://js13kgames.github.io/resources/#:~:text=Blog%20posts%20and%20post%20mortems)

Good luck and have fun! Would love to see your entries  in the comments below `<3`

## Troubleshooting

Did you encounter some bugs along the way following this tutorial? If you've never used it before, [Chrome's Developer Console](https://developer.chrome.com/docs/devtools/open/) is your friend. 

Press <kbd>⌘</kbd> + <kbd>Option</kbd> + <kbd>J</kbd> (macOS) or <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>J</kbd> (Windows, Linux, Chrome OS) to jump straight into the console panel. From there you'll see what isn't working correctly...

<img width="815" src="https://user-images.githubusercontent.com/121322/128947497-b2085468-29b4-413d-ac47-04105b82ca5f.png">

If you felt like a L337 H4X0R running `curl` or `jq` commands, you'll feel like you're in the matrix now with the things you can do in there.

You can also look in this [repo](https://github.com/leereilly/mish-lee-13k) to see the full source code. If you look at the [commit history](https://github.com/leereilly/mish-lee-13k/commits/main), you'll see the diffs/code for each of the steps above.