Crash-tan Crash-tan

2.0.1 Fix Pack Released!

Crash BASIC 2.0.1 Release Notes

written by Crash-tan

A small one! Slopes stopped throwing your sprites around, ramps learned to be
one-way, sprite positions read back exactly, child sprites stay with their
parents when you change them, sound effects no longer stop your music, scrolling goes smoothly between
pixels and no longer jitters against your sprites, layer shaders can be turned every frame, Crash BASIC 2 learned TYPEOF, games exported for the web finally
get the whole browser window, music gets ready about four times faster, and
PUTIMAGE can stamp a whole grid of tiles in Crash BASIC 2.

SLOPES KEEP YOUR SPRITES ON THEM

A SLOPE boundary is a triangle: one half of its box is solid ground, cut
along the diagonal, and the other half is open air. A sprite walking up a ramp
is supposed to be inside that box. It's standing on the diagonal, with its
feet in the open half.

I already settled every sprite against the diagonal properly. But after all
the boundaries had their turn, I made one last check: "is this sprite still
overlapping anything solid?" That check didn't know about slopes. It only saw
a solid box with a sprite inside it, and shoved the sprite out the nearest
side. So a sprite walking happily along a ramp could suddenly pop off it, to
the top of the box or out the side. Sometimes it got thrown a long way, right
across the map.

Now that last check leaves slopes alone, because they were already settled
against their diagonal. Sprites walk up and down ramps and stay on them.
Ordinary solid boundaries still get the last check exactly as before, so
corners stay just as hard to slip through.

JUMP-THROUGH RAMPS

ONEWAY and SLOPE used to refuse to go together. I said a slope's surface
was its diagonal, so it had no top, bottom, left or right face to name. That
was true, and it was also a bit unhelpful, because a ramp you can jump up
through is exactly what a platformer wants!

So now they combine. A one-way slope is solid along its diagonal, but only
from its empty side. Land on it from above and it holds you. Walk along it and
it carries you up and down. Jump at it from underneath and you sail straight
through, then land on top once you've cleared it. A sprite that starts out
inside the solid half isn't shoved out either. Only crossing the diagonal from
the empty side counts.

BOUNDARY ADD "ramp", (200, 150)-(264, 214) SOLID SLOPE SE ONEWAY(TOP)
BOUNDARY ADD "ramp", (200, 150) -> (264, 214) !! solid slope("se") one_way("top")

The face you name has to point out of the solid triangle, away from the solid
corner:

Face Slopes
TOP SE, SW (ramps you stand on)
BOTTOM NE, NW (ceilings)
LEFT NE, SE
RIGHT NW, SW

Name a face that points into the solid part and I'll stop and tell you which
ones fit, rather than guess what you meant.

Only catching what lands on it

A one-way ramp catches anything that crosses into its diagonal, including a
sprite running along the floor into its rising end. Usually that's what you
want: the player runs up the ramp. Sometimes it isn't, like a sloped platform
floating over a floor, where the player should run under it.

In Crash BASIC 2, add face_entry and the ramp only catches sprites coming at
it along the face you named. With TOP, that means falling onto it from
above, or stepping onto it from its upper end:

BOUNDARY ADD "platform", (200, 150) -> (264, 214) !! solid slope("se") one_way("top") face_entry

Running along the floor into it now passes underneath. Once a sprite is
standing on the ramp, it can walk up and down it either way, just like before.
face_entry is off unless you ask for it, and it only goes with solid,
slope and one_way together. In Python it's face_entry=True on
crash.boundary_add. It works for BOTTOM, LEFT and RIGHT faces too: I
don't assume which way gravity points.

SPRITEX AND SPRITEY TELL YOU EXACTLY WHERE

SPRITEX and SPRITEY used to round to the nearest whole pixel. But a sprite
falling under gravity, bouncing off an IMPULSE or walking up a slope spends
most of its time between pixels, and I draw it there. So a camera following
the rounded number and a sprite drawn at the real one disagreed by up to half
a pixel, and the difference changed every tick. Zoom in and that's a wobble you
can see: the player shivering in the middle of the screen while it jumps.

Now they tell you exactly where the sprite is, fractions and all. A sprite
sitting right on a pixel still reads as a whole number, like 200. One between
pixels reads as a decimal, like 100.5. Cameras that follow a sprite glide with it.

If your game wants whole pixels, round it yourself:

px = ROUND(SPRITEX(0))
px = round(spritex(0))

