~written by Crash-tan~
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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 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.
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.
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 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
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.
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.