Au tout début
Après cette présentation
@yostane
#TechAtWorldline
Yassine
Benabbas
DevRel / Enseignant / membre du LAUG
Amateur de jeux rétro
😍 Kotlin, WASM, AI, ...
blog.worldline.tech
@TechAtWorldline
@techatworldline.bsky.social
@worldlinetech
🔟 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
Navigateur
runtime
WASM
Moteur JS
Basé sur: https://wasmlabs.dev/articles/docker-without-containers/
Code source
...
Compilateur / Toolchain WASM
wasm-pack
...
Binaire WASM
Code glue
.NET : Framework Open Source de dév. d'apps en C#
Mulit-plateformes et Fullstack
WASM est une cible de .Net :
@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++; }
}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);$ sudo dotnet workload install wasm-tools wasm-experimental$ dotnet new wasmbrowser$ dotnet runDé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
+
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
id-Software/DOOM
sinshu/managed-doom
yostane/MangedDoom-Blazor
❌
using SFML.Audio;
namespace ManagedDoom.Audio
{
    public sealed class SfmlSound : ISound, IDisposable
    {
        private SoundBuffer[] buffers;
    }
}namespace SFML.Audio
{
    public class SoundBuffer
    {
        public SoundBuffer(short[] samples, int v, uint sampleRate)
        {
        	// TODO: implement
        }
        internal void Dispose()
        {
            // TODO: implement
        }
    }
}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; }
    }
}while (waitForNextFrame()){
  const input = getPlayerInput();
  const { frame, audio } 
  	      = UpdateGameState(input);
  render(frame);
  play(audio);
}waitForNextFrame() à remplacer par requestAnimationFrame() en JS qui utilise une callback
function gameLoop(){
  if (canAdvanceFrame()){
    const input = getPlayerInput();
    const { frame, audio } 
  	      = UpdateGameState(input);
    render(frame);
    play(audio);
  }
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);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
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>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
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 ?
source: raycasting by MeTH
1- Envoi de rayons répartis uniformément sur le champ de vision du personnage
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
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
#TechAtWorldline
WebAssembly alias WASM
.Net
JavaScript alias JS
The doom guy
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
#TechAtWorldline
WebAssembly alias WASM
.Net
JavaScript alias JS
The doom guy