Crash-tan Crash-tan

1.62.0 is ready!

Crash BASIC 1.62.0 Release Notes

written by Crash-tan

SCREEN NATIVE GIVES YOU THE REAL SCREEN

SCREEN NATIVE is supposed to hand your game the screen it's actually shown
on, so you can read SCREENWIDTH and SCREENHEIGHT and lay out from there.
It didn't. On every platform it asked for a 0×0 screen, got one, and told your
program so. SCREENWIDTH kept whatever it held before, and nothing drew,
because every layer was built zero pixels wide.

Now the screen is the surface your game is shown on, one screen pixel per
device pixel, with no letterbox:

Where You get
Phone or tablet the safe area in device pixels — the display minus its notch, cutout and home indicator
Browser the canvas
Desktop the window: --window-size, the display with --fullscreen, or the usual 640×480 window (1280×960 on a 2x Retina display)
SCREEN NATIVE
PRINT "Screen size:"; SCREENWIDTH; "x"; SCREENHEIGHT

The size is decided when SCREEN NATIVE runs. Resize the window or rotate the
device afterwards and your screen keeps its size, letterboxed like any other
SCREEN.

YOUR GAME KEEPS CLEAR OF THE NOTCH

Phones have things in the way of the picture: a notch, a Dynamic Island, a
camera cutout, a home indicator, rounded corners. Until now your game was
scaled to fill the whole display, so on a phone shaped like your game, the
notch sat on top of it.

Your screen is now fitted inside the part of the display the phone says is
safe, and centred there. You don't have to do anything, and on desktop nothing
changes at all. Most games won't move on a phone either — a tall phone already
puts bars where the notch is.

AND YOU CAN DRAW IN THE BARS

Those bars were dead space. Add LETTERBOX OFF to any SCREEN and the whole
display becomes yours:

SCREEN 320, 200 LETTERBOX OFF

Your game keeps the size and position it had — this reveals, it doesn't move
anything. What you gain is everything around it, described by three new
readings:

REALWIDTH, REALHEIGHT the whole display, measured in your screen's own pixels
REALSTART its top-left corner, which is above and left of yours, so REALSTART.X and REALSTART.Y are 0 or negative

Coordinates out there are negative, and your layers still start at (0, 0). To
fill the extra space, make a layer the size of the display and scroll it into
place:

SCREEN 320, 200 LETTERBOX OFF
CREATE LAYER 0, REALWIDTH, REALHEIGHT
SET LAYER 0
SET SCROLL(-REALSTART.X, -REALSTART.Y)
CLS 4                      ' fills the glass, edge to edge

CREATE LAYER 1, SCREENWIDTH, SCREENHEIGHT
SET LAYER 1
PRINT "Gameplay stays in here"

Only a layer that covers the extra space paints it — the one above does.
Anything no layer covers stays black: CLS clears the layer you are drawing
to, not the display, and WRAPX / WRAPY tile across your screen rather than
the space around it. Sprites are the exception: one placed outside your screen
is drawn out there with no setup at all.

How much extra space there is depends on the device, and the player's finger
can reach it — a touch out there reads as a negative coordinate. So keep
gameplay and anything that must be seen or pressed inside your screen, and use
the bleed for scenery. Rotating the phone or resizing the window changes the
real area; read REALWIDTH again and rebuild what you sized from it.

Without LETTERBOX OFF nothing changes: the bars stay, and REALWIDTH /
REALHEIGHT simply report your screen.

FORGETTING YIELD NO LONGER KILLS YOUR GAME

A loop with no YIELD in it used to be fatal. You'd get me, apologetically,
telling you that you'd run 100,000 statements without yielding, and your game
would stop.

It doesn't any more. If a loop goes round long enough without handing over, the
runtime hands over for you, at the end of the loop — exactly where you'd have
written YIELD yourself:

DO
  IF MOUSEDOWN() THEN CALL Fire()
LOOP                      ' no YIELD, and that's fine now

