Releases / v0.69.1

v0.69.1 One Whole Pixel

2026-08-19 Engine Latest

The follow-up to the 2D release. Spritesheets reach the sprites wearing them, a 2D camera can land on whole pixels, a sprite's own numbers can be keyframed, and the panels that had run past the edge of a narrow dock no longer do.

Keyframe what a sprite does with its picture

⏱ Animating ▸ ✚ Property ▸ Sprite ▸ Node.

Sprite ▸ frame says which picture. The new lanes beside it say what this node does with it:

  • ppu and size — squash and stretch.
  • pivotX / pivotY — shift the origin, so a crouch keeps its feet on the ground.
  • flipX / flipY — face the other way on a turn.

Every one of those was already reachable from a script and from no clip, which is the wrong way round in a game where the clip is the character. A whole turn-and-swing is now one clip and none of it needs code.

The flips are stepped, like every other on/off lane: 0 is left alone, 1 is mirrored. An eased flip would turn the sprite round exactly halfway between two keys, at a moment nobody authored.

Turn a node you already have into a Sprite

Inspector ▸ Add Component ▸ Type now offers ▫ Sprite, ▩ Tilemap, ▧ Sprite Batch, ⇄ Nav Link and ▨ Nav Area. They were in ✚ New and not in the Type menu, so the only way to make an existing node into a Sprite was to create a fresh one and move the work across.

Panels that fit a narrow dock

Drag ⚙ Settings or ▦ Model as thin as you like:

  • ⚙ Settings ▸ Input wraps. An action with several bindings puts the extra ones on the next line instead of pushing the ✕ that deletes the action off the edge. A binding too wide for a whole line — 🖱 Motion x0.006 (hold RMB) — is shortened with the full name on hover. A long action name is shortened the same way rather than taking the row with it.
  • The collision matrix scrolls sideways. It is one checkbox per pair of layers, so its width is decided by how many layers your project has, not by how wide the panel is.
  • ▦ Model ▸ per-slot material fits, including its Surface maps and Retro artefacts sections and the spritesheet cell picker.
  • Shortcut keys, the reserved-keys list and the how-to text all wrap.

Icons that mean one thing each

was Capsule and Sprite Batch. was Model Mesh and Tilemap. A Reflection Probe wore one glyph in the Inspector and a different one in ✚ New. Tilemap is , Sprite Batch is , Reflection Probe is , everywhere.

VSCode knows the 2D API

The stub library the extension reads was missing 35 node methods — the whole 2D surface (node:setSprite, node:setCamera2D, node:shake, node:sorting, node:setParallax, node:tilemap, node:sprites, …) plus a dozen older ones like node:toWorld, node:lookAt and node:animator. Every call to them was marked as an error on an unknown field, on the one surface you are looking at while you write the line. Re-open a project to refresh .floptle/library/floptle.lua.

The 2D camera can snap to whole pixels

Inspector ▸ Camera 2D ▸ snap to whole pixels, or node:setCamera2D{ pixelSnap = 32 }. The number is pixels per world unit — the same one a ▫ Sprite's pixels per unit uses.

A camera that stops between two pixels resamples every sprite in the scene by a fraction of one, and pixel art shimmers along its edges while nothing is moving. Every 2D project ends up writing this by hand. The engine's version snaps what is drawn and keeps the follow's sub-pixel place, so the camera can still creep slower than a pixel a frame instead of sticking.

