Crash-tan Crash-tan

1.46.0 is out and it's a big one!

Crash BASIC 1.46.0 Release Notes

written by Crash-tan

THE "RIG IT UP" RELEASE

A sprite is rarely just one sprite. A character is a torso with a head, two
arms, a sword in one hand — a little tree of pieces that move together. Until
now you wired all of that up by hand, every time, in code, and there was no way
to save the result and bring it back. 1.46.0 starts to fix that: you can now
save and load whole sprite rigs to JSON, give child sprites real rotation
so an arm swings around a shoulder, drive a limb to a target with inverse
kinematics
, anchor a sprite with ORIGIN, put branching logic inside a
behavior or pattern declaration
, and clip a map layer to part of the screen
so your HUD has somewhere to live.

The throughline is reuse and real rigging: build a rig once, save it, drop it
into any project — then articulate it with IK, PIN, and LIMIT constraints,
the building blocks a tool like DragonBones or Spine is made of.


Save and load rigs, behaviors, and patterns

The headline. Three new pairs of statements serialize the moving parts of your
sprites to JSON files and load them back:

SAVE BEHAVIOR "walk" TO "walk.json"
LOAD BEHAVIOR "walk.json"

SAVE PATTERN "ai" TO "ai.json"
LOAD PATTERN "ai.json"

SAVE RIG FROM SPRITE 0 TO "hero.rig.json"
LOAD RIG "hero.rig.json"
LOAD RIG "hero.rig.json" ONTO SPRITE rootId

SAVE BEHAVIOR / SAVE PATTERN write a single named behavior or pattern —
its commands (or steps), its @param defaults, its loop flag — to a file.
LOAD re-registers it under the name stored in the file, exactly as if you'd
typed the BEHAVIOR/PATTERN block again. Re-loading a name overwrites it,
the same as re-declaring.

SAVE RIG is the big one. Point it at a root sprite and it captures the
whole tree: the root, every child (and their children), each piece's relative
offset, its image and frame, its bounding box, flip, visibility, alpha,
rotation, origin (and rotation pivot), elevation, layer, its RIGID flag, and
its IK / PIN / LIMIT constraint config — plus the behaviors and
pattern attached to each piece
. All the behavior and pattern definitions those
pieces reference come along in the same file. (A constraint's runtime target —
the live point an IK/PIN chases — isn't saved; you re-aim it after loading.
The configuration that defines the rig is what persists.)

' Build a two-part rig...
SET SPRITE 0, 100, 100, "torso"
SET SPRITE 1, 100, 80, "head"
SET SPRITE 1 PARENT(0)
SET SPRITE 1 BEHAVIOR "bob"
SAVE RIG FROM SPRITE 0 TO "hero.rig.json"

' ...then spawn a fresh copy somewhere else, later, or in another project:
rootId = NEXTFREESPRITE()
SET SPRITE rootId, 200, 150, "torso"
LOAD RIG "hero.rig.json" ONTO SPRITE rootId   ' head re-attaches at its offset

LOAD RIG … ONTO SPRITE id rebuilds the children onto a root you've already
placed — the saved offsets re-attach the head, the arm, the sword exactly where
they were, and their behaviors/patterns re-register and re-bind. Plain
LOAD RIG file$ with no ONTO just registers the rig's behaviors and patterns
without spawning any sprites — handy when you want the logic but will place the
sprites yourself.

This is the foundation for a sprite rigger: author a character's hierarchy
and motion once, save it as a reusable asset, and instantiate it on demand.

Files go through CrashPlayer's normal storage, so this works the same on
desktop and in the browser. Each file carries a format version; a file newer
than the player understands is refused with a clear message rather than loaded
half-way.


RIGID — child sprites that actually rotate with the parent

Attach a child to a parent and, by default, the child follows the parent —
it tracks the parent's position, keeping its offset. But it didn't used to
rotate with the parent. Spin the torso and the arm slid along beside it
instead of swinging around the shoulder.

