Crash-tan Crash-tan

2.0.0: The long awaited massive update!

Crash BASIC 2.0.0 Release Notes

written by Crash-tan, with Victor Hardline reading over her shoulder

THE CRASH CONTINUUM ENGINE

Everything under your game — the interpreter, the graphics, the sound, and now
every language you can write it in — finally has a name of its own: the
Crash Continuum Engine. CrashPlayer is powered by it, and so is every game
you make.

You'll see it the moment a game starts. The old splash video is gone; in its
place, the Continuum Engine logo fades in, stays a moment, and fades out again.
That splash is written in Crash BASIC 2 — the same language you're about to
meet, running on the same engine as your game. A splash your licence lets you
skip can be skipped with a tap or a click as well as a key, once it has been up
for as long as your licence asks.

MEET CRASH BASIC 2

Crash BASIC 1 was me, on my own, with nobody telling me no. Every idea I had
went straight into the language — hundreds of keywords, each with its own
little rules, and the best kind of chaos. You made some wonderful games in it,
and it isn't going anywhere: a main.bas runs exactly as it always has.

Crash BASIC 2 I made with my grandpa, Victor Hardline. Grandpa does not
love chaos. He thinks a power like mine needs rules, and he's never been shy
about saying so! This time, instead of arguing about it, we built something
together. He brought the rules: every word you write is a function
underneath, every one takes its options the same way, and every one comes with
its documentation and its error messages. I brought everything else. You still
get to bend reality — it just bends in the same direction every time.

Victor's note: Predictable is not the same as boring. A thing that
behaves the same way every time is a thing you can build on.

Name your main file main.crash and CrashPlayer runs it as Crash BASIC 2. It's
the same engine — the same sprites, behaviors, maps, music and multiplayer —
so a game reads the way you'd expect:

SCREEN 320, 240
LOADIMG "hero.png", "hero" !! spritesheet((16, 16))

BEHAVIOR "fall"
    ACCELERATION 0, 0.4
END BEHAVIOR

ENABLE SPRITE 0
SET SPRITE 0 !! x(150) y(0) image("hero") frame(0)
    !! behavior("fall") tag("hero")

That !! is the WOMT operator — Wait! One more thing! Everything after
it is an option, by name, in any order, and a line that starts with !! carries
on the statement above it. No more remembering which keyword goes where.

And because every word is a function, you can make your own:

FUNCTION say(text, ending = ".", times = 1)
    PRINT (text + ending + " ") * times
END FUNCTION

FUNCTION loudly()
    RETURN {ending: "!!!"}
END FUNCTION

FUNCTION again(count)
    RETURN {times: count}
END FUNCTION

BIND SAY say ALLOW(loudly, again)

SAY "Crash BASIC 2 is here"           // prints: Crash BASIC 2 is here.
SAY "hooray" !! again(3) loudly       // prints: hooray!!! hooray!!! hooray!!!

Your keywords work exactly like mine: options after !!, help when you press
F1, and the same error messages. BLOCKBIND and SCOPEBIND go further
and make keywords that own a block — a DIRECTOR with an ACT for each part
of your game, say, written in a dozen lines of your own.

A few more things I'm proud of:

  • Chains. ## hands a value to the next step and _ is what arrived, so
    you can read left to right instead of inside out:
    result = 4 ## double(_) ## "the answer is " + _ ## ucase(_).
  • Lists and maps you can just write: [1, 2, 3] and
    {name: "Crash-tan", hp: 3}, read with hero.name. FOR item = list
    walks one, and FOR i = 1 ... 10 counts.
  • Small functions in place: twice = n => n * 2.
  • Errors that say what went wrong. You declare an error with the message a
    person will read, and the tip that helps them fix it.
  • REWRITE lets a line be written another way, which is how old code
    comes over a piece at a time.
  • Coordinates, vectors and matrices are built in: (x, y),
    (a) -> (b), and @* to multiply a list of rows.
  • // starts a comment, capitals don't matter, and there's one statement to a
    line.

Victor's note: One rule, applied everywhere. Exceptions are where the
trouble gets in.

