FRANZENONLINE
17 mei 2026

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 Enemy met 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.