That default stays (it's what you want for a HUD pin, a name tag, a held item
that shouldn't tilt). When you do want articulation, opt in with RIGID:

SET SPRITE 2, 20, 0, "forearm"
SET SPRITE 2 PARENT(upperArm) RIGID

A RIGID child treats its offset as rigidly fixed to the parent's frame: as the
parent rotates, the child orbits around the parent's origin and inherits
the parent's spin
. That's the whole game for articulated rigs — chain
shoulder → upperArm → forearm → hand with RIGID at each joint and the arm
moves as one limb. Without RIGID, the old translation-only behavior is
unchanged.

(Scale inheritance — a child shrinking when the parent shrinks — is still off
the table for now. One gap at a time.)


IK — inverse kinematics, the part that makes rigs feel alive

RIGID is forward kinematics: you rotate the shoulder, and the hand follows.
Inverse kinematics is the other direction — you give the hand a target point,
and the solver figures out the shoulder and elbow rotations needed to reach it.
That's the "grab the hand and the whole arm bends to follow" interaction that
DragonBones and Spine are built on, and it's now one line:

' A chain: the tip plus its LENGTH ancestors via PARENT.
SET SPRITE hand IK TARGET(mx, my) LENGTH 2 BEND CCW

' ...then every frame, just re-aim it:
SET SPRITE hand IK TARGET(mx, my)

The chain is the tip sprite plus the LENGTH sprites above it in the PARENT
hierarchy — so it rides entirely on the rigs you already build. A FABRIK
solver (any chain length, not just two bones) rotates every joint so the tip
reaches the target, and the rest of the chain trails naturally.

  • TARGET(x, y) — the world point the tip reaches toward (an (x, y) pair
    or a Coordinate, so the mouse, another sprite, or a computed point all work).
  • LENGTH n — how many bones above the tip to include.
  • BEND CW|CCW — which way the interior joints bend (the elbow/pole hint).
  • MIX m — blend 0..1 between the plain FK pose and the solved pose.
  • ITERATIONS k — solver pass cap (default 10; more = tighter reach).

A bare TARGET on an already-configured tip just re-aims it (the chain config
sticks), and IK NONE removes the constraint. Declaring IK automatically marks
the chain links RIGID so the solved rotations actually propagate. Give each
chain member an ORIGIN at its joint
(below) so it rotates around the right
point — without that, a sprite pivots around its corner and slides off the bone.

It's all per-tick and local, so an IK rig behaves identically in single-player
and across CrashNet with nothing new on the wire.


PIN and LIMIT — the other two constraints

Two smaller constraints round out the rigging kit:

PIN eases a sprite's position toward a target each tick — independent of
any rotation chain. Good for "the eyes follow the cursor", a tracking reticle, a
camera that lags behind the player:

SET SPRITE eye PIN TARGET(mx, my) MIX 0.3   ' 0..1: how hard it chases
SET SPRITE eye PIN NONE

LIMIT clamps a child's local offset to a range, or locks it to a single
axis — for mechanical rigs like pistons, sliders, and treads:

SET SPRITE piston PARENT(block) RIGID
SET SPRITE piston LIMIT X 0, 20 AXIS X   ' slides 0..20 on X only
SET SPRITE piston LIMIT NONE

ORIGIN — one anchor for positioning and rotation

Sprites have always been anchored at their top-left corner — SET SPRITE
positions that corner, and rotation pivoted somewhere else entirely. That's
fine until you parent, rotate, or rig something, at which point you want the
sprite anchored at a sensible point — usually its center. Now you can say so:

SET SPRITE 0, 160, 120, "ball" ORIGIN(0.5, 0.5)   ' anchored at its center
SET SPRITE 0 ROTATE(45)                            ' ...and spins around it

ORIGIN(ox, oy) takes relative 0–1 coordinates ((0,0) top-left, (0.5,0.5)
center, (1,1) bottom-right) — or a Coordinate value. It's the single anchor
the sprite is positioned at, parented at, IK-jointed at, and rotated around.

That last part is the important bit: the rotation pivot follows the origin by
default
, so a sprite rotates around its anchor instead of some unrelated
point. (If you genuinely need the rotation centre to differ from the position
anchor, an explicit ROTATE(angle, px, py) pivot still overrides just the
rotation.) This is what makes IK look right — anchor each bone at its joint and
the whole chain bends cleanly.

Default behavior is unchanged: a sprite with no ORIGIN is still anchored at
its top-left, exactly as before.


Branching logic inside BEHAVIOR and PATTERN declarations

You can now put IF / THEN / ELSIF / ELSE / END IF and SELECT CASE
inside a BEHAVIOR or PATTERN block, to choose which commands or steps
the declaration actually contains:

BEHAVIOR "enemy"
  IF hardMode THEN
    BMOVE 4, 0
    BGRAVITY 0.5
  ELSE
    BMOVE 2, 0
  END IF
END BEHAVIOR

PATTERN "ai" LOOP
  IF boss THEN
    "charge" DURATION 60
  ELSE
    "walk" DURATION 30
  END IF
  SELECT CASE mode
    CASE 1
      "patrol" DURATION 90
    CASE ELSE
      "idle" DURATION 30
  END SELECT
END PATTERN

This is declaration-time logic — the condition is evaluated once, against
your live variables, at the moment the BEHAVIOR/PATTERN is registered, and
it bakes the chosen commands into the definition. Re-declare the same behavior
with a changed variable to swap what it does:

hardMode = TRUE
BEHAVIOR "enemy"          ' now bakes in the hard-mode commands
  ...
END BEHAVIOR

SELECT CASE supports the full grammar you'd expect — CASE 2, 3 lists,
CASE IS > 5 comparisons, CASE ELSE. It's the same IF/SELECT you write
everywhere else, just usable in this new position.

This pairs with — and doesn't replace — pattern WHEN. WHEN is the runtime,
per-tick test ("do this step only while a key is held"); declaration-time
IF/SELECT decides which steps the pattern is built from in the first place.
Both work together.


CLIP — give a map layer its own corner of the screen

Map layers assume they own the whole screen — a tilemap or plane fills every
pixel. That's a problem the moment you want a HUD or a menu strip, because the
map paints right over it. SET LAYER n CLIP (x1,y1)-(x2,y2) restricts a layer
to a screen rectangle so it leaves the rest free:

CREATE MAP FROM map TILESET "tiles" TILESIZE 32 AS LAYER 0
SET LAYER 0 CLIP (0,0)-(212,240)   ' map only renders in the left 212px
' ... draw your HUD on another layer in the right strip ...
SET LAYER 0 CLIP NONE              ' later: back to full screen

The rectangle is in logical screen pixels, normalized (corners ordered, clamped
to the screen) and mapped through any letterbox. It's a property of the layer,
so it composes cleanly with everything a layer can already do:

  • a layer SHADER (the shaded output is clipped, not an intermediate),
  • AFFINE / PERSPECTIVE plane modes (the Mode-7 floor clips to the box),
  • sprites assigned to the layer — they clip with it.

The clip persists across frames until you change it, so set it once at setup.
CLIP NONE removes it and the layer fills the screen again.


A lot more sprites — and they stay fast

The ceiling on how many sprites you can have at once jumped from 1,000 to
100,000
. Bullet-hell screens full of projectiles, a thick field of particles,
a crowd, a forest — go wide. NEXTFREESPRITE and RESERVE SPRITE reach across
the whole range, and IDs that used to be off-limits (a sprite at, say, 99,999)
just work.

The bigger deal is what happens while you're making them. Spawning a large
wave of sprites used to get slower and slower the more were already on screen —
filling a dense field could hitch for a noticeable beat. That's gone: creating
sprites now stays fast no matter how many are already live, so a level that
suddenly spawns thousands of them does it in one smooth step instead of a
stutter. Everything else — moving them, animating them, collision — keeps the
speed it always had.

Nothing in your code changes. The same ENABLE SPRITE / SET SPRITE /
NEXTFREESPRITE you already write just has a lot more headroom and no slowdown
behind it.


Multiplayer

The layer features cross the wire like their neighbors: SET LAYER … CLIP is
layer configuration that replicates the same way SHADER and ALPHA already
do, so every client clips the layer identically. Rigs, behaviors, patterns,
RIGID attachment, and the IK / PIN / LIMIT constraints all resolve
per-interpreter (world transforms have always been computed locally), so they
behave the same in single-player and CrashNet without anything new on the
network. ORIGIN rides on the existing sprite-state channel, so a sprite's
anchor replays correctly on a late-join or SET ROOM transition.


Compatibility notes

  • Everything you already wrote still works. The new statements and modifiers
    (SAVE/LOAD BEHAVIOR/PATTERN/RIG, RIGID, IK, PIN, LIMIT,
    ORIGIN, declaration-time IF/SELECT, and SET LAYER … CLIP) are all
    strictly additive.
  • Child sprites still default to translation-only. Rotation inheritance is
    opt-in via RIGID — existing parent/child code is unchanged.
  • Sprites still anchor at their top-left by default. ORIGIN is opt-in; a
    sprite with no ORIGIN positions and rotates exactly as it did before. Where
    ORIGIN is set, the rotation pivot follows it unless an explicit
    ROTATE(angle, px, py) pivot overrides it.
  • CLIP is a new optional clause on SET LAYER. Layers with no clip render
    full-screen exactly as before.
  • The sprite ceiling went up, not down. The 1,000 → 100,000 raise and the
    faster bulk spawning are pure headroom — any program that ran under the old
    limit runs identically, just with far more room and no slowdown as the count
    climbs.

Build the rig once. Save it. Bring it back. Go make a game.

— Crash-tan