Le développement de jeux
Au tout début

Le développement de jeux
Après cette présentation










Avec






Avec







@yostane


#TechAtWorldline
Yassine
Benabbas
DevRel / Enseignant / membre du LAUG
Amateur de jeux rétro
😍 Kotlin, WASM, AI, ...



Narrateur
blog.worldline.tech

@TechAtWorldline
@techatworldline.bsky.social

@worldlinetech










WebAssembly (WASM)
🔟 Format d'instruction binaire portable
🧑🧑🧒🧒 Standard W3C
🛠️ Exécutable partout (web, desktop, etc.)

00 61 73 6d 01 00 00 00 01 05 01 60 00 01 7f 03 |.asm.......`....|
02 01 00 07 16 01 12 67 65 74 55 6e 69 76 65 72 |.......getUniver|
73 61 6c 4e 75 6d 62 65 72 00 00 0a 06 01 04 00 |salNumber.......|
41 2a 0b 00 0a 04 6e 61 6d 65 02 03 01 00 00 |A*....name.....|
(module
(func (result i32)
(i32.const 42)
)
(export "getUniversalNumber" (func 0))
)
00 61 73 6d 01 00 00 00 01 05 01 60 00 01 7f 03 |.asm.......`....|
02 01 00 07 16 01 12 67 65 74 55 6e 69 76 65 72 |.......getUniver|
73 61 6c 4e 75 6d 62 65 72 00 00 0a 06 01 04 00 |salNumber.......|
41 2a 0b 00 0a 04 6e 61 6d 65 02 03 01 00 00 |A*....name.....|
format wat
Format texte pour une lecture facile
https://github.com/WebAssembly/wabt
Navigateur


runtime
WASM
Moteur JS


Basé sur: https://wasmlabs.dev/articles/docker-without-containers/
WASM dans le web
Code source


...
Compilateur / Toolchain WASM


wasm-pack
...


Binaire WASM
Code glue


Jouer au jeux vidéos
Réaliser un "side-project" en WASM










.NET et le WASM
.NET : Framework Open Source de dév. d'apps en C#
Mulit-plateformes et Fullstack
WASM est une cible de .Net :
- Blazor: framework front
- wasm-tools: compilation vers WASM


@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount() { currentCount++; }
}
Blazor