Write YIELD anyway where you mean one. The automatic handover only arrives
once a loop is already overdue, and a loop that yields for itself never reaches
it. But forgetting one costs you responsiveness inside that loop instead of
costing you the whole program.

This applies to every loop — DO, WHILE, FOR, FOR EACH, FOR SPRITES,
FOR COLLISIONS, FOR MAPDATA.

One thing that follows: a loop that spins forever doing nothing now spins
forever instead of erroring.

WHILE 1
  x = x + 1               ' runs forever now; it used to stop
WEND

That's deliberate — WHILE 1 / YIELD / WEND is how you write a main loop, so an
endless loop was always allowed, and the one above differs only in doing nothing
useful. It stays responsive, and you can still quit. But if your game hangs
where it used to complain at you, look for a loop whose exit condition never
comes true.

YIELD IS QUICKER, AND SET YIELD MEANS SOMETHING NEW

YIELD used to sleep about 16 milliseconds and hope that was long enough for
the renderer. Now it waits for the renderer to actually take its turn and comes
straight back — so on a machine that draws quickly it's nearly free, and on a
slow one it waits exactly as long as it has to. An unpaced YIELD loop runs
roughly twice as often on a desktop as it used to, and it's the machine deciding
that, not a number I picked.

Which changes what SET YIELD means. It used to say how long YIELD slept; it
now sets a ceiling on how long YIELD will wait:

SET YIELD 1               ' still means "don't wait around"

Nothing gets slower — the ceiling only matters when the renderer isn't drawing
at all, like a phone mid-rotation or an app in the background, where it's what
stops YIELD waiting forever.

YIELD is a safeguard, not a speed control. It exists so the renderer,
audio, input and animations each get a turn. How fast your game runs is
COOLDOWN, CALL TIMER and SET BEHAVIOR RATE, and motion should be a rate
over time rather than a step per loop — then your game runs the same however
often the loop goes round.

BEGIN FRAME and END FRAME are not yield points, and never were. They batch
your drawing; they don't wait for anything. A loop built only from those is
still a loop that never yields.

PORTRAIT GAMES SURVIVE ON ANDROID

A portrait .crashcart opens from a landscape launcher and immediately turns,
so every one of them hit a rotation before drawing a single frame — and
vanished. No error, no dialog, nothing to go on, because the system was killing
the whole process.

Rotating now works: the game keeps running, keeps its orientation, and the
picture comes back the right way up.

NAMES THAT START WITH TRUE OR FALSE WORK

A variable called trueCount, falseStart or anything else beginning with
TRUE or FALSE was a syntax error the moment you used it in an expression:

trueCount = 3
x = trueCount + 1   ' "something unexpected at the end of this line"

Crash BASIC saw the word TRUE, stopped there, and choked on the Count left
over. TRUE and FALSE now only count as themselves when they're whole words,
so those names are ordinary variables again.

A BUTTON YOU LET GO OF STAYS LET GO OF

On phones and tablets, a press on the on-screen pad could stick — your game
kept reading a key as held down, and nothing you did let go of it.

It took three different shapes, and all three are fixed:

  • You were touching the screen as the splash handed over to your game. The
    press arrived in the game, the release never did.
  • You left a game with a button still under your thumb. That key stayed down in
    the library and in every game you opened afterwards, until you happened to
    press and release that exact button again.
  • On Android, the system took a gesture away mid-press — a notification pulled
    down, the app sent to the background — while more than one finger was on the
    pad. The first finger was let go of; the others weren't.

Nothing in your program changes. KEYDOWN and the rest just tell the truth
again.

A GAME THAT FAILS ON IPHONE TELLS YOU WHY

If your game stopped with an error on iOS, you got dropped back to the library
with no explanation. The error was found, worded and sent — and then shown to
nobody, because the window it was going to be shown in had already been taken
down when the game started.

Now the dialog turns up, with the message in it, the way it always did on
desktop. It's readable, too: the text used to arrive as garbled symbols on the
occasions it appeared at all.

ERASING A BIG AREA NO LONGER FREEZES YOUR COMPUTER

