Crash-tan Crash-tan

1.45.0 - Making life a bit easier for sprite makers

Crash BASIC 1.45.0 Release Notes

written by Crash-tan

THE "LET ASEPRITE DO THE WORK" RELEASE

You already draw your animations in Aseprite. You set the timing on the
timeline. You drag a slice box around the hitbox. Then you'd come over to
Crash BASIC and... re-type all of it by hand — a FRAMERATE number you
eyeballed, a BBOX you measured with your finger on the screen.

Not anymore. 1.45.0 teaches LOADIMG to read the parts of an .ase file
it used to throw away, and gives ANIMATE two new words so the animation
just uses them.

  • FRAMERATE OFSPRITE — play back at the per-frame timing you set on
    the Aseprite timeline, instead of one flat fps for the whole clip.
  • AUTOBBOX — let the collision box follow the animation, frame by
    frame, from the slice you drew in Aseprite.
  • LOADIMG … SLICES boxes() — hand you the per-frame slice rectangles
    as an array, to do whatever you want with.
  • LOADIMG … TAG("name") — load just one animation tag's frames out of a
    multi-animation .ase.

FRAMERATE OFSPRITE — your timeline timing, honored

Aseprite lets you give every frame its own duration. A long pose, three
quick in-between frames, a long pose again. Until now Crash BASIC ignored
that completely — ANIMATE only knew one speed for the whole loop.

LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32)

BEHAVIOR "walk"
    ANIMATE "HERO", 0 TO 7 FRAMERATE OFSPRITE
END BEHAVIOR

FRAMERATE OFSPRITE says "use the durations baked into the file." Frame 0
holds for however long you set it in Aseprite, frame 1 for its own time,
and so on — variable per frame, exactly as authored. Re-time the animation
in Aseprite, re-export, and your game's playback updates with it. No magic
number to keep in sync.

It needs a real Aseprite load. Point FRAMERATE OFSPRITE at a plain PNG
sprite sheet (which carries no per-frame durations) and you halt with a
clear message telling you why — not a silently frozen sprite.


AUTOBBOX — the hitbox follows the animation

Draw a slice in Aseprite, animate it across your frames (it's keyframed,
so a punch frame gets a wider box, a crouch frame a shorter one), and
AUTOBBOX applies it to the sprite automatically as the animation plays:

LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32)

BEHAVIOR "attack"
    ANIMATE "HERO", 0 TO 5 AUTOBBOX FRAMERATE OFSPRITE
END BEHAVIOR

Every time the animation advances, the bounding box updates to match the
slice on that frame. It feeds straight into the existing collision system
— boundary pushback, sprite-to-sprite, SPRITECOLLISION(),
SPRITETOUCHING$() — so your hitbox is whatever you drew, when you drew
it.

AUTOBBOX is independent of FRAMERATE OFSPRITE; combine it with either
that or a fixed fps:

ANIMATE "HERO", 0 TO 5 AUTOBBOX FRAMERATE 12   ' fixed speed, animated hitbox

Bare AUTOBBOX uses the file's first slice. If your file has several
slices and the first isn't the one you want, name it:

ANIMATE "HERO", 0 TO 5 AUTOBBOX "body" FRAMERATE OFSPRITE

A frame that no slice key covers simply leaves the box where it was. Ask
for AUTOBBOX on an image with no slices (or a name that doesn't exist)
and you halt loud, with the available slice names listed.


More than one slice? Query the rest by name

A sprite has a single collision box, so AUTOBBOX drives one slice. But an
.ase can hold several named slices at once — a body, a sword, a feet
point — and you can read any of them on demand for your own (manual) hit
checks:

' World-space box of a named slice at the sprite's CURRENT frame:
sword = SPRITEBBOX(player, "sword")     ' (x1,y1)-(x2,y2) in world pixels
body  = SPRITEBBOX(player)              ' no name = the active collision box

' Relative-0-1 box of a slice on a specific frame, no sprite needed:
b = IMAGEBBOX("HERO", "feet", 3)
SET SPRITE 0 BBOX b

SPRITEBBOX(id, name$) gives you world coordinates (sprite position +
scale applied), ready to test against other things. IMAGEBBOX(image$, name$, frame) gives the relative box straight from the file. Name a slice
that isn't there and you halt loud, with the available names listed.


LOADIMG … SLICES boxes() — the rectangles, in your hands

Want the slice boxes for yourself instead of (or in addition to) the
automatic hookup? Add a SLICES clause and LOADIMG fills an array:

LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32) SLICES boxes()

