Skip to content

Quick Start🔗

This example, using Bun, demonstrates Banjo in under 125 lines of code. All of these files can all be found within the quick-start examples folder of the GitHub repo. You can clone the repo, open a shell in the examples/quick-start folder, then use bun server.ts to run:

Bun is not a requirement to use Banjo. It's just our favorite way to prototype and build

This is a simple template to provide minimal styling and the elements used by the "game".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Banjo Project</title>
    <style>
      body,
      html {
        height: 100%;
        margin: 0px;
      }
      #debugHUD {
        left: 8px;
        font-size: 3rem;
        position: absolute;
        top: 8px;
      }
      #ball {
        background: blue;
        border: solid 0px transparent;
        border-radius: 1rem;
        height: 16px;
        position: absolute;
        width: 16px;
      }
    </style>
    <script type="module" defer src="./game.ts"></script>
  </head>
  <body>
    <div id="ball"></div>
    <div id="debugHUD"></div>
  </body>
</html>

This contains the actual "game" logic. It updates a <div> with the current estimated engine FPS and TPS, animates a round <div> across the screen in a bouncing ball pattern, and uses a watcher to monitor the engine pause state separately from the engine itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { createEngine } from '@studiokeywi/banjo/engine';
import { clamp } from '@studiokeywi/banjo/math/conversions';
import { native } from '@studiokeywi/banjo/math/random';
import { add, vector2 } from '@studiokeywi/banjo/math/v2';
import { createWatcher } from '@studiokeywi/banjo/watcher';

const pause = () => {
  if (!engine.paused) {
    engine.pause();
  }
};
const unpause = () => {
  if (engine.paused) {
    engine.pause();
  }
};

const hud = document.querySelector<HTMLDivElement>('#debugHUD')!;
const isPaused = createWatcher({
  do: () => {
    if (engine.paused && !hud.innerText.includes('Paused')) {
      hud.innerText += ' | Paused';
    }
  },
});
let FPS: number;
let TPS: number;

const ball = document.querySelector<HTMLDivElement>('#ball')!;
const rng = native();
const speedX = rng.randRange(1, 5, true) * (rng.randFloat() < 0.5 ? 1 : -1);
const speedY = rng.randRange(1, 5, true) * (rng.randFloat() < 0.5 ? 1 : -1);
const ballSpeed = vector2(speedX, speedY);
const startX = rng.randRange(0, innerWidth - 16, true);
const startY = rng.randRange(0, innerHeight - 16, true);
const ballPosition = vector2(startX, startY);

const engine = createEngine({
  TPS: 60,
  render: delta => {
    hud.innerText = `Engine TPS: ${TPS} | Engine FPS: ${FPS}`;
    ball.style.left = `${ballPosition.x + delta * ballSpeed.x}px`;
    ball.style.top = `${ballPosition.y + delta * ballSpeed.y}px`;
  },
  update: () => {
    ({ FPS, TPS } = engine);
    if (!isPaused.running) {
      isPaused.start();
    }
    add(ballPosition, ballSpeed, ballPosition);
    if (ballPosition.x < 0 || ballPosition.x >= innerWidth - 16) {
      ballPosition.x = clamp(ballPosition.x, 0, innerWidth - 16);
      ballSpeed.x *= -1;
    }
    if (ballPosition.y < 0 || ballPosition.y >= innerHeight - 16) {
      ballPosition.y = clamp(ballPosition.y, 0, innerHeight - 16);
      ballSpeed.y *= -1;
    }
  },
});

addEventListener('beforeunload', () => {
  engine.stop();
  isPaused.stop();
  removeEventListener('blur', pause);
  removeEventListener('focus', unpause);
});
addEventListener('blur', pause);
addEventListener('focus', unpause);

if (document.readyState === 'complete') {
  engine.start();
} else {
  addEventListener('load', () => {
    engine.start();
  });
}

This contains a simple Bun server to host the HTML and transpile the "game" logic on the fly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const server = Bun.serve({
  async fetch({ url }) {
    const path = new URL(url).pathname;
    if (path === '/game.ts') {
      const [body] = (await Bun.build({ entrypoints: ['./game.ts'] })).outputs;
      return new Response(body);
    }
    if (path.startsWith('/src:')) {
      return new Response(Bun.file(path.slice(5)));
    }
    return new Response(Bun.file('./index.html'));
  },
});

console.log(`Now listening on http://${server.hostname}:${server.port}\n${'-'.repeat(25 + server.hostname.length + server.port.toString().length)}`);