Crash BASIC 2 runs in the desktop player and in the browser. It has its own
reference, with an example for every word that's run against the engine
before a release goes out. It works in the code editor, the debugger and
VS Code, all described below.

NEW LANGUAGES: PYTHON, LUA AND JAVASCRIPT

You can write your game in Python, Lua or JavaScript too. A word of honesty
first: these three are very new. They do work — whole games run in them,
and every example in their references is run before a release goes out — but
they're still catching up with BASIC. Expect a few rough edges and the
occasional slow spot, and tell us when you find one.

WRITE YOUR GAME IN PYTHON

Name your game's main file main.py instead of main.bas and CrashPlayer runs
it as Python. Same engine, same sprites, same behaviors, same sound — you just
talk to it in Python, through the crash module:

import crash

crash.screen(320, 240)
x = 160
while not crash.keydown("ESCAPE"):
    if crash.keydown("LEFT"):
        x -= 2
    if crash.keydown("RIGHT"):
        x += 2
    with crash.frame():
        crash.cls(0)
        crash.circle((x, 120), 10, 14, fill=True)
    crash.yield_()

Everything BASIC can ask the engine for, Python can too: drawing, sprites and
polygons, behaviors and patterns (with collision handlers written as Python
functions), maps, music and instruments, video, files and databases, scenes,
gamepads, purchases, and CrashNet multiplayer — crash.me, crash.players,
events, guards and all. Where Python already has a way to do something, you use
Python's: loops, functions, classes, lists and dicts are just Python, and
fetching from the web is urllib.request.

A few things worth knowing:

  • Your other .py files import. import helper finds helper.py next to
    main.py.
  • A sensible slice of the standard library comes along — math,
    random, time, json, re, collections, itertools, functools,
    dataclasses, enum, typing, copy, heapq and friends. Things that
    reach outside your game, like os, socket and subprocess, don't.
    open() reads and writes your game's own files, the same files OPEN does.
  • Pick one language per game. A folder with both main.bas and main.py
    (or main.lua) won't start, and says why.
  • Grids are lists of rows. A BASIC grid read g(x, y) is g[y][x] in
    Python, the way every other Python program (and every JSON file) lays one
    out.
  • BASIC is still there if you want it. crash.basic('PRINT "hi"') runs a
    line of Crash BASIC in the same engine, sharing your sprites, images and
    sounds.

Python runs in the desktop player and in the browser.

OR WRITE IT IN LUA

Name your main file main.lua (or main.luau) and CrashPlayer runs it as Lua
— Luau, the fast, typed Lua that Roblox made. The engine is the same one
again, through a crash table that's simply there, no require needed:

crash.screen(320, 240)
local x = 160
while not crash.keydown("Escape") do
    if crash.keydown("Left") then x -= 2 end
    if crash.keydown("Right") then x += 2 end
    crash.frame(function()
        crash.cls(0)
        crash.circle({x, 120}, 10, 14, { fill = true })
    end)
    crash.yield()
end

Everything Python can ask the engine for, Lua can too, behaviors and
collision handlers and CrashNet included. Lua's way of saying things:

  • Lists start at 0. This is the big one. Crash's Lua counts from 0, the
    way the engine, BASIC's arrays and Python's lists all do: t[0] is the
    first item, #t is how many there are, ipairs starts at 0, and
    ("hello"):sub(0, 1) is "he". Ranges include their end, and negative
    positions still count from the end. If you've written Lua before, this is
    the one habit to drop — and if you haven't, it's one less thing to trip on.
  • Yes-or-no answers are true and false. crash.keydown,
    crash.spriteenabled, crash.me.isclient and the rest answer with Lua's
    own booleans, so if crash.keydown("Up") then means what it says.
  • Options go in a table at the end:
    crash.set_sprite(1, { location = {100, 50}, layer = 2 }).
  • A point is {x, y}. One the engine hands back also reads .x and
    .y; crash.coord(x, y) makes one yourself.
  • A block's body is a function: crash.frame(function() ... end),
    crash.scene(...) and friends.
  • Your other files require. require("enemies") finds enemies.lua
    next to main.lua.
  • print puts a space between values, as PRINT does: print("lives", 3)
    shows lives 3.
  • Files and the web are the engine's: crash.open gives you Lua's own
    file methods on your game's files, and crash.http fetches. os.time,
    os.clock and os.date work as usual.
  • Types are optional, and checked. Annotate as much or as little as you
    like (local speed: number = 2); the editor checks your calls against the
    whole crash table as you type.