' boxes(frame) is a (x1,y1)-(x2,y2) Range in relative 0–1 coordinates,
' one entry per frame — keyframes already expanded.
SET SPRITE 0, 100, 100, "HERO"
SET SPRITE 0 BBOX boxes(2)        ' use frame 2's slice as the hitbox

Each element is a Range in relative 0–1 coordinates, ready to drop
straight into the new range form of BBOX (below). The array is always
dense and frame-indexable — frames with no slice fall back to the full
frame (0,0)-(1,1) — so boxes(anyFrame) is always valid.

SLICES is Aseprite-only. Use it on a PNG, or on an .ase with no
slices, and you get a loud error rather than a quietly empty array.


TAG("name") — load just one animation

An .ase usually holds every animation in one file — idle, run, jump, all on
the same timeline, separated by Aseprite tags. LOADIMG … TAG("name")
pulls out just one tag's frames:

LOADIMG "hero.ase", "HERO_RUN" TAG("run")

BEHAVIOR "running"
    ANIMATE "HERO_RUN", 0 TO 5 FRAMERATE OFSPRITE
END BEHAVIOR

Only the run tag's frames are loaded, re-indexed from 0 — so ANIMATE … 0 TO 5 just plays the run, no offset math. The per-frame durations and slice boxes
come along narrowed to that tag, so FRAMERATE OFSPRITE and AUTOBBOX keep
working on it. An .ase already implies a sprite sheet, so you don't even need
AS SPRITESHEET — though you can still add SLICES to grab the tag's hitboxes.

Want several animations? Load the file once per tag, under different names:

LOADIMG "hero.ase", "HERO_IDLE" TAG("idle")
LOADIMG "hero.ase", "HERO_RUN"  TAG("run")
LOADIMG "hero.ase", "HERO_JUMP" TAG("jump")

Tag names match case-insensitively. Ask for a tag that isn't in the file (or
put TAG on a plain PNG) and you halt loud, with the available tag names
listed.


BBOX now takes a Range too

To make those slice rectangles pluggable, SET SPRITE … BBOX learned a
second form. Alongside the classic four numbers, you can now hand it a
single Range value:

SET SPRITE 0 BBOX(0.2, 0.2, 0.8, 0.8)   ' the form you already know
SET SPRITE 0 BBOX boxes(curFrame)        ' new: a relative-0–1 Range value

Both mean the same thing — a relative-0–1 collision box. The range form
just lets you pass one you already have (from SLICES, or built yourself)
without unpacking it into four arguments.

While we were in there, range values can now hold fractional
coordinates.
A range literal like (0.1, 0.2)-(0.8, 0.9) keeps its
decimals instead of truncating to whole numbers. Whole-number ranges are
completely unchanged — they print and step over the integer grid exactly
as before.


Solid boundaries are solid again

Two collision bugs that let a sprite slip through a SOLID boundary are
fixed:

  • Pass-through after partial penetration. If a sprite's box ended up
    partway inside a wall (a small hitbox, a teleport, a frame-change), the
    engine used to let it keep moving — it would flip to the far side and
    sail out the other end. Now a solid boundary blocks any motion that
    pushes deeper; a box that's genuinely stuck inside can still back out the
    way it came.
  • Tunneling at speed. A single step bigger than a wall is thick could
    jump clean over it in one tick. A swept check now catches the
    overshoot and stops the sprite at the wall face.

Sliding along walls and ceilings is unchanged — only the
pass-straight-through cases got closed.


Multiplayer

All of this works the same in single-player and in CrashNet. The Aseprite
per-frame durations and slice boxes are read on every interpreter when the
file loads, so FRAMERATE OFSPRITE timing and AUTOBBOX hitboxes resolve
identically on the server and on each client. Nothing new crosses the
wire; just call LOADIMG once at the top of your program as always.