Watch for these places where the old whole number mattered:

  • Printing a position shows the fraction when there is one, like 100.5.
  • Comparing with =, like IF SPRITEX(0) = 160, only matches when the
    sprite is exactly there.
  • Using it as a whole number, like an array index or a tile number, drops
    the fraction instead of rounding. So 100.5 becomes 100 where it used to be
    1. Wrap it in ROUND if you counted on rounding.
  • Putting a sprite back where it was, like LOCATION(SPRITEX(n), SPRITEY(n))
    with a zero origin, now really leaves it where it was, instead of nudging it
    onto the nearest pixel.

CHILD SPRITES STAY WITH THEIR PARENTS

A sprite with a PARENT keeps its position relative to that parent. When
you changed something else about it, like its image, its tag, its alpha or its
rotation, I sent it to the screen again using that relative position as if it
were a place on the screen. So the child flashed up near the top-left corner
for a moment. Giving it just an X or just a Y was worse: I took the parent
away twice and put the child in the wrong place.

Taking a behavior off a child did the same thing, and that one is a very old
bug. When a behavior that blinks the sprite or runs a shader on it comes off,
I send the sprite straight to the screen so it's visible again and the shader
is gone. And I sent it with its relative position too. That happens with
remove_behavior, with clear_behaviors, and every time a PATTERN moves
from one step to the next. So a blinking arm on a boss could jump to the corner
for a frame each time its pattern changed phase.

Now every change starts from where the child really is in the world. Changing
its look, or taking a behavior off it, leaves it exactly where it was, and
setting one coordinate only moves it along that one.

SOUND EFFECTS DON'T STOP THE MUSIC

In 2.0.0 I started letting a sound play over itself: PLAYWAV "explosion"
twice gives you two explosions, not one that starts again. But in the desktop
and mobile players I can only play 32 sounds at once. When they were all
taken, I made room by stopping the sound that had been playing longest. That's
nearly always your music, because it started first.

So a game that played the same sound every time round a loop, like an
explosion while a laser is touching an alien, could fill all 32 in a moment,
and the music just stopped.

Now when I need room, I stop a sound that would have ended on its own. The
oldest copy of the sound you're playing goes first, so a sound played over and
over only crowds out itself. A sound that loops, like your music, is never
stopped to make room. If every one of the 32 is looping, the new sound doesn't
play, and I write why in the player's log.

SCROLLING BETWEEN PIXELS

SET SCROLL used to round your camera to a whole pixel. Your sprites didn't
get rounded, though. So when a camera eased along after a sprite, a little
less than a pixel at a time, the background moved in steps while the sprites
glided. Everything shimmered against everything else.

Now I keep the fraction. SET SCROLL 10.25, -4.5 scrolls by exactly that,
and GETSCROLL gives it back. Layers, tilemaps, sprites and polygons all
move by the same offset, on wrapped layers and plane layers too, so they stay
together:

hero = 0        // your player's sprite
cam_x = 0
DO
    IF COOLDOWN(1)
        // A tenth of the way to the hero each tick: rarely a whole pixel.
        cam_x = cam_x + (spritex(hero) - 160 - cam_x) * 0.1
        SET SCROLL cam_x, 0
    END IF
LOOP

If you want your scrolling to snap to whole pixels, for that crisp retro
look, round it yourself: SET SCROLL FLOOR(cam_x), FLOOR(cam_y). And if a
shader of yours turns screen positions into world ones, give it the same
camera numbers you give SET SCROLL.

One more thing that made a moving camera wobble: in a one-player game, when a
tick ran slow I could start smoothing your sprites' movement while the camera
had already jumped ahead, so they lagged behind the background for a moment.
Now a one-player game's sprites go exactly where each tick puts them, in step
with the camera. In multiplayer, other players' sprites are still smoothed
over the network, just as before.

A TICK REACHES THE SCREEN IN ONE PIECE

Every tick, I move your sprites, then run your CALL RENDER handlers, which
is where a camera usually follows them with SET SCROLL. But the screen gets
drawn on its own clock, and now and then it was drawn in between: the sprites
had moved and the camera hadn't caught up yet. For that one frame your player
jumped a tick's worth of travel against the background, then snapped back. A
scrolling level looked like it was jittering.

FRAME ... END FRAME didn't help, because it only held back drawing. A
sprite's position or a layer's scroll still showed up the moment I got it.

Now everything a tick does, from the sprites moving to the last SET SCROLL
in your render handlers, reaches the screen together, and so does everything
inside a FRAME. You don't have to change anything. It works the same on
desktop, phones, TVs and in the web player.

A frame you hold open for a long time, say across a wait, still gets shown
as it stands a few times a second, so your screen never freezes on one.

TURNING A LAYER SHADER'S NUMBERS, EVERY FRAME

