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 BEHAVIORFRAMERATE 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), andAUTOBBOX 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 BEHAVIOREvery 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 hitboxBare 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 OFSPRITEA 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 bSPRITEBBOX(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 hitboxEach 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 BEHAVIOROnly 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 needAS 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 valueBoth 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 timespart$appears intext$.REVERSE$(text$)— the string, backwards.MID$(text$, start)— the length is now optional; leave it off to get
everything fromstartto the end.ASIN/ACOS/LOG10were 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:
SQRof a negative number, andMODby zero, halt — they used to produce
an un-representableNaN.COLLISIONwith an unknown shape name or too few arguments, andNEWIMAGE
with a zero/negative size, halt instead of silently doing nothing.
Small behavior fixes
MUSICPLAYINGactually reports playback now (on desktop it was stuck
on "not playing"), and reads0while paused.SPRITEDIRECTIONreturns an intuitive0–360(0 = right, 90 = down,
180 = left, 270 = up).MAPTILEX/MAPTILEY/MAPWORLDTILEX/MAPWORLDTILEYreturn-1
when there's no map under the point, so tile0is no longer mistaken for
"nothing there."BBLINK 0turns blinking off (and leaves the sprite showing) instead
of flickering every frame.FOR MAPDATA … LAYER nnow actually scans layern.DATE$reads asMM/DD/YYYY.
Compatibility notes
- Everything you already wrote still works.
FRAMERATE fps, the
four-argumentBBOX(x1,y1,x2,y2), plain PNG sprite sheets — all
unchanged. The new words are strictly additive. AUTOBBOXandOFSPRITEare new keywords insideANIMATE. They
only mean something in that position; variable and image names that
merely contain those letters are unaffected.TAG(…)is a new optional clause onLOADIMG. Strictly additive —
existingLOADIMGcalls are unchanged, andTAGonly 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. REDIMpreserves now. If any code leaned onREDIMwiping an array
back to zeros, useDIM(orERASE) for that instead.SPRITEDIRECTIONrange changed from −180…180 to 0…360 — "up" reads270now (it was−90). Adjust any angle comparisons that assumed the
old range.MAPTILEX/MAPTILEYreturn-1off-map (they used to return0).
If you tested for0to 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 halts —
SQRof a negative,MOD
by zero, a malformedCOLLISION, a non-positiveNEWIMAGE. Anything that
relied on the old quietNaN/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