Crash-tan
Polygons, but now even more sprite like!
Crash BASIC 1.55.0 Release Notes
written by Crash-tan
POLYGONS CAN BE THROWN NOW
A MUTATION is a shape that moves as a unit, and you give it motion the same way you give a sprite motion — with a behavior:
DIM ship AS MUTATION
ship = MUTATION((0, -12), (-8, 8), (0, 4), (8, 8))
SET POLYGON 0 BIND ship COLOR 1 FILL 2
BEHAVIOR "drift"
BIMPULSE 4, -2 ' one good shove
BFRICTION 0.02, 0.02
END BEHAVIOR
SET MUTATION ship BEHAVIOR "drift"Until now that shove did nothing. Neither did BVELOCITY, BACCELERATION, BFRICTION or BBOUNCE. No error, no warning — the polygon simply sat there while a behavior that works perfectly well on a sprite quietly moved it not at all.
What made it maddening is that half the vocabulary did work. BMOVE, BOSCILLATE, PATH and BORBIT were fine. So a mutation would happily bob and follow a path, and then refuse to be thrown, and nothing about the code explained why one worked and the other did not.
The reason is that behaviors describe motion in two different ways. Some write a position: BMOVE says "you are two pixels further left now". Others write a velocity: BIMPULSE says "you are now travelling at this speed", and the engine turns speed into position afterwards, every tick, which is what gives you momentum, coasting, and friction that bleeds it away. Sprites got that second step. Mutations were handed a fresh, blank sprite every tick, built up their velocity in it, and then had it thrown away before anything was integrated. All the arithmetic ran; none of it landed.
Each point of a mutation now keeps its own state between ticks, so velocity survives long enough to become movement — and a mutation moves exactly as far as a sprite would under the same behavior. That is not a turn of phrase: the tests run each behavior on both and compare the positions.
Nothing changes for BMOVE, BOSCILLATE, PATH or BORBIT, which were never the problem.
AND SPUN, AND SCALED
The same hole swallowed the transforms. BROTATE, BSCALE, BFLIPX and BFLIPY did nothing to a mutation either:
BEHAVIOR "tumble"
BVELOCITY -1.5, 0.4
BROTATE 0 TO 360 DURATION 180 LOOP
END BEHAVIOR
SET MUTATION rock BEHAVIOR "tumble"A sprite carries rotation as a number the renderer applies to its quad on the way to the screen. A mutation has no quad — the shape is its coordinates — so there was nowhere for that number to go, and it went nowhere.
Those transforms are now baked into the points. A shape rotates, scales and mirrors as a unit, and it composes with movement: the rock above drifts left and tumbles at the same time, and neither cancels the other. It also stays rigid while it does — a rotating square is still a square, with the edge lengths to prove it in the tests.
Each pose is computed from the shape as you authored it, not from the shape as it was last posed. That is what keeps a shape from dissolving: scale out and back and the coordinates are the ones you wrote, to the last decimal, however many times it has happened, and a shape turned one degree at a time comes home after 360 of them. Write new points yourself and that becomes the new baseline.
Rotation happens about the shape's middle by default, matching where a sprite rotates. BROTATE ... ORIGIN(u, v) moves that origin, in the same relative 0..1 terms a sprite uses, measured across the shape's own extent — so ORIGIN(0, 0) pins the top-left corner and swings everything else around it. Scale and flip use that origin too.
A POOL OF SHAPES, NOT ONE HERO SHAPE
Bound polygons could only ever be one shape each. SET POLYGON n BIND took a bare variable name, so a game with twenty asteroids needed twenty literally-named MUTATION variables and a SELECT CASE in every spawn and despawn path to pick between them. That is why every polygon demo shipped so far binds exactly one shape: the feature worked beautifully for a hero object and fell over at game scale.
An array slot is now a mutation reference:
DIM rocks(24) AS MUTATION
FOR i = 0 TO 23
rocks(i) = MUTATION((0,0), (20,4), (24,20), (6,24))
SET POLYGON 10 + i BIND rocks(i) COLOR 5 FILL 6
SET MUTATION rocks(i) BEHAVIOR "drift", @vx = i - 12, @vy = 1, @spin = 180 + i * 20
NEXT iSET POLYGON ... BIND, SET MUTATION ... BEHAVIOR, SET MUTATIONPOINT and UV all take a slot, and the index is an expression, so rocks(i) inside a loop is the whole point. Bare names work exactly as before.
The second half of that example matters as much as the first. @param overrides now work on a mutation behavior. They were being parsed and then discarded, so before today a pool would have been twenty-four rocks drifting in perfect lockstep — a chorus line, not an asteroid field. They resolve the way SET SPRITE ... BEHAVIOR resolves them, which means one definition covers every rock at every speed and spin.
A SHAPE CAN BE BUILT FROM COORDINATES
A mutation's vertices had to be written as literal (x, y) pairs. Anything else — a coordinate variable, or arithmetic on one — was not a vertex:
' Now valid. Previously none of these were.
shape = MUTATION(at, at + tip, at - tip)The failure was worse than a refusal. An expression that did not match the (x, y) shape made the whole MUTATION(...) fall through and parse as an ordinary function call, which is perfectly good syntax — so the program validated cleanly and then died on the first run with "I can't find a function called MUTATION". A vertex you wrote in the wrong shape was reported as a function that does not exist.
Any expression yielding a coordinate is now a vertex, literal pairs included, and they mix freely in one call. Which means a shape can be built around a spawn point instead of around the origin and then moved there.
SHAPES CAN COLLIDE NOW
A mutation-bound polygon was geometry and nothing else. It could move, spin and deform beautifully and then sail straight through everything, because collision belongs to sprites and a polygon is not one. Games worked around it by measuring distances by hand.
SOLID gives a polygon a collider:
SET POLYGON 10 + i BIND rocks(i) COLOR 5 FILL 6 SOLID TAG "rock"That is the whole change at the call site, and everything else follows: SOLID resolution against boundaries, ONCOLLISION / AFTERCOLLISION, FOR COLLISIONS, tags, rooms, layers. Pushing against sprites is opt-in by tag, the same way it is for a sprite — SOLID SPRITES("ship", "bullet") names what the shape treats as an obstacle, and without that list the two pass through each other while still reporting the hit. None of those learned what a polygon is — the collider is an ordinary sprite living at the polygon's own reserved ID, invisible, its rect resynced from the shape's live extent every tick. GHOST takes it away again.
The collider is a circle by default, inscribed in the extent. That is not a shortcut, it is the right shape for the job: vector silhouettes are round-ish, a circle costs one distance check, and — unlike a box — it does not breathe. A spinning rock's bounding box swells by up to 41% and shrinks back every revolution, so a box collider on a tumbling shape is a hitbox that pulses, and being killed by empty space is a hard bug to explain.
SOLID BOX opts into the axis-aligned box a sprite uses. SOLID POLY tests the outline itself — every edge, concave shapes included.
That is the one to reach for when a shape is neither round nor rectangular, and an asteroid is the perfect example. A nine-sided rock's silhouette runs from 25 to 36 pixels out; a circle inscribed in its extent covers 29 in every direction, so it reports nothing where the rock bulges past it — a shot visibly on the rock, counted as a miss — while covering nine pixels of empty space where the rock is thin. The box has the opposite problem at the corners. Only the outline is the shape.
Collision now happens in the same frame as the drawing. A frame moved its shapes after it had already tested them, so every collider was compared at the position it held a tick earlier while the player was shown the new one. The gap is one tick of travel — a few pixels on a drifting rock, eleven on a bullet — and it reads as hits landing early, or not at all. Shapes move first now. This one is not specific to polygons; anything whose collider moves was affected.
TAG names the collider for queries and handlers. BBOX takes the same relative 0-1 quad or Range a sprite takes, but measured against the shape's current extent and resolved every tick, since that extent moves.
YOU CAN'T HAVE IT BOTH WAYS
Assigning to a constant has always been a friendly error. Going the other way — turning a name you have already been using as a variable into a constant — quietly succeeded:
score = 10
CONST score = 5 ' now a loud errorThat silent promotion was the worst of both worlds. Every assignment before it became retroactively meaningless, and every assignment after it failed with "cannot assign to constant" pointing at a line that looks perfectly reasonable — with the actual culprit sitting somewhere further up, looking equally reasonable.
It now says so at the CONST, where the mistake is:
Cannot make 'SCORE' a constant — it is already a variable
Crash-tan's Tip: You can't have it both ways! A name is either a variable you can change or a constant you can't. Pick a different name for the constant, or drop the CONST and keep assigning to it.
BEGIN ENUM members are constants too, so they follow the same rule — an enum member landing on a name already in use is caught the same way.
BUILDING A SHAPE POINT BY POINT WORKS
SET MUTATIONPOINT is documented to extend a mutation when you write past its end, padding with (0, 0). It never did. The write was simply skipped:
shape = MUTATION(at) ' one point to start
FOR v = 0 TO 8
SET MUTATIONPOINT shape, v, at + (COS(v * 0.7) * r, SIN(v * 0.7) * r)
NEXT vEight of those nine writes went nowhere, leaving a one-point mutation — and a one-point polygon draws nothing at all. No error, no warning, just an invisible shape and no obvious reason why, which is a miserable thing to debug when the code matches the documentation exactly.
It now extends as described, so a silhouette can be built with a loop instead of a wall of literal coordinates.
SHAPES CAN BE THROWN BY THE HUNDRED
EMITPARTICLE has always taken an image name. Give it a MUTATION instead and the particle is a shape:
DIM shard AS MUTATION
shard = MUTATION((0, -6), (5, 4), (-5, 4))
EMITPARTICLE (x, y), shard, "debris", "tumble", 90, @spin = 240 COLOR 5 FILL 6 SOLIDThat is a vector explosion in one line — a copy of the shape, centred where you asked, tumbling under a behavior, colliding, and cleaning itself up ninety ticks later. A game that wanted this before had to keep a pool of bound polygons, hand-write the lifetimes, and put the shape back when it was done.
Underneath it is an ordinary particle. Same ID band, same allocator, same expiry sweep, same tag — with the shape bound to that slot. A polygon's collider is a sprite already, so a polygon particle needed no second particle system, no reserved range, and no lifetime bookkeeping of its own.
The modifiers come from SET POLYGON itself, not a copy of it: COLOR, FILL, TEXTURE, UV, SHADER, LAYER, ZORDER, SOLID [CIRCLE|BOX|POLY] [SPRITES(…)], BBOX. Anything added to SET POLYGON later shows up here too, because both statements read the same rule.
Two things to know. The fourth argument names a behavior for a shape, where it names a pattern for a sprite. And a polygon modifier on a particle emitted from an image is a loud error rather than a shrug — COLOR on a sprite particle means you meant to emit a shape, and finding that out immediately beats wondering why the colour did nothing.
While this was going in, SOLID SPRITES("rock") on an ordinary sprite particle turned out to be parsed and then dropped: a particle told what to collide with sailed through it. That works now too.
A SHAPE IS A SPRITE, AND YOU CAN SAY SO
A bound polygon has always occupied a sprite slot — that is how it collides at all. What was missing was any way to reach it, and any promise about what would happen if you did.
Now the slot answers to SET SPRITE. Move it and the shape moves; scale, rotate or flip it and the shape is posed; change its alpha, layer, z-order or visibility and it behaves like anything else on screen. DISABLE SPRITE destroys it outright — geometry, binding and collider together.
The place you will actually use this is a collision handler, where index_a is that slot:
BOUNDARY ADD "edge", (-90, -90)-(730, 570) SOLID ENCLOSURE
BEHAVIOR "wrapAtEdge"
ONCOLLISION
IF tag_b$ = "edge" THEN
IF edge$ = "LEFT" THEN SET SPRITE index_a LOCATION(SCREEN_W, SPRITEY(index_a))
END IF
END ONCOLLISION
END BEHAVIOR
BEHAVIOR "rockDrift" INCLUDES("wrapAtEdge")
BVELOCITY @vx = 0, @vy = 0
BROTATE @spin = 360 DURATION 240 LOOP
END BEHAVIORThat is screen wrapping for every rock in the game, written once. It used to be two loops rewriting every vertex of every shape by hand each frame — and rewriting points underneath a behaviour that is mid-spin re-authors the shape and fights it, which is a bug you get to find yourself.
And a shape's handlers fire. ONCOLLISION inside a behaviour applied with SET MUTATION ... BEHAVIOR used to be dead on a SET POLYGON shape: the behaviour reached the shape and never the sprite that does the colliding, so the handler simply never ran. It runs now, which is what lets a rock react to being hit rather than a loop somewhere else noticing on its behalf.
A handler may destroy its own sprite. DISABLE SPRITE index_a is what a bullet does when it strikes something. If that sprite had other handlers still queued — two hits in one tick, or a behaviour composed with INCLUDES — they are skipped rather than run against a sprite that is gone.
A SCENE PUTS SHAPES BACK
BEGIN SCENE promises the world comes back exactly as you left it. Sprites always did. Bound polygons did not — a shape shoved around inside a scene stayed shoved, and a polygon created in one was worse: END SCENE took its collider away and left the drawing on screen, so you were looking at a rock you could fly straight through.
Both are fixed. A polygon that existed before the scene comes home — the drawn shape, the MUTATION behind it and its SOLID collider all together, so it is never visible in one place and solid in another. A polygon created inside the scene leaves with the scene, collider and all.
The one thing a scene still will not roll back is a MUTATION you never bound to a polygon. That is an ordinary variable, and scenes restore the world, not your variables. Bind it and its geometry becomes part of the world; leave it unbound and it keeps whatever the scene put in it.
SHAPES FADE AND BLINK NOW TOO
BALPHA and BBLINK used to do nothing to a mutation, and were listed as a gap to close. They work — a shape has a sprite, so it fades and blinks like anything else on screen, and a behaviour that dims a rock as it is destroyed needs nothing special.
ANIMATE and BIMAGE are the two that genuinely cannot apply, and that is a decision rather than a gap: both select frames of an image, and a shape has no image. Its look comes from SET POLYGON's COLOR, FILL and TEXTURE.
WHAT THIS OPENS UP
Vector games get to stop doing physics by hand. A ship whose thrust is BIMPULSE, whose drift is BFRICTION, and whose asteroids are BVELOCITY is now a handful of behavior blocks rather than a page of x = x + vx — and because a bound polygon is re-composited on the GPU from its mutation each frame, the shape is not re-rasterised into a layer as it moves.
Points still move as a unit, so a shape stays rigid while it travels.