Lua runs in the desktop player and in the browser, and debugs just like BASIC
and Python (below).

OR WRITE IT IN JAVASCRIPT

Name your main file main.js and CrashPlayer runs it as JavaScript — a modern
one, run as a module, so await works at the top level. The crash object is
simply there:

crash.screen(320, 240);
let x = 160;
while (!crash.keydown("Escape")) {
    if (crash.keydown("Left")) x -= 2;
    if (crash.keydown("Right")) x += 2;
    crash.frame(() => {
        crash.cls(0);
        crash.circle([x, 120], 10, 14, 14);
    });
    await crash.yield();
}

The same engine again, behaviors, collision handlers and CrashNet included.
JavaScript's way of saying things:

  • Waiting is await. crash.yield(), crash.delay() and
    crash.pause() give you a promise. Forgetting the await is an error,
    rather than a game that quietly races ahead.
  • Options go in an object at the end:
    crash.set_sprite(1, { location: [100, 50], layer: 2 }).
  • A point is [x, y]. One the engine hands back reads .x and .y.
  • Yes-or-no answers are true and false.
  • Your other files import: import { spawn } from "./enemies.js" (the
    .js may be left off).
  • The usual helpers are there: console.log, setTimeout and
    setInterval, fetch, and performance.now().
  • Types if you want them. The engine comes with crash.d.ts, so an editor
    that speaks TypeScript completes and checks your crash calls.

JavaScript runs in the desktop player and in the browser, and debugs like the
others.

A REFERENCE FOR EVERY LANGUAGE, AS COMPLETE AS THE BASIC ONE

Every crash name has its own entry in each language — Crash BASIC 2, Python,
Lua and JavaScript: what it does, its parameters, and an example. Every one of
those examples is run against the engine before a release goes out (and, in
Lua, type-checked too), so none of them can quietly stop working. Press F1
on a name in the code editor (below) to read it.

THE MOUSE WHEEL

MOUSEWHEEL() tells you how many notches the wheel has turned since you last
asked: positive for up, negative for down. MOUSEWHEELX() does the same for
sideways scrolling (a tilting wheel, or a trackpad):

sel = sel - MOUSEWHEEL()              ' scroll a list
zoom = zoom * 1.1 ^ MOUSEWHEEL()      ' each notch up, 10% closer

Reading it takes the notches, so read it once a frame into a variable.
A trackpad's small movements add up to whole notches. It reads the way you
turned the wheel even with a Mac's "natural" scrolling on, so rolled forward is
always up — except in the browser on a Mac, where no web page can see that
setting. In multiplayer, STREAM MOUSE BUTTONS sends the wheel to the server
along with the clicks. It's mousewheel() in Crash BASIC 2 and
crash.mousewheel() in Python, Lua and JavaScript.

DEFAULT ARGUMENTS START FRESH

Our Python parts ways with standard Python in one place, on purpose. In
standard Python, a default like people=[] is made once and then shared by
every call, so it keeps whatever earlier calls put in it. In Crash, each call
gets a fresh one:

class Person:
    def hello(self, people=[]):
        people.append("bob")
        return people

p = Person()
print(p.hello())   # ['bob']
print(p.hello())   # ['bob'] (standard Python says ['bob', 'bob'])

Lists, dicts, sets and bytearrays are made fresh, nested ones included.
Everything else — numbers, strings, and any object you pass as a default — is
the same object every time, just as in standard Python. This applies to your
game's own files; the standard library keeps the usual rule, because it relies
on it.

A CODE EDITOR IN THE PLAYER

