Hoe maak je een Nintendo 64-game in 2026?
Het resultaat is Xibalba 64 – een FPS in de stijl van Wolfenstein 3D. Modretro is ermee ingestemd om het spel uit te brengen als fysieke launch-titel voor hun M64 (een moderne N64-kloon), compleet met cartridge, verpakking en handleiding.
Voor zover ik weet, is dit pas de tweede fysieke release van een nieuwe N64-game sinds het einde van de commerciële levensduur van de console. De beruchte Xeno Crisis van Bitmap Bureau – oorspronkelijk een nieuw spel voor de Sega Mega Drive en later uitgebracht op vele andere consoles – kwam in 2023 naar de N64. Sinds Tony Hawk's Pro Skater 3 in 2002 zijn er geen andere nieuwe games voor de N64 gepubliceerd.
De Engine
Impact was een JavaScript-game-engine die ik in 2010 ontwikkelde. Deze was specifiek gemaakt voor 2D-actiespellen en beheerde tile sheets, achtergrondkaarten, sprites en collisiedetectie. Het is zeer eenvoudig, maar vormt nog steeds een solide basis voor alles wat je ermee wilt doen.
Twee jaar geleden heb ik Impact herschreven in C. Waarom? Ik weet het niet; het was leuk. Deze C-port, highimpact, maakt gebruik van een "platform backend". Het platform regelt de low-level infrastructuur – het openen van een venster, het creëren van een tekenoppervlak, het lezen van input, enzovoort. Standaard wordt highimpact geleverd met twee platform backends (SDL2 en Sokol), waardoor je je spel voor beide kunt compileren. Dit stelt high_impact-games al in staat om op veel verschillende apparaten te draaien.
Ook de rendering backend in high_impact is modulair. Je kunt je game compileren met een software-renderer, OpenGL of Metal (voor iOS/macOS). Ondersteuning voor nieuwe platform backends of rendering backends kan worden toegevoegd zonder andere delen van de engine aan te passen. Een perfect startpunt voor een N64-game.
N64 Hardware en Platformbibliotheek
De N64 is een eigenaardig apparaat. Naast de 93 MHz MIPS CPU (big-endian!) heeft het twee coprocessoren voor graphics, geluid en meer:
- “Reality Display Processor” (RDP) – een fixed-function graphics processor.
- “Reality Signal Processor” (RSP) – een programmeerbare vectorprocessor.
Beide bevinden zich in hetzelfde fysieke pakket, gemeenschappelijk aangeduid als de “Reality Coprocessor” (RCP).
Gedurende de eerste jaren van het bestaan van de N64 bewaakte Nintendo de toegang tot de RSP strikt. Deze werd uitsluitend gebruikt door de officieel goedgekeurde platformbibliotheek van Nintendo, “libultra”. Pas later stond Nintendo gamestudio's toe om eigen “microcode” (eigenlijk gewoon MIPS-assembly) voor de RSP te schrijven.
Het hardwaregelukkig houden is geen eenvoudige opgave, en de instructies voor de RDP zijn vreemd en ingewikkeld. Programmeren op "bare metal" is vrijwel uitgesloten. In recente jaren is Nintendo's officiële “libultra” op internet beland, maar het gebruik ervan zou het risico op een auteursrechtelijke rechtszaak met zich meebrengen.
Gelukkig heeft de N64-homebrew scene de laatste jaren veel vaart gemaakt en hebben we nu een zeer capabel alternatief: Libdragon. Libdragon is in feite SDL voor de N64. Het biedt faciliteiten voor het tekenen van sprites en driehoeken, geluidsoutput, controller-input en veel meer.
Het kostte me slechts een paar avonden om een nieuwe platform backend voor high_impact te bouwen bovenop Libdragon. Ik heb dit getest met Biolab Disaster. De gamecode bleef ongewijzigd; de prestaties waren matig, maar dat kwam omdat ik de N64-hardware op de meest naïeve manier mogelijk gebruikte.
Ontwikkelomgeving
Libdragon biedt de compilers en alles wat nodig is om een ROM-bestand voor de N64 te bouwen. De installatie-instructies en overige documentatie zijn uitgebreid en goed geschreven, en de bibliotheek bevat veel voorbeelden om mee te beginnen. Over het algemeen was het een plezier om met Libdragon te werken. Een waarschuwing: je wilt waarschijnlijk de preview branch gebruiken, aangezien de “stable” trunk branch hopeloos achterloopt.
Voor tests is een goede emulator onmisbaar. Lange tijd was N64-emulatie extreem onnauwkeurig, vooral door gebrekkige emulatie van de RSP- en RDP-coprocessoren. De meeste emulators emuleerden simpelweg Nintendo's platformbibliotheek, libultra; ze emuleerden de intentie om een driehoek te tekenen, niet wat de hardware daadwerkelijk zou doen. Hoewel onnauwkeurig, maakte dit emulatie in de begindagen überhaupt mogelijk. Beroemd is UltraHLE (“Ultra High Level Emulator”), die werd uitgebracht tijdens de levensduur van de N64 en veel hoofdpijn en rechtszaken veroorzaakte.
Tegenwoordig komt de N64-core in Ares veel dichter bij de werkelijke hardware – de RDP en RSP worden volledig geëmuliseerd, inclusief nauwkeurige timing voor de RSP. De beruchte trage geheugenbandbreedte van de N64 kan echter nog steeds alleen op echte hardware worden getest.
Je hebt dus een echte N64 nodig en een cartridge waarmee je willekeurige .z64 ROM-bestanden kunt spelen. De open-source SummerCart64 is uitstekend en verkrijgbaar bij verschillende fabrikanten (let op: sommige fabrikanten, vooral op AliExpress, bezuinigen op de componenten van de printplaat).
SummerCart64 heeft het gebruikelijke SD-kaartslot voor ROM's, maar wat het geweldig maakt voor ontwikkeling is de USB-C poort: je kunt hem rechtstreeks aansluiten op je pc en een ROM uploaden als onderdeel van je build-proces met sc64deployer.
Ik heb de N64 naast mijn pc gezet, verbonden via USB, en gebruikte een goedkope analoge USB-capturecard van $10 om de video-output in een venster op mijn desktop weer te geven. Op Linux kostte het wat gepruts met mpv om een output met lage latentie te krijgen. Met deze setup was itereren op echte hardware slechts een kwestie van compileren en op de resetknop van de N64 drukken.
Het Spel
Oorspronkelijk maakte ik Xibalba in 2014 als demo voor mijn JavaScript-game-engine. WebGL was toen nog het nieuwe grote ding; een 3D-game in een browser was destijds heel bijzonder. De game was erg kort, met slechts een handvol levels, wapens en vijandtypen.
In tegenstelling tot de demo wilde ik dat Xibalba 64 een echt spel zou zijn. Daarom moest ik het spel niet alleen porteren naar C en high_impact, maar het ook uitbreiden met meer levels, vijanden en wapens.
high_impact is een 2D-game-engine, maar Xibalba 64 is duidelijk 3D. Nou ja, niet helemaal. Omdat het spel geen hoogteverschillen heeft, kan het grotendeels als 2D worden behandeld. Conceptueel zou je Xibalba 64 vanuit een 2D top-down perspectief kunnen spelen; de physics, beweging en schieten zouden op precies dezelfde manier werken. In dat opzicht lijkt het spel sterk op Wolfenstein 3D.
Veel physics-functies van highimpact verwachten een vec2t argument met .x en .y componenten. Maar voor het tekenen had ik absoluut een 3D-positie nodig, dus heb ik deze definitie voor een vec3t type bedacht en het entityt type aangepast:
typedef struct {
float x, y;
} vec2_t;
typedef union {
vec2_t xy;
struct {
float x, y, z;
};
} vec3_t;
typedef struct {
// ...
vec3_t pos;
vec3_t vel;
// ...
} entity_t;
Nu kan ik, wanneer ik een functie moet aanroepen die een vec2t accepteert, "gratis" converteren vanuit vec3t:
trace_t res = trace(collision_map, entity->pos.xy, entity->vel.xy);
Omdat de innerlijke vec3_t struct “anoniem” is, kan ik nog steeds alle waarden direct benaderen; entity->pos.z werkt dus gewoon prima.
De initiële port van de bestaande levels en vijanden verliep soepel en was in ongeveer twee weken klaar. Daarna besteedde ik enkele maanden aan het uitbreiden van het spel en het optimaliseren van de renderer. De meeste functies van Libdragon pasten natuurlijk in een nieuwe platform- en rendering backend, hoewel ik sommige delen van high_impact moest aanpassen om de mixer (Libdragon heeft zijn eigen RSP-versnelde mixer) en image loader te omzeilen.
Gedurende het hele proces behield ik de mogelijkheid om het spel te bouwen met de SDL2- of Sokol-backends. Dit was ideaal voor het testen van de game-logica en het gedrag van vijanden. Voor het maken van levels implementeerde ik een eenvoudig hot-reload mechanisme dat werd geactiveerd zodra een levelfile veranderde.
De level editor, meegeleverd met high_impact, is één enkel HTML-bestand. Ik heb deze behoorlijk uitgebreid om betere ondersteuning te bieden voor lightmaps, het tonen van werkelijke sprites voor entiteiten (in plaats van alleen blokken), en het toevoegen van beschrijvingen voor entity-instellingen. De enige bron van waarheid blijft de C-broncode – de level editor leest deze en extraheert automatisch de entity-types en ondersteunde instellingen.
Aangezien de level editor nog steeds met JSON-bestanden werkt, heb ik een kleine map-compiler gebouwd die JSON leest en binaire data produceert. Hoewel het laden van JSON op de N64 mogelijk is, voegde het onnodig ongeveer 100 ms aan laadtijd toe. Tijdens het build-proces wordt elk JSON-levelfile dus geconverteerd naar een struct die er in essentie zo uitziet:
typedef struct {
uint16_t magic;
uint16_t entities_len;
uint16_t map_width;
uint16_t map_height;
struct {
uint16_t type_id;
uint16_t x;
uint16_t y;
uint16_t settings_len;
struct {
uint16_t setting_type; // zoals "name", "target", "size", ...
union {
float16_t float_value;
int16_t int_value;
struct {
int16_t string_len;
char string_value;
};
} value;
} settings[settings_len];
} entities[entities_len];
uint16_t collision_map[map_width * map_height];
uint16_t floor_map[map_width * map_height];
uint16_t wall_map[map_width * map_height];
uint16_t ceiling_map[map_width * map_height];
uint16_t light_map[map_width * map_height];
} level_t;
De level-compiler schrijft deze waarden in big-endian formaat voor de N64 en little-endian formaat voor x86 (SDL2, Sokol, WASM), zodat we alles op alle platforms kunnen lezen zonder byte swapping.
Rendering
Libdragon heeft zelf een functie voor het tekenen van driehoeken: rdpq_triangle() plaatst een enkele draw call in de RDP-wachtrij. Hoewel dit werkt, is het beter om je draw calls naar de RSP te sturen, custom microcode te gebruiken voor transformaties, belichting en diepteberekeningen, en vervolgens de RSP de RDP te laten instrueren om de driehoek daadwerkelijk te tekenen.
De complexiteit van de RDP en RSP was nieuw voor mij, maar gelukkig regelt Tiny3D, een andere uitstekende open-source bibliotheek, dit alles met een eenvoudige API. Iets op het scherm krijgen was het makkelijke deel; het performant maken was een heel ander verhaal.
De N64 heeft berucht slechts 4 KB aan texturegeheugen. De grootste textures die je kunt uploaden zijn slechts 64×64 pixels. Erger nog is dat de latentie voor een texture-upload verschrikkelijk is. Een oplossing, gebruikt door Mario 64 en veel andere titels, is om ongetextureerde polygonen te renderen waar mogelijk.
Dat paste niet bij de stijl van mijn spel, dus moest ik heel zorgvuldig zijn met de tekenvolgorde van level-tiles om texture-uploads te minimaliseren. Bovendien kan Tiny3D tot 17 quads tegelijk laden en indienen. Daarom verzamelde ik batches driehoeken met dezelfde texture in 64-bit draw calls:
typedef union render_call {
uint64_t packed;
uint32_t hashable;
uint64_t ident : 46;
struct {
uint64_t translucent : 1;
uint64_t texture_index : 9;
uint64_t x : 10;
uint64_t y : 10;
uint64_t w : 8;
uint64_t h : 8;
uint64_t vbi : 14;
uint64_t len : 4;
};
} render_call_t;
Hier is vbi de bijbehorende vertex buffer index, en len het aantal quads in deze call. Omdat elke call slechts 64 bits breed is, kunnen we ze aan het einde van het frame efficiënt sorteren en doorgeven aan Tiny3D.
Voordat ik dit kon doen, moest ik uitzoeken welke delen van een level daadwerkelijk zichtbaar zijn. De oorspronkelijke JavaScript Xibalba gebruikte een portaalsysteem, waarbij elk level werd verdeeld in sectoren. Dit werkte redelijk, maar produceerde meer overdraw dan ik wilde.
Ik koos daarom voor een andere aanpak: raycasting. Het spel schiet 320 stralen de scène in, die het volledige gezichtsveld bestrijken. Elke straal markeert de doorkruiste tiles in een bitmap voor verzending naar de renderer. Later optimaliseerde ik dit door het gezichtsveld recursief te verdelen totdat twee stralen dezelfde tile raakten. In dit proces controleer ik ook of tiles een plafond missen; zo ja, dan moet er een skybox getekend worden.
Leuk feitje: de skybox in Xibalba 64 is slechts één enkele texture van 32×32 pixels die prachtig over de horizon wordt uitgesmeerd.
Als verdere optimalisatie heb ik elke tile sheet die niet in één upload paste, in één kolom geplaatst en geprobeerd om waar mogelijk 4-bit geïndexeerde kleuren te gebruiken. Minder kleuren zorgden ervoor dat er meer pixels in het texturegeheugen pasten, en de kolomindeling zorgde ervoor dat elke tile als één continu stuk geheugen kon worden geüpload.
Dankzij al dit werk draait het spel op een stabiele 60 FPS. Iets wat niet veel andere N64-games kunnen claimen! De four-player split-screen modus haalt niet altijd de 60 FPS, maar blijft vloeiend. Ter vergelijking: GoldenEye 007 zakte in die modus vaak naar een frame rate in de enkelvoudige cijfers.
Geluid & Muziek
Zoals bij al mijn andere spellen heeft mijn goede vriend Andreas Lösch prachtige muziek geproduceerd. Het spel bevat ook een ingebouwde muziekspeler die je kunt ontgrendelen in de singleplayer-campagne.
Cartridge-ruimte is schaars, en zelfs gecomprimeerde audio is meestal te groot of te duur om te decoderen. In een heroïsche inspanning heeft Giovanni Bajo – een van de maintainers van Libdragon en een absolute toovenaar als het gaat om alles wat met N64 te maken heeft – een RSP-versnelde Opus-decoder geïmplementeerd. Ter context: Opus is een audiocodec die pas in 2012 werd gepubliceerd, zestien jaar na de N64. Het is een wonder dat het überhaupt werkt, maar helaas is het nog steeds computationeel te duur voor gebruik tijdens het gamen. (Terzijde: Giovanni Bajo implementeerde ook een real-time H.264 decoder voor de N64).
De betere optie is momenteel een eenvoudig 4-bit VADPCM-formaat dat Libdragon transparant decodeert op de RSP tijdens het afspelen. De compressieratio is niet geweldig, maar het is veel beter dan ongecomprimeerde WAV. Ongeveer 31 MB van de 32 MB ROM wordt gebruikt door geluid en muziek.
Na de release van Xibalba 64 ben ik begonnen met het implementeren van een ander audio-compressieformaat dat de afweging tussen ruimte, kwaliteit en complexiteit beter aanpakt. Meer daarover in het volgende blogbericht!
Publicatie & Verkoop
Modretro maakte eerder een Game Boy Color-kloon – de Chromatic – die compatibel was met alle bestaande Game Boy- en Game Boy Color-games. Ze waren zeer enthousiast om ook nieuwe games van hobby-ontwikkelaars uit te brengen. Toen ze de M64 aankondigden, dacht ik dat dit een geweldige kans zou zijn.
Zodra ik een werkend prototype had en er vertrouwen in had dat ik het hele spel goed kon bouwen, stuurde ik een e-mail naar het algemene klantenservice-adres van Modretro. Tot mijn verbazing kreeg ik binnen een dag reactie van het hoofd publicatie. De bureaucratie was minimaal en het contract was rechttoe rechtaan. Ik vroeg om enkele wijzigingen zodat ik de game engine later als open source zou kunnen uitbrengen, waar Modretro mee akkoord ging.
Natuurlijk waren er vertragingen en ontving ik pas laat in het ontwikkelproces een pre-productie M64. Dat maakte echter niet uit – de M64 werkte zoals beloofd; er waren geen aanpassingen aan het spel nodig voor de M64.
Modretro bood ook hulp aan met de box art, maar een andere vriend van mij wilde dit graag doen. Hij vertelde me later dat hij in de jaren '90 verantwoordelijk was voor de verpakkingen van veel titels die door Sierra in Duitsland werden uitgebracht, waaronder Half-Life. Het is dus geen verrassing dat de box art van Xibalba 64 geweldig is geworden.
Voor de handleiding leverde ik teksten en illustraties aan Modretro, en zij verzorgden de lay-out voor drukwerk. Alles verliep soepel.
Ik kan niet in detail praten over verkoopcijfers of mijn contract met Modretro, en het spel is pas een paar dagen geleden uitgebracht, maar tot nu toe ziet het er goed uit. Natuurlijk zul je waarschijnlijk niet je baan kunnen opzeggen door N64-games te maken, maar het lijkt erop dat het genoeg oplevert voor een paar mooie vakanties.
Enorme dank aan Giovanni Bajo van Libdragon, Max Bebök van Tiny3D, de gehele N64brew Discord community en natuurlijk het Modretro-team voor het mogelijk maken hiervan!
Bronnen
Als je wilt beginnen met N64-ontwikkeling, kunnen deze bronnen helpen:
- n64.dev – een collectie van alles wat gerelateerd is aan N64-ontwikkeling.
- N64brew Discord – een zeer vriendelijke en behulpzame community; veel library-ontwikkelaars zijn hier aanwezig.
- Libdragon – de systeembibliotheek voor de N64.
- Tiny3D – een eenvoudige en snelle 3D-graphics bibliotheek.
- Pyrite64 – een visuele editor en runtime voor het maken van 3D-games.
Groetjes,