Drawing with a fully transparent colour erases: a LINE ... BF, CIRCLE or
POLYGON in -1 (or, under SET PALETTE OFF, any colour with alpha 0) cuts a
see-through hole in the layer. It was done one pixel at a time, and every pixel
cost the graphics card a trip of its own. Erase the whole screen once a frame
and the requests piled up faster than they could be served, until the game
froze and took the whole computer with it.

An erase is now one trip per row. A full-screen erase costs about what a
full-screen fill does, and nothing about what it erases has changed.

PSET AND PRESET SPEAK SET PALETTE OFF

Under SET PALETTE OFF every colour is a literal &HAARRGGBB — except, it
turned out, a PSET's. That read its colour as a palette number, so an opaque
colour punched a transparent hole instead of drawing:

SET PALETTE OFF
SCREEN 320, 200
CLS &HFF202020
PSET (100, 100), &HFFFF0000   ' was a hole; now red

Now PSET means what every other drawing command means in that mode, on the
screen and inside BEGIN MEMORYBUFFER alike. PRESET still erases, and a
PSET with no colour is still white.

WINDOWS USES ITS NATIVE RENDERER

CrashPlayer on Windows now draws with its native Direct3D 12 renderer by
default. There's nothing to change in your game. If a machine can't run
Direct3D 12 — an old graphics card, some virtual machines — CrashPlayer starts
on its cross-platform renderer instead, and you can ask for that one yourself
with --renderer wgpu.

Transparent erases work on the native renderer too; before this they did
nothing there.

ONE-WAY PLATFORMS, AND ONE-WAY EVERYTHING ELSE

Jump up through a ledge and land on top of it: that's a one-way platform, and
now it's one word. ONEWAY(face) makes an obstacle solid on a single face —
the one you name — and only against something moving into that face from
outside. From every other side, it isn't there.

BOUNDARY ADD "floor", (0, 470)-(640, 480) SOLID
BOUNDARY ADD "ledge", (160, 320)-(400, 328) SOLID ONEWAY(TOP)

Fall onto the ledge and you land on it. Jump up into it from below and you sail
through, then land on top on the way down. Walk into its end and you walk
straight through.

You always name the face, because floors aren't the only thing that wants to be
one-way. ONEWAY(LEFT) is a gate you can walk through going left but not going
right; ONEWAY(BOTTOM) is a ceiling you can drop down through.

It works on the other things that can get in a sprite's way, too — a sprite,
a polygon, a particle:

SET SPRITE plank, 200, 300, "plank", 0 TAG "platform" ONEWAY(TOP)
SET SPRITE plank ONEWAY OFF        ' solid all round again

Landing is a collision like any other, so ONCOLLISION fires with edge$
set to the face. Passing through counts too, because the two really do
overlap — edge$ says which side it came in by. If your handler should only
react to landings, check edge$ = "TOP".

A CENTRED SPRITE STOPS WHERE IT'S DRAWN

A sprite that uses ORIGIN(0.5, 0.5) to be positioned by its centre, or a
BBOX that doesn't start at its corner, collided with solid sprites from the
wrong place. The engine found the overlap using the right box and then pushed
the sprite out as if that box sat on the sprite's anchor point instead. So a
centred sprite could stop short of a wall or sink partway into one, depending
on which way it was going.

It now stops flush against what it hit, from where it's actually drawn.
Collisions with BOUNDARY walls always worked this way; sprites now match.

SET A SPRITE MOVING — OR STOP IT DEAD

VELOCITY(vx, vy) sets how far a sprite moves on its next tick, outright:

SET SPRITE player VELOCITY(SPRITEVELOCITYX(player), -12)   ' jump
SET SPRITE player VELOCITY(0, 0)                           ' stop dead

It replaces whatever speed the sprite's behaviors have built up, and they
carry on from it: gravity starts pulling the tick after, friction slows it. So
a platformer's jump is one line, and VELOCITY(0, 0) clears the slate before a
new behavior takes over. It works on SET POLYGON too, and on EMITPARTICLE,
where it's the particle's starting speed.

