CrashNet Server 1.62.0 Release Notes

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:

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.