Most of the functions if floaty closely resemble functions in Pico-8 . There is a bit of extra code needed due to the JS nature of floaty , so the quickest way to get started is to use the following snippet as a starting point:
import { Engine } from 'https://floaty.dev/engine-v1.js';
var engine = new Engine();
engine.expose();
var sprites = {
// this is where your sprites go
};
var sounds = {
// this is where your sounds go
};
function init() {
// things to do when the game starts
}
function update() {
// things to do each game tick:
// updating the game state
// moving the player
// handling input
}
function draw() {
// things to do each time the screen updates:
// - clearing the canvas
// - drawing on the canvas
}
engine.start({
sprites,
sounds,
init,
update,
draw,
});
// code: https://codepen.io/assertchris/pen/yLZMVGO
One of the goals of floaty is that it should be easy to get started. That's why I've gone with a module importing approach. You can use this code in any modern browser without a build step. The code for the examples on this website actually works like this.
In the layout file:
<script type="module">
import { Engine } from 'https://floaty.dev/engine-v1.js';
window.Engine = Engine;
</script>
In each preview:
window.gameX = new window.Engine();
window.gameX.expose();
// game x code
window.gameX.start({
sprites,
sounds,
init,
update,
draw,
target: document.querySelector('.targetX'),
});
You don't actually need to assign the
Engine
instance to a variable, but I do because we need a way to stop one game when another is started, for
performance reasons.
To make
floaty
more like Pico-8, I made a way to expose commonly used functions to the top scope. They aren't global by
default. You have to enable that by calling the
expose
method:
var engine = new Engine();
// the following will not work partly because
// spr is undefined at this point
spr('player', 16, 16);
engine.expose();
engine.start({ ... });
// the following will work as long as it's inside
// init or draw and the engine has been started
spr('player', 16, 16);
Every global function can also be used on the same
Engine
instance
expose
was called on:
var game1 = new Engine();
game1.expose();
// these two function calls do the same thing
spr('player', 16, 16);
game1.spr('player', 16, 16);
var game2 = new Engine();
game2.expose();
// this will happen in game2 not in game1
spr('player', 16, 16);
// if you want to do more stuff in game1 you will need
// to use the non-global form
game1.spr('player', 16, 16);
If you plan to have multiple games running on the same page, you need to write your games without using global functions. It's definitely possible to do this using the correct context:
var engine = new Engine();
var random;
function update() {
random = this.rnd() * 127;
};
function draw() {
this.spr('player', 16, random);
}
engine.start({ ... });
Animator
is a small utility class that handles starting and stopping of animation loops. If you wanted to animate
something in typical JS; you might write something like this:
function animate() {
// animate all the things
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
You might also build a way to cancel the animation. This is great code because it overcomes a lot of the
downsides of traditional
setTimeout
/
setInterval
approaches, but it does make it harder to control things like max FPS.
Animator packages
requestAnimation
frame up with the ability to call:
start(fps = 60, animation) -> void
stop() -> void
frameRate([new fps]) -> void | int
You can call
frameRate
with no arguments to find out what the current framerate is.
All sounds are handled by the Tone.js
library. Synthesizing sound in the browser is hard, and I don't want to maintain that kind of code. I'd much prefer to let Tone.js handle the heavy lifting. Heavy is the right word, though. At the time of writing, the engine source code is less than 10% of the size of Tone.js; so keep that in mind when building games with sound. You might want to create your own basic MP3/WAV playback functionality. The moment you use the sfx()
function, you'll load all the Tone.js source code as well.
The easiest way to understand what the engine is using Tone.js for is by look at an example sound:
var sounds = {
'explosion': [
1,
[40, 0.8, 2],
[40, 0.8, 2],
[40, 0.8, 2],
[40, 0.8, 2],
[30, 0.8, 2],
[20, 0.8, 2],
[10, 0.8, 2],
[10, 0.8, 2],
[10, 0.8, 2],
[10, 0.8, 2],
],
};
In this
'explosion'
sound, the first number is the length of time each note is played for. The greater
this number, the longer the sound effect will be.
This is followed by arrays of
[frequency, volume, instrument]
. Frequency is the pitch of the note.
The available instruments are those supported by the Audio Context API . You can use the following instrument numbers:
0
= sine
1
= square
2
= sawtooth
3
= triangle
You're unlikely to use this class directly, because it's cumbersome to use. Calling the
sfx('explosion')
function will pick up this array inside
sounds
, and handle all the instrument, frequency, and volume nonsense for you.
Preloader
is another small utility class that allows you to define your assets away from your game code. Say you're
building a game in a team; where you are writing code another someone else is making sprites, and someone
else
is making sounds.
You might not all want to be working in the same JS file; and you probably don't want to be hand-coding sprites and sounds.
In this case, the
Engine
provides a
preload
function that takes a URL. This URL can be anywhere (provided you set up the appropriate
CORS headers
). You can use
Preloader
directly, or via the engine:
var sounds = {
'explosion': engine.preload('https://example.com/explosion'),
};
These are asynchronously loaded, so you need to remember to call the
load
method before you require any preloaded assets:
var mode = 'loading';
async function init() {
// these don't need assets for spr() or sfx()
// so they can do their thing before load()
stars.init(this);
particles.init(this);
// this loads all the preloaded assets
await this.load();
// now we can begin using the assets in update() and draw()
mode = 'start';
}
Right now, the engine API isn't very stable. When it is, I will figure out a way to tag it, probably via different URLs for different versions. Expect change, but feel free to reach out with questions or for help.
I have tried to stick as closely to the defaults of Pico-8 as possible.
The screen is 128 pixels wide and 128 pixels high. If the container is bigger then the
canvas on which everything is draw will scale to fit while maintaining a square aspect ratio. That means
your game will always look pixelated, even if each pixel is actually a square block of pixels. You
can inspect the
ratio
property if you're interested how many pixels wide and high each pixel is:
function update() {
text('ratio: ' + this.ratio, 16, 16);
}
The rest of the screen, around the game canvas, will be the same as color
0
. If you want to override this, you can provide a different background color to the
start()
function:
engine.start({
// ...
background: 'white',
});
The background color can be any supported by CSS .
It can be tricky to get the container to stretch, especially if you're not overly familiar with CSS or HTML. Try something like this:
<!doctype html>
<html>
<head>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.game {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div class="game"></div>
<script type="module">
import {Engine} from "https://floaty.dev/engine-v1.js";
var game = new Engine();
// ...
game.start({
// ...
target: document.querySelector('.game'),
});
</script>
</body>
</html>
In Pico-8, these are 8x8 or a multiple thereof; and larger sprites are drawn using optional parameters on the
spr
function. They're also 8x8 in
floaty
, though that is not enforced very well by the definition of the
sprites
variable. Use this pattern to avoid issues:
var sprites = {
'player': [
-1, -1, -1, 2, 2, -1, -1, -1,
-1, -1, 2, 8, 8, 2, -1, -1,
-1, -1, 2, 8, 8, 2, -1, -1,
-1, 2, 14, 8, 8, 14, 2, -1,
2, 14, 8, 7, 12, 8, 14, 2,
2, 8, 8, 1, 1, 8, 8, 2,
-1, 2, 8, 5, 5, 8, 2, -1,
-1, -1, 2, 9, 9, 2, -1, -1,
],
};
In this array,
-1
means no color is draw on that pixel. All other numbers are the numbers corresponding to palette colors.
I use the same colors as the default color palette in Pico-8:
0
=
#000000
1
=
#1D2B53
2
=
#7E2553
3
=
#008751
4
=
#AB5236
5
=
#5F574F
6
=
#C2C3C7
7
=
#FFF1E8
8
=
#FF004D
9
=
#FFA300
10
=
#FFEC27
11
=
#00E436
12
=
#29ADFF
13
=
#83769C
14
=
#FF77A8
15
=
#FFCCAA