Gilbert François Duivesteijn
Retro Tennis, (also known as Pong) is a table tennis–themed twitch arcade sports video game, featuring simple two-dimensional graphics, manufactured by Atari and originally released in 1972. (Source: Wikipedia)
This program runs as a native desktop application or in the browser as a WebAssembly application.
Native application | WebAssembly in the browser |
key | description | key | description |
---|---|---|---|
[w] | player 1 up | [u] | player 2 up |
[s] | player 1 down | [j] | player 2 down |
[mouse] | player 1 up/down | ||
[1] | 1 player | [0] | demo mode |
[2] | 2 players | [r] | restart |
key | description (desktop version only) |
---|---|
[f] | Toggle full screen |
[q] / [esc] | Quit |
Compiling the project should be straight forward, thanks to vcpkg. After compiling and installing, the program pong
is located in the <project_folder>/dist
.
git clone https://github.com/gilbertfrancois/retro-tennis.git
cd retro-tennis
# Important!
git submodule update --init --recursive
# Build the project and its dependencies
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake --install build
Test and run with:
cd dist
./pong
Compiling for the web requires some more steps. First build the C code with cmake, then the web code with npm:
git clone https://github.com/gilbertfrancois/retro-tennis.git
cd retro-tennis
# Important!
git submodule update --init --recursive
# Install emscripten (only first time)
./3rdparty/emsdk/emsdk install latest
# Activate the build toolchain
./3rdparty/emsdk/emsdk activate latest
source 3rdparty/emsdk/emsdk_env.sh
# Build the project and its dependencies
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DWASM=1
cmake --build build
cmake --install build
# Build the HTML and JavaScript code
cd web
npm install
npm run prod
Test and run with:
cd web/dist
python -m http.server 8000
Open a browser and go to http://localhost:8000
-
The usual main loop in C, a
while(true) {...}
loop cannot be used in WASM. You have to use the browser'srequestAnimationFrame
mechanism to prevent blocking all resources of the browser. Look for theemscripten_set_main_loop_arg
andemscripten_set_main_loop_timing
functions in the Emscripten documentation. [source: Emscripten API] -
Make sure that the SDL2 Audio / Mixer is loaded after the user clicked on the canvas. Modern web browsers will not permit web pages to produce sound before the user has interacted with them. SDL-based apps also have to deal with this problem. If the user hasn't interacted with the page, SDL_OpenAudioDevice will fail and the WASM app crashes. Retro-Tennis solves it by having an intro screen with a bouncing ball. Only when the user has clicked on the canvas to start the game, the audio is initialised and plays sound successfully. [source: SDL Wiki]
-
The audio files need to be preloaded and packed as
target_filename.data
file, by adding--preload-file [filename]
. If you omit this step, you won't get enough memory assigned by thetarget_filename.js
WASM loader and your WASM application will crash due to memory errors. -
When you use PackerJS or Webpack to compile your JavaScript sources, you need to add
-s ENVIRONMENT='web'
to theC_FLAGS
inCMakefileLists.txt
. Emscripten's emcc command produces both .js and .wasm files. The .js file is not only a wrapper of the .wasm but also provides a C/C++ runtime for the web, and more importantly in the question, a generic loader for the .wasm file.The .js file's loader is very comprehensive that covers node, web, web worker, and D8 JS shell by default. This often make problems with bundlers such as Webpack because the bundlers try to resolve all modules in the source code, including require('fs') which is used for loading .wasm file in the nodejs environment, and the bundler produces error if any module is not available for the web environment.
One way to avoid the problem is that to use ENVIRONMENT flag in emcc command. This will remove some problematic codes such as require('fs'). [source: stackoverflow]
-
When you use PackerJS, you have to copy manually your .wasm and .data files in the ./dist folder. PackerJS does not know by itself that these files are needed. In this project, it has been solved by adding explicit copy commands to the
package.json
file for the dev and prod targets.