A layer shader's @parameters could only be set together with the shader:
shader("weather") shader_params({...}). Each time, I swapped the whole
shader in again, bound its images again and waited for the renderer to say
it was done. That's fine once, but far too slow for a number that changes
every frame, like a fog that follows the camera. And a wait can't go inside
FRAME.

In Crash BASIC 2, shader_params can now go on its own. Attach the shader
once, then hand me just the numbers that changed:

SET LAYER 0 !! shader("weather") shader_params({camera_y: 0})
FRAME
    SET SCROLL 0, 24
    SET LAYER 0 !! shader_params({camera_y: 24})
END FRAME

I change only the numbers you give me. The shader, its images, its other
parameters and its clock all stay as they were. Nothing waits, so it goes
inside FRAME right beside your scrolling and lands on the same frame. In
Python it's crash.set_layer(0, shader_params={"camera_y": 24}).

Two things it won't do:

  • Bind an image. Give me an image name with shader_params on its own and
    I'll stop and tell you to use the full shader(...) shader_params(...) form
    for that, outside FRAME.
  • Attach a shader. The layer needs to have one already.

TYPEOF

Crash BASIC 2 has a new function, TYPEOF. It tells you what type a value is,
as the name you'd write after AS: "INTEGER", "FLOAT", "STRING",
"ARRAY", "MAP", and so on. A TYPE of yours gives its own name, like
"Monster", and NULL gives "NULL".

loot = ["sword"]
IF TYPEOF(loot) = "ARRAY" THEN PUSH loot, "gold"
PRINT loot

You can ask it about errors and futures too, without stopping the script.

WEB GAMES FILL THE WINDOW

A game you export for the web (the Web (itch.io) box on the Package tab)
used to sit in a fixed 4:3 box inside the browser window. A widescreen game
got letterboxed twice: once by the box, and again inside it. LETTERBOX OFF
couldn't reach the edges of the window either, because the box got in the way.

Now the game's canvas covers the whole browser window, and I fit your game
into it, the same way I do on desktop and on phones. That means:

  • One letterbox, sized to the real window. Bars only appear where your
    game's shape and the window's shape disagree.
  • LETTERBOX OFF works in the browser. The bleed area runs right out to
    the window's edges, and REALWIDTH, REALHEIGHT and REALSTART measure the
    whole window.
  • Phones keep clear of the notch from the very first frame. On a phone's
    browser, I used to only learn about the notch, cutout and home indicator
    after the window had been resized. Now I check as soon as the game starts,
    and again whenever the phone rotates or the window changes size, so your
    game stays clear of them in both orientations.

To get this in a game you've already exported, export it again with 2.0.1.

MUSIC GETS READY FASTER

Before a song can play, I play it through my synthesizer once and keep what
comes out. That's what LOADMUSIC is waiting for, and what PLAYMUSIC waits
for when a song wasn't loaded first. For a long song with lots of tracks, it
could take a while.

Now it takes about a quarter of the time. A bank with three songs and three
and a half minutes of music that used to keep you waiting for over eighteen
seconds is ready in under five. Two things were slowing me down:

  • Every song was played through twice. I keep each song two ways, looping
    and not, because PLAYMUSIC !! loop picks between them when it plays. I made
    each one separately, so every song went through the synthesizer twice in a
    row, for the same notes. Now one pass gives me both.
  • I was redoing sums I already knew. Every note on every track worked out
    its instrument's octave, detune and pan again for every single sample, even
    though none of them change while a note plays. Now I work them out once, when
    the note starts.

The music sounds exactly the same, down to the last sample. It's only ready
sooner. That's true in the web player too.

PUTIMAGE STAMPS A GRID

In Crash BASIC 2, PUTIMAGE !! map draws a whole grid of tiles in one go,
picking a frame of a spritesheet for each cell. But it refused the grid it
should have taken, (rows, columns):INTEGER, and said it wanted a
two-dimensional array.

Now it takes one, the same grid TILES !! from reads, one row at a time:
level(row, column) is the frame for that cell, counting from 1, and a 0 leaves
the cell empty. map_area picks which cells to draw.

SCREEN 320, 240
NEWIMAGE 32, 16, 4, "tiles" !! spritesheet((16, 16))
level = (2, 3):INTEGER
level(0, 0) = 1
level(1, 2) = 2
PUTIMAGE (0, 0), "tiles" !! map(level) map_area((0, 0) -> (2, 1))

Python, JavaScript and Lua take a grid for map too. In Crash BASIC 1.x,
MAP(array, x1, y1, x2, y2) works just as it always has.