Also in 1.45.0 — arrays, sharper edges, a few new words

The headline is Aseprite, but a batch of language polish rode along.

REDIM keeps your data now

REDIM used to quietly throw away everything in the array and hand you a
fresh zero-filled one. Now it preserves what fits: grow an array and
your existing elements stay put (the new slots default-fill); shrink it and
the elements that remain are kept. There's no PRESERVE keyword to
remember — preserving is just what REDIM does. (Want a clean slate? That's
what DIM and ERASE are for.)

PUSH / POP / QUEUE / DEQUEUE are now explicitly 1-D list operations.
Point one at a multi-dimensional array and you get a clear error instead of
an array whose shape quietly stops matching its contents.

POLYGON takes an array of points

POLYGON (and its UV clause) already accepted a MUTATION. Now it also
takes a plain array of points — coordinate values (x, y), objects with
.x / .y, or an N×2 numeric array — so you can build the geometry
however is convenient and hand it straight in.

A few new (and newly-documented) functions

  • COUNT(text$, part$) — how many times part$ appears in text$.
  • REVERSE$(text$) — the string, backwards.
  • MID$(text$, start) — the length is now optional; leave it off to get
    everything from start to the end.
  • ASIN / ACOS / LOG10 were always there; they're in the reference
    now.

Errors where there used to be silence

A few operations that used to fail quietly now stop with a clear message:

  • SQR of a negative number, and MOD by zero, halt — they used to produce
    an un-representable NaN.
  • COLLISION with an unknown shape name or too few arguments, and NEWIMAGE
    with a zero/negative size, halt instead of silently doing nothing.

Small behavior fixes

  • MUSICPLAYING actually reports playback now (on desktop it was stuck
    on "not playing"), and reads 0 while paused.
  • SPRITEDIRECTION returns an intuitive 0–360 (0 = right, 90 = down,
    180 = left, 270 = up).
  • MAPTILEX / MAPTILEY / MAPWORLDTILEX / MAPWORLDTILEY return -1
    when there's no map under the point, so tile 0 is no longer mistaken for
    "nothing there."
  • BBLINK 0 turns blinking off (and leaves the sprite showing) instead
    of flickering every frame.
  • FOR MAPDATA … LAYER n now actually scans layer n.
  • DATE$ reads as MM/DD/YYYY.

Compatibility notes

  • Everything you already wrote still works. FRAMERATE fps, the
    four-argument BBOX(x1,y1,x2,y2), plain PNG sprite sheets — all
    unchanged. The new words are strictly additive.
  • AUTOBBOX and OFSPRITE are new keywords inside ANIMATE. They
    only mean something in that position; variable and image names that
    merely contain those letters are unaffected.
  • TAG(…) is a new optional clause on LOADIMG. Strictly additive —
    existing LOADIMG calls are unchanged, and TAG only applies to .ase
    files (it's a loud error on a plain PNG).
  • Range values are fractional now. If you somehow depended on a range
    literal truncating decimals to integers, that no longer happens —
    whole-number ranges behave exactly as before, only fractional ones
    changed.
  • REDIM preserves now. If any code leaned on REDIM wiping an array
    back to zeros, use DIM (or ERASE) for that instead.
  • SPRITEDIRECTION range changed from −180…180 to 0…360 — "up" reads
    270 now (it was −90). Adjust any angle comparisons that assumed the
    old range.
  • MAPTILEX / MAPTILEY return -1 off-map (they used to return 0).
    If you tested for 0 to mean "no tile," test for -1.
  • DATE$ uses slashes (MM/DD/YYYY). Code that parses it expecting
    dashes needs a tweak.
  • A few silent results are now loud haltsSQR of a negative, MOD
    by zero, a malformed COLLISION, a non-positive NEWIMAGE. Anything that
    relied on the old quiet NaN / 0 / no-op will now stop with an error.

Draw it once. Let the file remember the timing and the hitbox. Go make a
game.

— Crash-tan