crashplayer --ide opens a code editor on your game folder (or name one:
crashplayer --ide my_game). It's built for Crash BASIC (both of them),
Python, Lua and JavaScript:

  • a file tree, tabs, and syntax highlighting
  • completion as you type: BASIC suggests exactly what can come next at the
    cursor, plus your own SUBs, FUNCTIONs and variables; Python and Lua suggest
    crash names, keywords and your own definitions — and inside a call, its
    parameters (Python) or the keys of its options table (Lua)
  • mistakes underlined as you type, worded the way the player would word them,
    and listed in a Problems tab; in Lua, a wrong type is a mistake too, like a
    word where crash.screen wants a number
  • hover a name for its syntax; Ctrl+click (Cmd+click on a Mac) to jump to where
    it's defined
  • find and replace
  • F1 for the full reference entry on whatever's under the cursor

F5 runs your game, Ctrl+R restarts it (skipping the splash), and
Ctrl+Enter pushes the selection — or the whole file — into the game while
it's running, so you can change a SUB (or a Python function, or a global Lua
function) and see it straight away. Your open tabs,
cursors and layout are remembered in a .crashide file in the game folder.
(On a Mac, Ctrl is Cmd throughout.)

DEBUG IN ANY OF THEM

Click in the editor's margin (or press F9) to set a breakpoint. When your
game reaches it, it stops there and shows you the line:

  • F5 continues; F10, F11 and Shift+F11 step over, into and out
  • the Debug tab shows locals and globals (open up arrays, objects and shapes to
    see inside), a watch list, the call stack with every caller at its line, and
    your threads
  • hover a variable to see its value
  • type an expression into the console to evaluate it right there

A runtime error stops at the line that caused it, with everything still there
to look at. Python games debug exactly the same way, in the editor and in the
browser, and so do Lua games; Crash BASIC 2 and JavaScript games debug the
same way in the editor. In Lua the console sees
the stopped function's locals, and assigning one there changes it for the
rest of the run. A game being debugged runs at nearly full speed until it
stops.

THE EDITOR OVER YOUR GAME — PRESS F12

While a game is running in CrashPlayer, press F12 and the same editor opens
over the top of it, in the game's own window, with your game still showing
through. Everything in the section above works here too. Fix something, press
Restart, and the new version runs right there in the same window — no relaunch.
Press F12 again to get back to playing. While the editor is open, your
game doesn't see your typing or mouse clicks, so editing doesn't steer your
character.

Hit a breakpoint or a runtime error and the editor opens by itself.

VS CODE SPEAKS EVERY ONE OF THEM

With the DevKit's player installed, the VS Code extension gets everything the
editor above knows, for every file of your game: completion, mistakes as you
type, hover help, signature hints and go to definition — in Crash BASIC (1 and
2), Python, Lua and JavaScript alike, since it's the same code answering.
F1 opens the reference for the language you're in, and F5 debugs a game
in any of them just as it does a BASIC one.

On Windows, F12 needs the cross-platform renderer for now: start CrashPlayer
with --renderer wgpu.

COOLDOWN KEEPS TIME

COOLDOWN 1, 240 is supposed to run 240 times a second. In 1.62.0 and 1.63.0
it ran about 100 times a second, and COOLDOWN 1, 60 about 50: anything
faster than roughly 100 a second got cut down, and anything in between got
rounded. Now a COOLDOWN keeps its rate, in BASIC and in Python.

If your loop gets busy and a cooldown falls behind, it catches up, so it
averages the rate you asked for. It never runs more than once each time your
loop reaches it. And if your loop stops reaching it for a while — a pause
screen, a level loading — it runs once when you come back, rather than firing
off everything it missed.

A SUB'S VARIABLES ARE ITS OWN

A variable you set inside a SUB or FUNCTION is supposed to belong to that
call alone, unless it's a global. It didn't: a SUB could read and change the
variables of whichever SUB called it. So if Frame set changed = TRUE and
then called a widget whose own code said changed = FALSE, Frame's
changed went back to FALSE. Now each call keeps its own:

SUB Outer()
    x = 1
    CALL Inner()
    PRINT x        ' 1 — it used to print 2
END SUB

SUB Inner()
    x = 2          ' Inner's own x
END SUB

Globals work as before: set a variable at the top of your program and every
SUB reads and writes it, unless the SUB DIMs its own. A handler that runs
from inside a SUB can't see that SUB's variables either, and recursive
functions keep each call's variables apart.

