The Web¶
An Acron game can be exported as a static web site: an HTML page, the game compiled to WebAssembly, and its files. It renders with WebGL 2 (WebGL 1 as a fallback) and plays sound through Web Audio, in any current desktop browser, and needs nothing but a web server to host it – your own, GitHub Pages, or a game portal such as itch.io. Saved games stay in the player’s browser.
Nothing has to be installed on the player’s side, and the same site runs on Windows, macOS and Linux. The export itself also works from any of the three: WebAssembly is the same everywhere, so the engine is built for it once.
Exporting a C++ game¶
A C++ game is compiled against the web SDK: the engine as static
libraries for Emscripten, with the CMake
package and a toolchain file. The SDK records the Emscripten version it was
built with, in EMSCRIPTEN_VERSION; static WebAssembly libraries only
link with that one.
With acronmanager, declare the game’s CMake build (see its README) and
run:
acronmanager dist --web
It fetches the latest web SDK and the Emscripten it names (once, into a
shared cache; Emscripten is about 1 GB), configures the project with the
SDK’s toolchain file in build/web, builds it, and zips the site into
dist/<name>-web.zip. A [tool.acron.web] table in pyproject.toml
can name a local SDK (sdk), a step that prepares the game’s data first
(prepare), and extra configure arguments.
By hand, with the SDK unpacked and Emscripten installed (EMSDK pointing
at it):
cmake --toolchain <sdk>/lib/cmake/Acron/AcronWebToolchain.cmake \
-S . -B build-web -G Ninja
cmake --build build-web
The toolchain file is Emscripten’s own plus the SDK as a search root, so
find_package(Acron) finds the SDK. In the game’s CMakeLists.txt,
acron_web_executable() does the rest:
find_package(Acron REQUIRED COMPONENTS Framework)
add_executable(mygame main.cxx)
if(EMSCRIPTEN)
acron_web_executable(mygame
TITLE "My Game"
DATA data/models data/textures # into the program's file system
DATA_DIR /data # under /data (default /)
PERSIST /save) # kept in the browser between visits
endif()
It links the engine (the framework, the WebGL renderer, OpenAL and the egg
loader; link other components, such as Bullet, yourself), registers the
plugins a static program cannot load by name, and after every build lays
out the site in <build dir>/mygame-web/:
index.html the page
acron-web.js loads the program and its files, keeps PERSIST
mygame.js
mygame.wasm the game
acron-web.json what acron-web.js loads
files/... the DATA
More options: ICON (the site’s icons, a directory as acronmanager
assets makes them or one PNG; see App Icons and Store Art), SHELL (your own
page, see Your own page), ARGS (the command line), ENV NAME=value
..., OUTPUT_DIR, INITIAL_MEMORY (MB; the heap grows as needed)
and STACK_SIZE (KB).
The game’s main loop has to give the frames to the browser.
AcronFramework::main_loop() does: it hands the task manager over and does
not return, so all set-up comes before it and per-frame work goes in tasks.
A game that calls do_frame() in a loop of its own must call
main_loop() instead.
Starting before everything has arrived¶
Everything a web game starts with is downloaded before it starts. A large game can start much sooner if the big assets – maps, skies, music – come later, as content packs: Multifiles that the page downloads while the game runs, and that the game mounts over its own directory when they have arrived, so their files keep their paths.
Name the packs in acron_web_executable(), once per pack, with the
files or directories they hold; their paths are those they would have had
as DATA:
acron_web_executable(mygame
DATA models/player.bam
PACK arena models/arena textures/arena
PACK music music
BACKGROUND_PACKS arena) # fetched as soon as the game runs
In the game, ContentPack fetches a pack and tells when its files can be
loaded:
// Starts the download.
PT(ContentPack) pack = ContentPack::request("arena");
...
// In a task, each frame until it is in:
if (pack->is_failed()) { /* pack->get_error() */ }
else if (pack->is_ready()) { /* load models/arena/... */ }
else { /* show pack->get_progress() */ }
Writing a pack takes Python 3.8 or later (the one Emscripten itself runs on).
The page fetches one pack at a time, the ones the game asks for first, and keeps them in memory (not between visits). Downloads are whole Multifiles: split the content where the game needs it in parts (a pack per map) rather than streaming parts of one pack.
What is different on the web¶
The browser is a sandbox, and a few things a desktop game takes for granted work differently or not at all:
Files. The game sees its files as ordinary files, read from memory: they are downloaded before it starts, except for content packs (see Starting before everything has arrived). Writes to a persistent directory reach the browser’s storage (IndexedDB) within two seconds and when the page is hidden; everything else is lost when the page closes. The player can clear that storage with the site’s data.
One thread. There are no threads and no blocking waits. Long work in one frame freezes the page for that long.
No native plugins. Everything the game uses is in the one program. For Python, that means pure-Python packages only: a package with compiled extensions needs a WebAssembly build of its own (there is none for numpy, so
acron.hud’sRaster, which uses it, is not available; the rest ofacron.hud,MapViewincluded, is).Shaders. GLSL 330 and later goes through the shader pipeline and reaches WebGL as the ESSL it accepts; older GLSL (
#version 120) is refused, as it is on the desktop. A shader that names#version 300 esitself goes to WebGL as written, and must declare its own precisions (precision highp float; precision highp sampler2DShadow;). Desktop-only OpenGL features – geometry and tessellation shaders,glPolygonMode, reading textures back withglGetTexImage– are not there, and code that calls OpenGL directly gets OpenGL ES 3.0. Offscreen buffers can be multisampled; one in a format WebGL cannot multisample renders without it (with a warning).Input. Keyboard, mouse and touch (as a mouse). The page focuses the game on the first click. Relative mouse mode (
M_relative) becomes the browser’s pointer lock, which only a click can start, and which the browser ends itself when the player presses Esc: a game with mouse look should check the window’s mouse mode, treat its return toM_absoluteas a pause, and lock again on the next click.Networking. A page can neither send UDP nor listen for connections.
acron.enetis in the runtime, but a game’s server in the same page needs an in-process transport (queues standing in for the ENet host and peers), and play over the network needs a server reachable over WebSockets or WebRTC.Sound starts with the player’s first click or key press; browsers allow no sound before it.
Quitting is closing the tab: hide the game’s Quit item on the web.
Size. The engine is a large program to download: a Python game’s runtime (CPython and the whole engine) is about 30 MB, 7 MB with Brotli; a C++ game links only what it uses (the starter is 11 MB). Serve
.wasm,.jsand data files compressed (see below).
Serving the site¶
The site is static files, but the server has to send .wasm files as
application/wasm: browsers only compile WebAssembly while it downloads
when it has that type. Most servers do; Python’s own does:
python -m http.server -d build/emscripten_wasm32 8000
then open http://localhost:8000/. acronmanager run --web does the same and
opens the browser. Opening index.html straight from disk does not work:
browsers do not let a page fetch file: URLs.
For a public site, compress: .wasm files shrink to about a quarter with
Brotli and a third with gzip, .js files more. Most hosts and portals
compress by themselves; on your own server, enable it for
application/wasm and text/javascript, or upload precompressed
.br/.gz files with the matching Content-Encoding.
On itch.io, upload the zip as an HTML game, leave SharedArrayBuffer support off (the engine does not use threads), and give the frame the game’s size or choose click to launch in fullscreen.
Your own page¶
The default page fills the browser window, shows a progress bar while the files
load, and a fullscreen button. To use your own (web_shell, or SHELL),
start from the engine’s (share/acron/web/shell.html in the web SDK). All
a page needs is a <canvas id="canvas" tabindex="-1">, acron-web.js,
and:
AcronWeb.start({
manifest: "@MANIFEST@",
onProgress(loaded, total) {}, // bytes of the files downloaded so far
onStatus(text) {},
onReady() {}, // the game is running
onError(message) {},
});
@ICONS@ in <head> becomes the tags of the site’s icons (the
favicon, the home screen icon and the web app manifest), or an empty icon
when it has none.
AcronWeb.module is then the game’s Emscripten module, and
AcronWeb.sync() writes the persistent directories out at once.