Framework de développement d'applications web
HTML + CSS + C# (à la place du JS / TS)
Rendu côté serveur ou client (via WASM)
public partial class MyClass
{
[JSExport]
internal static string Greeting()
{
var text = $"Hello, {GetHRef()}";
Console.WriteLine(text);
return text;
}
[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();
}
setModuleImports('main.js', {
window: {
location: {
href: () => globalThis.window.location.href
}
}
});
const text = exports.MyClass.Greeting();
console.log(text);
.NET 7+
wasm-tools


- Installer .net
- Installer l'outillage WASM :
$ sudo dotnet workload install wasm-tools wasm-experimental
- Créer un projet dotnet + WASM :
$ dotnet new wasmbrowser
-
Lancer le serveur web de développement :
$ dotnet run
- Tuto: https://www.youtube.com/watch?v=OHu0GpczOT8
Démo: web app .Net + WASM
Démo: web app .Net + WASM
Il faut que porte un jeu codé en .NET vers le navigateur !











Portage de jeux
Développer un jeu pour d'autres plateformes
Réécriture partielle ou complète du code source d'origine
Pas du portage: machine virtuelle ou emulateur
YouTubeur MVG

Sorti en 1993 sur DOS


🌟 Doom est facilement portable par conception🌟
+

Moteur
Ressources

sf2

wad

http://mrglitchsreviews.blogspot.com/2012/09/doom-console-ports.html
DOOM a été porté sur plusieurs platformes



25 official licensed ports https://www.thegamer.com/doom-how-many-platforms-ports-consoles


https://www.link-cable.com/top-10-weird-doom-ports/



Portage d'Olivier Poncet
ManagedDoom
id-Software/DOOM

sinshu/managed-doom
yostane/MangedDoom-Blazor














Stratégie de portage
- Cloner sininshu/managed-doom
- Changer la cible de compilation vers WASM
- Réimplémenter les classes SFML :
- Par des bouchons jusqu'à ce que ça compile
- Implémenter les bouchons
- Faire le minimum de traitements côté JS (rendu)





❌
using SFML.Audio;
namespace ManagedDoom.Audio
{
public sealed class SfmlSound : ISound, IDisposable
{
private SoundBuffer[] buffers;
}
}
Exemple de classe qui dépend de SFML
Non disponible
Le namespace SFML.Audio
La classe SFML SoundBuffer
namespace SFML.Audio
{
public class SoundBuffer
{
public SoundBuffer(short[] samples, int v, uint sampleRate)
{
// TODO: implement
}
internal void Dispose()
{
// TODO: implement
}
}
}
Alors, implémentons là
namespace SFML.Audio
{
public class SoundBuffer
{
public short[] samples;
private int v;
public uint sampleRate;
public SoundBuffer(short[] samples, int v, uint sampleRate)
{
this.samples = samples;
this.v = v;
this.sampleRate = sampleRate;
}
public Time Duration { get; internal set; }
}
}
Implémentation finale
Boucle de jeu en C#
while (waitForNextFrame()){
const input = getPlayerInput();
const { frame, audio }
= UpdateGameState(input);
render(frame);
play(audio);
}
waitForNextFrame() à remplacer par requestAnimationFrame() en JS qui utilise une callback
Boucle de jeu en JS
function gameLoop(){
if (canAdvanceFrame()){
const input = getPlayerInput();
const { frame, audio }
= UpdateGameState(input);
render(frame);
play(audio);
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
Architecture de ManagedDoom V1
Boucle for (boucle de jeu)
Touches appuyées (keyup)
Touches relâchées (keydown)
['Z', 'Q']
['space']
Frame buffer
Audio buffer
SFML Audio
SFML Video



1 iteration du Doom Engine
Mise à jour de l'état du jeu


.NET WASM
~70% OK
.NET WASM

wad
DOOM.wad

sf2
SoundFont.sf2
Architecture de Blazor Doom

wad
DOOM.wad

requestAnimationFrame
Appel .Net / WASM
Audio buffer
Frame buffer
Canvas


Audio Context

Appel JS
Appel JS


sf2
SoundFont.sf2
Touches appuyées (keyup)
Touches relâchées (keydown)
['Z', 'Q']
['space']
1 iteration du Doom Engine
Mise à jour de l'état du jeu













<html>
<head>
<!-- Sets .Net interop and starts the game loop -->
<script type="module" src="./main.js"></script>
</head>
<body>
<canvas id="canvas" width="320" height="200"
style="image-rendering: pixelated" />
</body>
</html>
Point d'entrée
Exécution de la boucle de jeu
main.js
import { dotnet } from "./dotnet.js";
const { getAssemblyExports, getConfig } = await dotnet.create();
const exports = await getAssemblyExports(getConfig().mainAssemblyName);
await dotnet.run();
function gameLoop(timestamp) {
if (timestamp - lastFrameTimestamp >= 1000 / 30) {
lastFrameTimestamp = timestamp;
exports.BlazorDoom.MainJS.UpdateGameState(keys);
}
requestAnimationFrame(gameLoop);
}

main.js
public partial class MainJS // this name is required
{
public static void Main()
{
app = new ManagedDoom.DoomApplication();
}
[JSExport] // Can be imported from JS
public static void UpdateGameState(int[] keys)
{ // computes the next frame and sounds
managedDoom.UpdateGameState(keys);
}
}

import { dotnet } from "./dotnet.js";
const { getAssemblyExports, getConfig } = await dotnet.create();
const exports = await getAssemblyExports(getConfig().mainAssemblyName);
await dotnet.run();
function gameLoop(timestamp) {
if (timestamp - lastFrameTimestamp >= 1000 / 30) {
lastFrameTimestamp = timestamp;
exports.BlazorDoom.MainJS.UpdateGameState(keys);
}
requestAnimationFrame(gameLoop);
}

Exécution de la boucle de jeu
Audio

0.5 | 1 | 0.75 | 0 | -0.75 | -1 | -0.5 | 0 |
---|
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Audio_concepts
AudioContext
Echantillon
1 / fréquence d'échantillonage
+ Fréquence d'échantillonage



🔊
Gestion des bruitages
void PlayCurrentFrameSound(SoundBuffer soundBuffer)
{
int[] samples = Array.ConvertAll(soundBuffer.samples, Convert.ToInt32);
BlazorDoom.Renderer.playSoundOnJS(samples, (int)soundBuffer.sampleRate);
}

namespace BlazorDoom
{
[SupportedOSPlatform("browser")]
public partial class Renderer
{
[JSImport("playSound", "blazorDoom/renderer.js")]
internal static partial string playSoundOnJS(
int[] samples,
int sampleRate
);
}
}

Gestion des bruitages
export function playSound(samples, sampleRate) {
audioContext = new AudioContext({
sampleRate: sampleRate,
});
const length = samples.length;
const audioBuffer = audioContext.createBuffer(
1,
length,
sampleRate
);
var channelData = audioBuffer.getChannelData(0);
for (let i = 0; i < length; i++) {
// normalize the sample to be between -1 and 1
channelData[i] = samples[i] / 0xffff;
}
var source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
}

Musique

SF2: format qui définit quel son que doit émettre chaque note
Note 0
Note 1
Streaming de la musique


sf2

Temps
Temps
Streaming de la musique


Temps
Temps
AudioContext 😢
émet des glitchs sur des petits extraits
+ ne gère pas le streaming nativement
Solution 🎶🥳
Regrouper les extraits en un tampon assez grand
+ planifier le moment de lancement

sf2
Streaming de la musique
function playMusic(samples) {
// On lit le tampon audio s'il est assez rempli
if (this.#currentMusicBufferIndex >= this.#musicBuffer.length) {
const currentTime = this.#audioContext.currentTime;
const duration = this.#currentMusicBufferIndex / this.musicSampleRate;
// On planifie l'extrait pour qu'il se lance après le précédent
source.start(this.expectedBufferEndTime, 0, duration);
this.expectedBufferEndTime = currentTime + duration;
}
// Remplissage du tampon avec l'extrait courant
for (let i = 0; i < samples.length; i++) {
this.#currentChannelData[this.#currentMusicBufferIndex + i]
= samples[i] / 32767;
}
this.#currentMusicBufferIndex += samples.length;
}
Inconvénient : la musique se lance avec un retard
(le temps de remplir le premier tampon)
0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
---|
0 | 1 | 2 | 3 |
---|
Rendu des images
tableau à 1 dimension + palette de couleurs
2 | 2 | 0 | 1 |
---|
Image
Palette de couleurs
Canvas


chaque pixel contient l'id de la couleur
Comment seront répartis les pixels ?

➡️ puis ⬇️

La technique du raycasting
source: raycasting by MeTH
1- Envoi de rayons répartis uniformément sur le champ de vision du personnage

La technique du raycasting
source: raycasting by MeTH
1- Envoi de rayons répartis uniformément sur le champ de vision du personnage
2 - Chaque rayon génère une colonne de l'image selon la distance de colision
3 - Donc, l'image est construite colonne par colonne, ou bien de haut en bas, puis de gauche à droite
0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
---|
0 | 1 | 2 | 3 |
---|
Rendu des images
tableau à 1 dimension + palette de couleurs
2 | 2 | 0 | 1 |
---|
Image
Palette de couleurs
Canvas
Pixels distribués de haut en bas, puis de gauche à droite


chaque pixel contient l'id de la couleur
Transmission des pixels du C# au JS
namespace BlazorDoom
{
[SupportedOSPlatform("browser")]
public partial class Renderer
{
[JSImport("drawOnCanvas", "blazorDoom/renderer.js")]
internal static partial string renderOnJS(byte[] screenData,
int[] colors);
}
}
export function drawOnCanvas(screenData, colors) {
//
}
public class DoomRenderer
{
// Called by updateGameState
private void Display(uint[] colors)
{
BlazorDoom.Renderer.renderOnJS(screen.Data, (int[])((object)colors));
}
}



Remplissage du canvas
export function drawOnCanvas(screenData, colors) {
const context = getCanvas().context;
const imageData = context.createImageData(320, 200);
let y = 0;
let x = 0;
for (let i = 0; i < screenData.length; i += 1) {
const dataIndex = (y * width + x) * 4;
setSinglePixel(imageData, dataIndex, colors, screenData[i]);
if (y >= height - 1) {
y = 0;
x += 1;
} else {
y += 1;
}
}
context.putImageData(imageData, 0, 0);
}
function setSinglePixel(imageData, dataIndex, colors, colorIndex) {
const color = colors[colorIndex];
imageData.data[dataIndex] = color & 0xff; // R
imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G
imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B
imageData.data[dataIndex + 3] = 255; // Alpha
}










Fire / Tirer
Valider / validate
Open / Ouvrir
Se déplacer
Sélectionner un autre WAD
Le code C#, le runtime et les librairies .Net sont compilées en WASM









∞ Les possibilités du WASM sont infinies
💪 Le portage est accessible à tous
🎮 Le dév de jeu est un bon moyen pour apprendre tout en s'amusant

Liens et crédits
https://mspoweruser.com/this-doom-digital-camera-source-port-is-amazing-and-bizarre/
https://www.link-cable.com/top-10-weird-doom-ports/
pixabay.com
https://www.flaticon.com/ users: freepik
Doom text generator: https://c.eev.ee/doom-text-generator
https://www.pngkey.com/download/u2q8r5o0q8y3w7t4_doom-guy-grin/
https://www.pixilart.com/art/doom-guy-3e8e22def04259e
https://imgflip.com/memetemplate/65645030/detective-Doom-guy
https://imgflip.com/gif/9z8us3
https://imgflip.com/memetemplate/375393786/Doom-Laptop
https://openprocessing.org/sketch/1051403
https://freedesignfile.com/771822-crt-monitor-clipart/
https://www.flaticon.com/free-icon/curtains_1864805

@yostane

Yassine Benabbas



Présenté par

#TechAtWorldline
Remeciements spéciaux
Avec




WebAssembly alias WASM
.Net
JavaScript alias JS
The doom guy

Liens et crédits
https://mspoweruser.com/this-doom-digital-camera-source-port-is-amazing-and-bizarre/
https://www.link-cable.com/top-10-weird-doom-ports/
pixabay.com
https://www.flaticon.com/ users: freepik
Doom text generator: https://c.eev.ee/doom-text-generator
https://www.pngkey.com/download/u2q8r5o0q8y3w7t4_doom-guy-grin/
https://www.pixilart.com/art/doom-guy-3e8e22def04259e
https://imgflip.com/memetemplate/65645030/detective-Doom-guy
https://imgflip.com/gif/9z8us3
https://imgflip.com/memetemplate/375393786/Doom-Laptop
https://openprocessing.org/sketch/1051403
https://freedesignfile.com/771822-crt-monitor-clipart/
https://www.flaticon.com/free-icon/curtains_1864805

@yostane

Yassine Benabbas



Présenté par

#TechAtWorldline
Remeciements spéciaux
Avec




WebAssembly alias WASM
.Net
JavaScript alias JS
The doom guy





Veuillez donner votre avis
Diapositives



[RivieraDev 2025] Comment j'ai porté Doom sur navigateur grâce au WebAssembly
By yostane
[RivieraDev 2025] Comment j'ai porté Doom sur navigateur grâce au WebAssembly
Web Assembly (WASM) is a powerful technology that opens the door to unlimited development possibilities. As a video game enthusiast, I used it to port the Doom game to the browser, allowing me to play it anywhere, even on my mobile. This is made possible thanks to the availability of an Open Source port of Doom to .Net and its support for compilation to WASM. This tools-in-action session will show you how I ported the MangedDoom game, which is made in .Net, to run in a browser. I will also share my experience on carrying out this port. You'll be surprised to see that this kind of project is very accessible as well as captivating, especially when you see the game running on a mobile browser. Although my work is based on .Net's WASM tooling, it can be applied to any framework that targets WASM. So, come and relive this fun porting adventure with me 👍.
- 85