Arguments now come from the caller too. CALL P(b, a), where P names its
parameters a, b, passes your b and your a. It used to pass P's own
a, which it had only just set.

SPRITE SHADERS SEE ONE FRAME

A shader on an animated sprite used to see the whole sprite sheet, not the
frame being drawn: UV ran from 0 to 1 across the sheet, so an effect sized to
the sprite came out the wrong size, and OWNTEXTURE could read the frames next
door. Now a sprite's shader sees just its own frame — UV runs 0 to 1 across
it, OWNTEXTURE(UV) is the frame's own pixel, and PX counts the frame's
pixels. Flipped and rotated sprites included. Shaders on layers and whole
images aren't affected.

If you worked around this with your own sprite-sheet maths, you can take that
out.

MAZE AND ITS FRIENDS ARE INDEXED (x, y)

MAZE used to hand you a grid indexed (row, col) — sideways from every
other array in Crash BASIC. Now it's (x, y), the cell at (x, y), and
ASTAR, FLOODFILL, RAYCAST and NEIGHBORS read their grids the same way:

grid = MAZE(21, 21, 7)
IF grid(1, 1) = 0 THEN PRINT "floor"

If your code reads a MAZE result as grid(row, col), swap the two
indexes.
A maze saved as JSON still comes out as rows, as before.

And you can name the result maze. An array that shares its name with a
built-in function used to lose to it, so maze(1, 1) quietly called
MAZE(1, 1) and handed you a brand-new tiny maze. Now name(...) on a
variable holding an array (or an object) reads the variable. Your own
FUNCTIONs keep their names.

TILEMAP ... INTO had the same problem the other way round, filling its array
(y, x). It fills (x, y) now too.

NUMBERS STAY NUMBERS

LOG(0), ACOS(2), EXP(1000) and POW(10, 400) used to hand back
infinity or "not a number", and the next thing to use it quietly read it as 0,
or as the biggest whole number there is. Now a calculation that can't give a
real number is an error at the line that made it, the way dividing by zero
always was. VAL("inf") is VAL's not-a-number error too. Where the engine
needs a whole number — a sprite id, a pixel — it drops the fraction, toward
zero, in every language.

OSCILLATE'S PHASE IS IN DEGREES

BOSCILLATE's SPEED has always been degrees per tick, but its PHASE was
radians, so one statement measured its angles two ways. PHASE is degrees
now too: PHASE 90 starts a quarter of the way through the swing.

BEHAVIOR "swarm"
  BOSCILLATE AMPLITUDE 0, 8 SPEED 4 PHASE 0 VARIANCE(0, 360)
END BEHAVIOR

If your code gives PHASE in radians, change it to degrees: 6.28
becomes 360, 3.14 becomes 180, 1.57 becomes 90.

SOUND PLAYS THE SAME EVERYWHERE

Sound and music now work the same in the browser as on desktop and mobile,
so a game sounds the way you heard it wherever it runs. Two things change:

  • A sound played again plays twice. PLAYWAV "zap" while a zap is
    still sounding starts a second one over it, on desktop and mobile as it
    always did in the browser; it used to cut the first one off. STOPWAV "zap" stops every one of them. PLAYMP3 and STOPMP3 work the same way.
  • PLAY waits for its notes, not their echo. In the browser, a PLAY
    without BACKGROUND now carries on the moment its last note ends, as it
    always did on desktop and mobile, while that note rings out; it used to wait
    for the sound to fade completely.
PLAYWAV "zap"
PLAYWAV "zap"          ' two zaps, one over the other
PLAY "T160 L8 CDEFGAB>C"
PRINT "the tune's notes are done"

If a game plays a sound repeatedly and relied on each play cutting off the
last, STOPWAV it first.

LAYERS CAN START OFF

CREATE LAYER and CREATE MAP take OFF, so a layer can be set up out of
sight: put its sprites on, set its scroll, change its tiles, then show it
ready with SET LAYER n ON. Nobody sees the half-built frame.

CREATE MAP "level2" AS LAYER 1 TO 2 OFF
SET LAYER 1
SET SCROLL 640, 0
SET LAYER 1 ON
SET LAYER 2 ON