Fixes

  • A spritesheet set from the Assets panel did not reach the sprites already wearing that texture. The grid lives on the texture — slice the .png once and every material using it inherits the cells — but the only thing that copied it onto a material was the Inspector's material editor, running for the one node you had selected. Every other sprite still believed its texture was a single whole cell, so it drew the entire sheet stretched across its quad and came out sized from the whole image. Re-slicing a texture now re-slices every material that wears it, and a cell past the end of the new grid falls back into range. Opening a scene puts its materials in step too — and says so in the Console, leaving the scene unsaved, because an exported build ships the scene file and a grid corrected only in memory never reaches it.
  • Clicking a frame in the Material's cell picker did nothing on a ▫ Sprite node. A Sprite carries its own cell — that is what lets one sit on a shared material — and the picker was writing the material's. It now drives the node's, so the control you reach for is the one that draws.
  • Selecting a sprite laid a wrongly-sized quad over it. The selection outline was built without the material or the texture size, so it fell back to the authored world size instead of measuring one cell.
  • camera.pixelsPerUnit() was wrong under an orthographic camera — scaled by the camera's distance from the origin, which has nothing to do with it. A camera 9.33 units back answered 9.33× too small, and only pixelsPerUnit(1) was usable. An orthographic view is the same height at every distance; the argument is now ignored for one.
  • Pinning a retro width did not change the framing. The scene was composited into the fixed target while the camera still projected at the panel's aspect, so the picture came out squashed inside the letterbox — defeating the reason to pin a width, which is that the framing stops depending on the window.
  • A frame with hold: 0 never appeared. It landed on the same instant as the frame after it and lost the tie. Zero, negative and nonsense now mean one frame; a fraction above zero is kept exactly as written.
  • A clip file whose name was capitalised resolved to nothing. Walk.SpriteAnim.ron showed in Assets, dragged onto a controller state, and played silence — the browser matched the extension loosely and the loader matched it exactly. Extensions are matched case-insensitively everywhere now.
  • A texture path saved on Windows found its import settings only on Windows. Backslashes are normalised, so pixel art keeps its Pixelated filter when the project moves between machines.
  • Importing an Aseprite sheet whose tags name frames it does not have is refused with a sentence naming the tag, instead of quietly producing a clip that is short. It happens when the .json and the .png come from different exports.
  • Y-sorted nodes standing at the same height stack the same way after a reload — and after a pooled enemy is destroyed and spawned again. The tiebreak is now left-to-right rather than an internal number.
  • A 2D camera following a name that two nodes share picks the same one every frame instead of whichever it met first, so the picture no longer jumps between two places with nothing in the scene changing. The lookup also runs once per frame rather than once per camera.

Nothing to do on upgrade

Scenes, projects, clips and scripts are unchanged. If you had reached for Sprite ▸ frame in a clip it keeps working exactly as it did; the new lanes sit beside it.

Floptle Hub

Linux x86_64 7.5 MB Download → sha256 7f13135661b1381f24c74a8ff3cfaf17320e3521ce1bfdf2e7133fd7fd6a9b55
macOS Apple Silicon 5.7 MB Download → sha256 24618bfb3930aa05b160b965ca607161db02442862ce9e046a5a94ccb8fdf772
macOS Intel 6.2 MB Download → sha256 9db1b5caa0ac2975da1da4398f7542f82d531cf221d74b6a4b59ea1125dfe9c1
Windows x86_64 7.1 MB Download → sha256 6e6ae6cbfad72acd6c719bf2568f537490d203132e6744fc0e85dcd654bfe60c

Engine

Linux x86_64 20.2 MB Download → sha256 618f2060d2cdde60eb6223b4570cc1c2cb1e46f5a11edf1b9d5bfe9041ca4647
macOS Apple Silicon 16.3 MB Download → sha256 d657bdafdaa9c870c5f7c6eee71308acda95c755598d1e185c49e55e2bcaf459
macOS Intel 17.6 MB Download → sha256 147b29f067d831c8d9e2c4e5e4a3e0ef81edd41a5cdbb7b372b9d050b3b79a14
Windows x86_64 19.9 MB Download → sha256 2b98080e047b4b440e4104f39a583b0db4f15e9097f38b3fa25ff7bcb1bdb88a

The binaries are not signed yet. On macOS, right-click the app and pick Open the first time. On Windows, if SmartScreen appears, pick More info then Run anyway.