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 withhero.name.FOR item = list
walks one, andFOR i = 1 ... 10counts. - 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. REWRITElets 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
.pyfiles import.import helperfindshelper.pynext tomain.py. - A sensible slice of the standard library comes along —
math,random,time,json,re,collections,itertools,functools,dataclasses,enum,typing,copy,heapqand friends. Things that
reach outside your game, likeos,socketandsubprocess, don't.open()reads and writes your game's own files, the same filesOPENdoes. - Pick one language per game. A folder with both
main.basandmain.py
(ormain.lua) won't start, and says why. - Grids are lists of rows. A BASIC grid read
g(x, y)isg[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()
endEverything 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,#tis how many there are,ipairsstarts 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
trueandfalse.crash.keydown,crash.spriteenabled,crash.me.isclientand the rest answer with Lua's
own booleans, soif crash.keydown("Up") thenmeans 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.xand.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")findsenemies.lua
next tomain.lua. printputs a space between values, as PRINT does:print("lives", 3)
showslives 3.- Files and the web are the engine's:
crash.opengives you Lua's own
file methods on your game's files, andcrash.httpfetches.os.time,os.clockandos.datework 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
wholecrashtable 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()andcrash.pause()give you a promise. Forgetting theawaitis 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.xand.y. - Yes-or-no answers are
trueandfalse. - Your other files import:
import { spawn } from "./enemies.js"(the.jsmay be left off). - The usual helpers are there:
console.log,setTimeoutandsetInterval,fetch, andperformance.now(). - Types if you want them. The engine comes with
crash.d.ts, so an editor
that speaks TypeScript completes and checks yourcrashcalls.
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% closerReading 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 andcrash.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 suggestcrashnames, 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 wherecrash.screenwants 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'schanged 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 SUBGlobals 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 owna, 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), andASTAR, 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 calledMAZE(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 ownFUNCTIONs 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 BEHAVIORIf 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.PLAYMP3andSTOPMP3work the same way. PLAYwaits for its notes, not their echo. In the browser, aPLAY
withoutBACKGROUNDnow 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 ONIt 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. LENandINSTRcount characters, not bytes:LEN("héllo")is 5.REGEXwith a broken pattern is an error that tells you so, rather than
quietly returning 0.MUSICLENGTHlikewise, when it can't measure the song.GUARD(SINGLE)now matches in a single-player game, andGUARD(CLIENT)
doesn't — they had it backwards.EVENT HOST,EVENT SERVERand the other targeted forms fire right where you
are in a single-player game, instead of stopping it.- Giving a sprite both
SCALEandSCALETOis now an error (one of them used
to quietly win). Same forSET POLYGON,POLYGON,EMITPARTICLEandPUTIMAGE. SET POLYGON n RESETnow resets rotation and flips too, and no longer sets
the shape moving again a moment later.LINE 0, 5and a polygon point written0 UV (...)no longer swap their
values.CALL PLAYERandCALL HOSTwith 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 muses the layers you name.SCREENwith a title no longer mixes the title into its scale,CLS SIZE
works,MAXVELOCITY NONEtakes effect,CALL &fcalls the function,SET MUTATION ... BEHAVIOR DELETEremoves 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
LOADMUSICloads 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,FLOODFILLandNEIGHBORSgiven 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) = valueon an object writes under that key; it used to write
every key as "0".- A
.cbmsong's loop, repeat, muted and soloed flags accept 1 and 0 as well
as true and false — whatSAVEJSONof a BASIC object writes.