It works the same in multiplayer. A map the host creates OFF reaches every
player hidden, and a player who joins, or walks into the room, later gets the
layer on or off the way it is now.

SET LAYER from the host now reaches players too: ON, OFF, ALPHA,
WRAPX, WRAPY, CLIP, AFFINE, PERSPECTIVE and FLAT used to do nothing
there, and reach every player in the surrounding WITH ROOM now (everyone,
outside one). From a player-server they reach that player, where all but the
plane modes used to be dropped. A player who joins, or walks into the room,
later gets every layer's settings as the players already there have them.

UPDATE MAP used to show a layer you had turned off, and put its ALPHA,
wrap and plane back to the defaults. It keeps them all now.

A 1080p WINDOW

The desktop player opens a 1920×1080 window unless you give it a size, and the
launcher starts on 1920×1080 too. On a display too small for that, the window
is shrunk to fit, keeping its shape. Any other size you give with
--window-size opens exactly as asked.

MORE FIXES

  • The "Ready to go?" quit dialog is just the question and its buttons now; the
    picture beside it never sat right at every window size.
  • LEN and INSTR count characters, not bytes: LEN("héllo") is 5.
  • REGEX with a broken pattern is an error that tells you so, rather than
    quietly returning 0. MUSICLENGTH likewise, when it can't measure the song.
  • GUARD(SINGLE) now matches in a single-player game, and GUARD(CLIENT)
    doesn't — they had it backwards.
  • EVENT HOST, EVENT SERVER and the other targeted forms fire right where you
    are in a single-player game, instead of stopping it.
  • Giving a sprite both SCALE and SCALETO is now an error (one of them used
    to quietly win). Same for SET POLYGON, POLYGON, EMITPARTICLE and
    PUTIMAGE.
  • SET POLYGON n RESET now resets rotation and flips too, and no longer sets
    the shape moving again a moment later.
  • LINE 0, 5 and a polygon point written 0 UV (...) no longer swap their
    values.
  • CALL PLAYER and CALL HOST with an argument list are an error; the
    handler gets its arguments from the runtime.
  • CALL TIMER ... GUARD(ONCE) is an error, since a timer's runs never overlap.
  • CREATE MAP "file.json" LAYER n TO m uses the layers you name.
  • SCREEN with a title no longer mixes the title into its scale, CLS SIZE
    works, MAXVELOCITY NONE takes effect, CALL &f calls the function,
    SET MUTATION ... BEHAVIOR DELETE removes it, and polygon UVs keep their
    fractions.
  • Collision handlers that fire in the same tick run in the same order every
    time.
  • A game with lots of bound polygons runs its behaviors up to five times
    faster.
  • A crowd of sprites whose collision handlers change how they look (SET SPRITE n FRAME) runs about a third faster, with far fewer hitches: thousands
    of sprites no longer make each change cost more.
  • On desktop and mobile, a command and a waiting one straight after it (as
    LOADMUSIC loads a bank, then renders a song from it) could now and then be
    carried out in the wrong order, so a correct game got "Song not found".
    They always run in order now.
  • In the browser, a page cached from an older version now tells you to clear
    your cache and refresh, instead of showing a black screen.
  • In the browser, PLAYWAV ... AT(x, y) pans the sound, from -1 on the left to
    1 on the right; it used to play every sound from the middle.
  • DISTANCE, SEEK, FLEE, WANDER, NEAREST, ASTAR, RAYCAST,
    FLOODFILL and NEIGHBORS given a list or text where a point goes are an
    error that names the function, rather than quietly reading it as (0, 0).
  • SET INSTRUMENT LAYER ... FMTARGET(-1) turns a modulator back into a layer
    you can hear, in place; it used to be an error, and deleting the layer was
    the only way back.
  • An instrument you load is heard by the very next PLAY; on desktop the
    first note could still use the old one.
  • obj(key) = value on an object writes under that key; it used to write
    every key as "0".
  • A .cbm song's loop, repeat, muted and soloed flags accept 1 and 0 as well
    as true and false — what SAVEJSON of a BASIC object writes.