constserver=Bun.serve({asyncfetch({url}){if(newURL(url).pathname==='/game.ts'){const[body]=(awaitBun.build({entrypoints:['./game.ts']})).outputs;returnnewResponse(body);}returnnewResponse(Bun.file('./index.html'));},});console.log(`Now listening on http://${server.hostname}:${server.port}\n${'-'.repeat(25+server.hostname.length+server.port.toString().length)}`);
The Ticks Per Second (or TPS) is a measure of how often you want your game logic to run
The render function will get called approximately as many times per second as the user's monitor refresh rate
The update function will get called approximately as many times per second as defined in TPS
The Engine interface exposes the approximate FPS and TPS for quick access like this
1 2 3 4 5 6 7 8 9101112131415
constserver=Bun.serve({asyncfetch({url}){constpath=newURL(url).pathname;if(path==='/game.ts'){const[body]=(awaitBun.build({entrypoints:['./game.ts']})).outputs;returnnewResponse(body);}if(path.startsWith('/src:')){returnnewResponse(Bun.file(path.slice(5)));}returnnewResponse(Bun.file('./index.html'));},});console.log(`Now listening on http://${server.hostname}:${server.port}\n${'-'.repeat(25+server.hostname.length+server.port.toString().length)}`);
IntelliSense doesn't recognize DOM types like document?
Try adding the "DOM" value to your tsconfig.json in the lib property under compilerOptions:
{"compilerOptions":{"lib":["DOM"]// (1)!}}
While not required, we suggest a minimum lib setting of ["DOM", "DOM.AsyncIterable", "DOM.Iterable", "ESNext"]
Under the hood, Banjo's game loop utilizes requestAnimationFrame. This means we can attempt to target the user's monitor refresh rate for maximum FPS, and provide developers with a customizable target for game ticks (or updates) per second. Both the render and update functions are passed a delta value in milliseconds.
The render function should handle whatever logic your game requires to display the game. This could involve using the <canvas> element, manipulating DOM elements, or more based on your project structure. For now, we'll create a simple HUD using a <div> to render engine data.
render is passed a delta value representing the elapsed time since the last frame
If you have been running your code after each change (or using Bun's --watch mode to restart automatically), you'll notice that nothing is actually updating on the page. This is because the engine needs to be started first. We advise using patterns to start, stop, or pause/unpause the engine based on browser ready state and whether it has focus:
The pause and unpause helpers here exist to make sure that we don't alter the pause state incorrectly (although that shouldn't happen in this example)
1 2 3 4 5 6 7 8 9101112131415
constserver=Bun.serve({asyncfetch({url}){constpath=newURL(url).pathname;if(path==='/game.ts'){const[body]=(awaitBun.build({entrypoints:['./game.ts']})).outputs;returnnewResponse(body);}if(path.startsWith('/src:')){returnnewResponse(Bun.file(path.slice(5)));}returnnewResponse(Bun.file('./index.html'));},});console.log(`Now listening on http://${server.hostname}:${server.port}\n${'-'.repeat(25+server.hostname.length+server.port.toString().length)}`);
Pausing the engine does not stop its internal loop; it only prevents the update and render callbacks from being executed. So you may be wondering how you can detect/display the paused state of the engine from outside of the render and update loops? This seems like a good use case for a Watcher:
This usage of a Watcher operates outside of the normal engine update cycle. This should not be used for logic that affects the game state directly
The Watcher object performs behaviors based on configurable conditions and timings. Here, we want the Watcher to do our update for the pause display.
Watcher objects run for a set period of time before stopping, and so this will restart the watcher automatically as needed.
1 2 3 4 5 6 7 8 9101112131415
constserver=Bun.serve({asyncfetch({url}){constpath=newURL(url).pathname;if(path==='/game.ts'){const[body]=(awaitBun.build({entrypoints:['./game.ts']})).outputs;returnnewResponse(body);}if(path.startsWith('/src:')){returnnewResponse(Bun.file(path.slice(5)));}returnnewResponse(Bun.file('./index.html'));},});console.log(`Now listening on http://${server.hostname}:${server.port}\n${'-'.repeat(25+server.hostname.length+server.port.toString().length)}`);
While technically a full Banjo project, this is still a little... basic. Our final step for this guide will be introducing a "bouncing ball" animation. It's still pretty simple, but it shows how easily Banjo features can be combined to obtain desired behavior:
constserver=Bun.serve({asyncfetch({url}){constpath=newURL(url).pathname;if(path==='/game.ts'){const[body]=(awaitBun.build({entrypoints:['./game.ts']})).outputs;returnnewResponse(body);}if(path.startsWith('/src:')){returnnewResponse(Bun.file(path.slice(5)));}returnnewResponse(Bun.file('./index.html'));},});console.log(`Now listening on http://${server.hostname}:${server.port}\n${'-'.repeat(25+server.hostname.length+server.port.toString().length)}`);