How to Build a Plugin
This tutorial was written in February 2026, for v2 of the engine.
Plugins let you bolt new methods onto the engine and wrap existing ones — all without touching engine code. They're perfect for stuff you find yourself rebuilding across projects: debug overlays, screen shake, particle systems, that kind of thing.
We're going to build a debug overlay plugin from scratch. It'll show FPS and mouse coordinates on screen, toggled with a single function call. Along the way we'll cover the full plugin API, and then walk through creating, testing, and releasing your plugin on Floaty.
The Plugin API
A plugin is a function you pass to engine.use(). It receives two tools: register for adding new methods and wrap for decorating existing ones:
engine.use((register, wrap) => {
// register('name', function(...args) { })
// adds a brand-new method to the engine
// wrap('name', function(original, ...args) { })
// decorates an existing method
});
That's the whole thing. register creates something new, wrap modifies something that already exists. Let's look at each one.
Registering New Methods
register(name, fn) adds a brand-new method to the engine. Once registered, it's available everywhere — on the engine instance directly, and inside scope() callbacks right alongside the built-ins.
Here's a plugin that registers a greet method. Inside the function, this is the engine instance, so you've got access to all the drawing and input methods:
engine.use((register) => {
register('greet', function (x, y, color) {
this.text('hello!', x, y, color);
});
});
engine.scope(({ start, cls, greet }) => {
function draw() {
cls(0);
greet(10, 10, 7);
}
start({ sprites: {}, sounds: {}, draw, target });
});
Notice that greet gets destructured right alongside start, cls, and the rest. Your plugin method is a first-class citizen.
Tip: If you try to register a name that already exists — say, cls or a method from another plugin — the engine throws a helpful error telling you to use wrap instead. No silent overwrites.
Wrapping Existing Methods
wrap(name, fn) lets you intercept an existing method. Your wrapper receives the original as its first argument — call it to keep the default behaviour, then add your own stuff before or after.
Here's a plugin that wraps cls to draw a white border around the screen after every clear:
engine.use((register, wrap) => {
wrap('cls', function (original, color) {
original(color);
this.rect(0, 0, 127, 127, 7);
});
});
Every time anything calls cls, the original clear happens first, then the border gets drawn on top. The rest of your game code doesn't know or care.
You can wrap the same method multiple times, even from different plugins. They chain automatically — each wrapper calls the previous one through original, like layers of an onion.
Building a Debug Overlay
Let's build something real. A debug overlay is one of the first things you want in any project — FPS counter, mouse coordinates, and a way to toggle it on and off.
We'll start with the toggle. We need a debug() method that flips a flag. Since the plugin function is a closure, we can store state in a plain variable — no need to put it anywhere on the engine:
engine.use((register) => {
let enabled = false;
register('debug', function () {
enabled = !enabled;
return enabled;
});
});
Call debug() once to turn it on, call it again to turn it off. The enabled variable lives in the plugin's closure, so it persists across frames and stays private to this plugin.
Drawing the Overlay
Now the visual part. We wrap cls so that after every clear, if debug mode is on, we draw the FPS counter and mouse position in the top-left corner.
Here's the complete plugin — register and wrap working together:
engine.use((register, wrap) => {
let enabled = false;
register('debug', function () {
enabled = !enabled;
return enabled;
});
wrap('cls', function (original, color) {
original(color);
if (!enabled) return;
const { x, y } = this.mouse();
this.text('fps:' + this.currentFps, 1, 1, 7);
this.text('x:' + x + ' y:' + y, 1, 8, 7);
});
});
The wrapper reads this.currentFps for the measured frame rate and this.mouse() for the cursor position. The enabled flag comes from the closure — shared between debug and the cls wrapper without touching any engine state.
Tip: The overlay draws after cls but before anything else in your draw() function. If you want the overlay to render on top of everything instead, wrap a different method — or call a dedicated drawDebug() at the end of your draw function.
Creating Your Plugin on Floaty
The snippets above show the plugin running locally with engine.use(). To use it on Floaty, you need to create it through the plugin editor.
Sign in and head to your plugins page. Create a new plugin — the editor opens with a template. The format is a bit different from the engine.use() version: plugins on the site use export default function:
export default function (register, wrap) {
let enabled = false;
register('debug', function () {
enabled = !enabled;
return enabled;
});
wrap('cls', function (original, color) {
original(color);
if (!enabled) return;
const { x, y } = this.mouse();
this.text('fps:' + this.currentFps, 1, 1, 7);
this.text('x:' + x + ' y:' + y, 1, 8, 7);
});
}
The logic is identical — the only change is the outer wrapper. Paste this into the editor, give your plugin a name, and save.
Testing in Draft Mode
Open any playground you own — or create a new one. Open the plugins sidebar and look under "Your Plugins." Your debug overlay should be listed there. Click Add.
By default, your own plugins attach in draft mode. You'll see a "Use draft" checkbox that's already ticked. This means the playground loads your latest saved code from the editor, not a released version. It's your development loop:
- Edit the plugin code in the plugin editor
- Save
- Refresh your playground
- See the changes immediately
To test the overlay, call debug() in your game's init() function (or trigger it with a key press). The FPS counter and mouse coordinates should appear in the top-left corner.
Publishing a Release
Once you're happy with your plugin, open the plugin editor and click "Releases" in the top bar. The sidebar shows your release history. Create a new release — this snapshots your current code as a numbered version.
Other users can then find and add your plugin to their playgrounds. Their playground pins to the version they added — when you release a new version, they'll see an indicator and can choose to upgrade from the version dropdown.
Going Further
- Screen shake — register a
shake(intensity, duration)method and wrap drawing methods to apply a random offset each frame while the timer runs - Particle system — register
emit()andparticles()methods, wrapclsto update and draw particles after each clear - Multiple plugins — plugins compose without conflict. Register methods in one, wrap them in another. The engine keeps everything sorted
- Sharing publicly — mark your plugin as public in the editor so other folks can search for it and add it to their playgrounds