Van Python naar Rust en WASM: een Mario clone opnieuw bouwen
Ik had een werkende Super Mario clone in Python. Pygame, sprites, levels als PNG-bestanden — het werkte prima.
En toen dacht ik: wat als ik dit herschrijf in Rust, en het draait in de browser?

Dat was het begin van een avontuur.
Waarom Rust?
Niet omdat Python te langzaam was. De clone draaide prima.
Maar Rust trok al een tijdje aan me. De belofte: geheugenveiligheid zonder garbage collector, razendsnel, en — cruciaal — compileert naar WebAssembly. Eén codebase, draait in elke browser.
En eerlijk: soms is een project gewoon een goed excuus om iets nieuws te leren.
Bevy: een andere manier van denken
Voor de game engine koos ik Bevy — een open source Rust engine die werkt met een Entity Component System (ECS).
Dat is fundamenteel anders dan object-georiënteerd programmeren:
- Geen
class Enemymet methods - Wel: entiteiten met losse componenten (
Position,Velocity,Enemy,Sprite) - Systemen die die componenten lezen en schrijven
Het voelt in het begin onwennig. Maar als het klikt, is het elegant: alles is data, logica is gescheiden, en de engine kan het paralleliseren.
fn move_enemies(mut query: Query<(&mut Transform, &Velocity), With<Enemy>>) {
for (mut transform, vel) in query.iter_mut() {
transform.translation.x += vel.x * dt;
}
}
Geen inheritance, geen shared state. Gewoon queries op de wereld.
De port: wat er bij komt kijken
De originele Python code was redelijk gestructureerd, maar pygame en Bevy denken heel anders. Het was geen conversie — het was een herschrijf.
De grote onderdelen:
-
Levels — opgeslagen als PNG-bestanden, pixels als tiles. In Python leest pygame ze gewoon. In WASM kan je geen bestanden lezen. Oplossing: include_bytes!() — de PNG's worden letterlijk in de binary
gebakken bij compilatie. -
Physics — zwaartekracht, botsingen, springen. Bevy heeft geen ingebouwde 2D physics voor dit soort platformers, dus dat is handmatig gebouwd: AABB-collision, aparte zwaartekrachtconstanten voor het
indrukken én loslaten van de springknop. -
Audio — Bevy's audiosysteem werkt, maar in de browser loop je direct tegen Chrome's autoplay-beleid aan. Een AudioContext mag niet starten zonder gebruikersinteractie. Oplossing: de WASM alvast compileren zodra de pagina laadt, en pas uitvoeren na een klik — dan valt de new AudioContext() binnen het gesture-venster van de browser.
-
UI — tekst in Bevy 0.15 werkt in wereldcoördinaten. De camera staat niet op de oorsprong. Dat betekent: elke tekst die je op het scherm wil tonen, moet je positioneren relatief aan de camera. Dat kostte meer tijd dan verwacht.
De bugs waren het interessantst
Een paar dingen die mis gingen — en wat er achter zat:
-
Het stompen van een vijand resettte het spel. Bleek: het systeem dat botsingen detecteerde draaide na het stomp-systeem, maar voor de cleanup. De vijand was nog niet echt weg, dus Mario raakte hem alsnog
van de zijkant. Fix: systemen expliciet in volgorde zetten met .chain(), en vijanden eerst als dead markeren in plaats van direct te despawnen. -
De death-animatie was onzichtbaar. De tekst "YOU DIED" werd gespawned op wereldpositie (0, 0) — maar de camera stond op (2000, -240) na een heel level lopen. Fix: altijd positioneren relatief aan de huidige camerapositie.
-
B0003 dubbele despawn. Bevy waarschuwt als je een entity twee keer probeert te verwijderen. Dat gebeurde doordat cleanup-systemen entiteiten wilden verwijderen die in hetzelfde frame al gequeued waren voor despawn. Fix: apply_deferred voor de cleanup systemen, zodat de command buffer eerst geflusht wordt.
▎ Elke bug had een logische verklaring. Rust en Bevy dwingen je om precies na te denken over wat wanneer gebeurt.
WASM: het eindresultaat
De buildpipeline:
cargo build --target wasm32-unknown-unknown --profile wasm-release
wasm-bindgen --out-dir ./dist --target web rusty-mario.wasm
cp -r assets dist/ && cp index.html dist/
En dan:
python3 -m http.server 8765 — en de game draait in de browser.
Alle 4 levels, vijanden, baas, geluid, animaties. Exact hetzelfde als de Rust binary, maar dan in Chrome.
Wat ik ervan meeneem
Rust heeft een steile leercurve, maar die is eerlijk. De compiler zegt je precies wat er mis is — en als het compileert, werkt het meestal ook.
Bevy is nog jong (0.15 ten tijde van dit project), maar al indrukwekkend bruikbaar. Het ECS-model vraagt om een andere manier van denken, maar is de moeite waard.
En WASM? Dat is gewoon magie. Dezelfde code, in elke browser, zonder installatie.
De originele Python-versie waar dit op gebaseerd is kwam trouwens van een oud SourceForge-project:
Super Mario Bros. in Python op SourceForge De Rust/WASM-versie en broncode staan inmiddels op GitHub. Gewoon klikken, Z indrukken, en springen.
Soms is iets opnieuw bouwen de leukste manier om te begrijpen hoe het eigenlijk werkt.