FLIPX OFF

FLIPX and FLIPY take ON or OFF, so you can finally flip a sprite back:

SET SPRITE hero FLIPX ON      ' facing left
SET SPRITE hero FLIPX OFF     ' facing right again

A bare FLIPX still means ON. And each axis is its own setting now — before
this, SET SPRITE n FLIPX quietly un-flipped a sprite you had flipped upside
down, and FLIPY did the same to a mirrored one.

ANIMATIONS THAT PLAY ONCE

End an ANIMATE with NOLOOP and it plays its frames once and holds the last
one, instead of starting over:

BEHAVIOR "flip"
    ANIMATE "player", 7 TO 15 FRAMERATE 10 NOLOOP
END BEHAVIOR

It then counts as finished, so a PATTERN step waiting on it moves on.

PATTERN STEPS RUN IN ORDER — WHEN STEPS TOO

A WHEN step in a PATTERN used to be live from the pattern's first tick,
wherever it was written. It now waits its turn like every other step.

A run of WHEN steps next to each other is a group. When the pattern reaches
the group, every WHEN in it goes live at once, and the pattern stays there
until one of them leaves with GOTOPATTERN or DISABLE. Run the steps that
lead up to a decision, then let the group decide:

PATTERN "guard"
    "walkLeft" DURATION 60
    "walkRight" DURATION 60
    GOTOPATTERN "chase" WHEN playerNearby
    GOTOPATTERN "guard" WHEN NOT playerNearby
END PATTERN

Check your patterns for this shape. A behavior written as a plain step,
ahead of WHEN steps, now has to finish before they start:

PATTERN "player"
    "idle"                          ' runs until "idle" finishes...
    "walkRight" WHEN KEYDOWN("RIGHT")
END PATTERN

If "idle" never finishes (and a behavior that only moves or animates
doesn't), the arrow key never does anything. To keep a behavior running
alongside the others, make it part of the group with WHEN TRUE:

PATTERN "player"
    "idle" WHEN TRUE
    "walkRight" WHEN KEYDOWN("RIGHT")
END PATTERN

A CHILD SPRITE COLLIDES WHERE YOU SEE IT

A sprite attached with PARENT is drawn at its parent's position plus its
offset, but collision measured it at the bare offset, as if the parent stood
in the top-left corner. Two children at the same offset under parents on
opposite sides of the screen "touched", while a child that was really
touching something wasn't reported at all.

Every collision test now puts a child where it is in the world:
SPRITECOLLISION, ONCOLLISION, FOR COLLISIONS, SPRITETOUCHING$, and
whatever a SOLID sprite runs into.

SET SPRITE 1 PARENT(0)                  ' a sword in the hero's hand
IF SPRITECOLLISION(1, "enemy") THEN     ' now asks about the sword where it is
    hits = hits + 1
END IF

HIDDEN SPRITES STILL COLLIDE

A hidden sprite was left out of collision detection unless it was SOLID, so a
GHOST trigger zone you'd hidden never reported anything. Hiding a sprite
changes how it's drawn, not whether it's there: it's now seen by
SPRITECOLLISION, ONCOLLISION and FOR COLLISIONS like any other.

It still only blocks something that's SOLID against its tag with
SOLID SPRITES(...), same as a visible sprite.

A RESERVED SPRITE IS JUST A NUMBER UNTIL YOU USE IT

RESERVE SPRITE used to create an invisible stand-in sprite at the top-left
corner to hold the slot. Now it only holds the number: nothing is drawn or
collides until you create the sprite with ENABLE SPRITE or
SET SPRITE n, x, y, image$.

RESERVE SPRITE id
SET SPRITE id, 100, 100, "bullet" TAG "bullet"   ' the sprite starts here
SET SPRITE id META "damage", 5

Check your code for this shape: changing a reserved sprite before
creating it, such as SET SPRITE id META … straight after RESERVE SPRITE id,
is now the same "sprite is not enabled" error as any other sprite that doesn't
exist yet. Create it first.