Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[TnT 2025] Comment j'ai porté Doom sur navigate...

[TnT 2025] Comment j'ai porté Doom sur navigateur grâce au Web Assembly

Le portage de jeux vidéos permet de découvrir de nouvelles technologies tout en s'amusant. En plus, c'est un excellent point d'entrée dans le monde du développement de jeux vidéos. Cela est d'autant plus vrai grâce au Web Assembly qui simplifie le portage de jeux sur navigateur.

Durant cette session, je partage mon retour d'expérience sur la réalisation de mon premier portage. Je vous expliquerai pourquoi j'ai choisi de porter Doom. Ensuite, je montrerai étape par étape comment j'ai réussi à intégerer le Canvas et l'AudioContext en JavaScript pour avoir un rendu qui peut aller jusqu'à 60 fps.

A l'issue de cette conférence, vous ne verrez plus le portage de jeux vidéos de la même manière. Donc, si vous êtes passionnés par les jeux vidéos ou que vous souhaitez découvrir comme le WASM contribue aux portage de jeux, cette session est faite pour vous.

yostane

April 09, 2025
Tweet

More Decks by yostane

Other Decks in Programming

Transcript

  1. @yostane #TechAtWorldline Yassine Benabbas DevRel / Enseignant / membre du

    LAUG Amateur de jeux rétro 😍 Kotlin, WASM, AI, ...
  2. WebAssembly (WASM) Format d'instruction binaire portable Standard W3C Exécutable par

    les navigateurs et environnements non-Web 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.....| 1 2 3 4
  3. 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.....| 1 2 3 4 Format texte pour une lecture facile
  4. (module (func (result i32) (i32.const 42) ) (export "getUniversalNumber" (func

    0)) ) 1 2 3 4 5 6 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.....| 1 2 3 4 format wat Format texte pour une lecture facile https:/ /github.com/WebAssembly/wabt
  5. Navigateur Code source Moteur JS Basé sur: https://wasmlabs.dev/articles/docker-without-containers/ WASM dans

    le web Compilateur / Toolchain WASM wasm-pack ... ... Binaire WASM Code glue
  6. Navigateur Code source runtime WASM Moteur JS Basé sur: https://wasmlabs.dev/articles/docker-without-containers/

    WASM dans le web Compilateur / Toolchain WASM wasm-pack ... ... Binaire WASM Code glue
  7. .NET et le WASM .NET : Framework Open Source de

    dév. d'apps Mulit-plateformes et Fullstack WASM est une cible de .Net :
  8. .NET et le WASM .NET : Framework Open Source de

    dév. d'apps Mulit-plateformes et Fullstack WASM est une cible de .Net : Blazor: framework front wasm-tools: compilation vers WASM
  9. @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++; } } 1 2 3 4 5 6 7 8 9 10 Blazor Framework de développement d'applications web HTML + CSS + C# (à la place du JS / TS) Rendu côté serveur ou client (via WASM)
  10. 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 setModuleImports('main.js', { window: { location: { href: () => globalThis.window.location.href } } }); const text = exports.MyClass.Greeting(); console.log(text); 1 2 3 4 5 6 7 8 9 10 .NET 7+ wasm-tools
  11. 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 setModuleImports('main.js', { window: { location: { href: () => globalThis.window.location.href } } }); const text = exports.MyClass.Greeting(); console.log(text); 1 2 3 4 5 6 7 8 9 10 .NET 7+ wasm-tools
  12. 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 setModuleImports('main.js', { window: { location: { href: () => globalThis.window.location.href } } }); const text = exports.MyClass.Greeting(); console.log(text); 1 2 3 4 5 6 7 8 9 10 .NET 7+ wasm-tools
  13. Portage de jeux Pouvoir lancer un jeu sur d'autres plateformes

    Réécriture partielle ou complète du code source d'origine Pas du portage: machine virtuelle ou emulateur YouTubeur MVG
  14. Sorti en 1993 sur DOS 🌟 Doom est facilement portable

    par conception 🌟 Moteur + wad Ressources sf2
  15. ManagedDoom Port en .NET de ManagedDoom V1 utilise (V2 utilise

    ) Mon portage est un fork de ManagedDoom V1 LinuxDoom SFML silk.net
  16. ManagedDoom Port en .NET de ManagedDoom V1 utilise (V2 utilise

    ) Mon portage est un fork de ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM
  17. ManagedDoom Port en .NET de ManagedDoom V1 utilise (V2 utilise

    ) Mon portage est un fork de ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM sinshu/managed-doom
  18. ManagedDoom Port en .NET de ManagedDoom V1 utilise (V2 utilise

    ) Mon portage est un fork de ManagedDoom V1 LinuxDoom SFML silk.net id-Software/DOOM sinshu/managed-doom yostane/MangedDoom-Blazor
  19. Stratégie de portage Cloner sininshu/managed-doom Changer la cible de compilation

    vers WASM Réimplémenter les classes SFML : 1. Par des bouchons jusqu'à ce que ça compile dall-e
  20. Stratégie de portage Cloner sininshu/managed-doom Changer la cible de compilation

    vers WASM Réimplémenter les classes SFML : 1. Par des bouchons jusqu'à ce que ça compile 2. Implémenter les bouchons dall-e
  21. Stratégie de portage Cloner sininshu/managed-doom Changer la cible de compilation

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

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

    vers WASM Réimplémenter les classes SFML : 1. Par des bouchons jusqu'à ce que ça compile 2. Implémenter les bouchons 3. Faire le minimum de traitements côté JS (rendu) dall-e
  24. using SFML.Audio; namespace ManagedDoom.Audio { public sealed class SfmlSound :

    ISound, IDisposable { private SoundBuffer[] buffers; } } 1 2 3 4 5 6 7 8 9 Exemple de classe qui dépend de SFML
  25. using SFML.Audio; namespace ManagedDoom.Audio { public sealed class SfmlSound :

    ISound, IDisposable { private SoundBuffer[] buffers; } } 1 2 3 4 5 6 7 8 9 using SFML.Audio; private SoundBuffer[] buffers; 1 2 namespace ManagedDoom.Audio 3 { 4 public sealed class SfmlSound : ISound, IDisposable 5 { 6 7 } 8 } 9 Exemple de classe qui dépend de SFML Non disponible Le namespace SFML.Audio La classe SFML SoundBuffer
  26. namespace SFML.Audio { public class SoundBuffer { public SoundBuffer(short[] samples,

    int v, uint sampleRate) { // TODO: implement } internal void Dispose() { // TODO: implement } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Alors, implémentons là
  27. 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; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Implémentation finale
  28. 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; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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; namespace SFML.Audio 1 { 2 public class SoundBuffer 3 { 4 5 6 7 8 9 10 11 12 13 } 14 15 public Time Duration { get; internal set; } 16 } 17 } 18 Implémentation finale
  29. 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; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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; namespace SFML.Audio 1 { 2 public class SoundBuffer 3 { 4 5 6 7 8 9 10 11 12 13 } 14 15 public Time Duration { get; internal set; } 16 } 17 } 18 public Time Duration { get; internal set; } namespace SFML.Audio 1 { 2 public class SoundBuffer 3 { 4 public short[] samples; 5 private int v; 6 public uint sampleRate; 7 8 public SoundBuffer(short[] samples, int v, uint sampleRate) 9 { 10 this.samples = samples; 11 this.v = v; 12 this.sampleRate = sampleRate; 13 } 14 15 16 } 17 } 18 Implémentation finale
  30. Boucle de jeu en C# while (waitForNextFrame()){ const input =

    getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); }
  31. Boucle de jeu en C# while (waitForNextFrame()){ const input =

    getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } waitForNextFrame() à remplacer par requestAnimationFrame() en JS qui utilise une callback
  32. Boucle de jeu en JS function gameLoop(){ if (canAdvanceFrame()){ const

    input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); 1 2 3 4 5 6 7 8 9 10 11
  33. Boucle de jeu en JS function gameLoop(){ if (canAdvanceFrame()){ const

    input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); 1 2 3 4 5 6 7 8 9 10 11 if (canAdvanceFrame()){ const input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } function gameLoop(){ 1 2 3 4 5 6 7 8 requestAnimationFrame(gameLoop); 9 } 10 requestAnimationFrame(gameLoop); 11
  34. Boucle de jeu en JS function gameLoop(){ if (canAdvanceFrame()){ const

    input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); 1 2 3 4 5 6 7 8 9 10 11 if (canAdvanceFrame()){ const input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } function gameLoop(){ 1 2 3 4 5 6 7 8 requestAnimationFrame(gameLoop); 9 } 10 requestAnimationFrame(gameLoop); 11 function gameLoop(){ requestAnimationFrame(gameLoop); requestAnimationFrame(gameLoop); 1 if (canAdvanceFrame()){ 2 const input = getPlayerInput(); 3 const { frame, audio } 4 = UpdateGameState(input, WAD); 5 render(frame); 6 play(audio); 7 } 8 9 } 10 11
  35. Boucle de jeu en JS function gameLoop(){ if (canAdvanceFrame()){ const

    input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); 1 2 3 4 5 6 7 8 9 10 11 if (canAdvanceFrame()){ const input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } function gameLoop(){ 1 2 3 4 5 6 7 8 requestAnimationFrame(gameLoop); 9 } 10 requestAnimationFrame(gameLoop); 11 function gameLoop(){ requestAnimationFrame(gameLoop); requestAnimationFrame(gameLoop); 1 if (canAdvanceFrame()){ 2 const input = getPlayerInput(); 3 const { frame, audio } 4 = UpdateGameState(input, WAD); 5 render(frame); 6 play(audio); 7 } 8 9 } 10 11 function gameLoop(){ if (canAdvanceFrame()){ const input = getPlayerInput(); const { frame, audio } = UpdateGameState(input, WAD); render(frame); play(audio); } requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop); 1 2 3 4 5 6 7 8 9 10 11
  36. Boucle for (boucle de jeu) Touches appuyées (keyup) Touches relâchées

    (keydown) ['Z', 'Q'] ['space'] Architecture de ManagedDoom V1 1 iteration du Doom Engine Mise à jour de l'état du jeu wad DOOM.wad sf2 SoundFont.sf2
  37. Boucle for (boucle de jeu) Touches appuyées (keyup) Touches relâchées

    (keydown) ['Z', 'Q'] ['space'] Architecture de ManagedDoom V1 Frame buffer Audio buffer 1 iteration du Doom Engine Mise à jour de l'état du jeu wad DOOM.wad sf2 SoundFont.sf2
  38. Boucle for (boucle de jeu) Touches appuyées (keyup) Touches relâchées

    (keydown) ['Z', 'Q'] ['space'] Architecture de ManagedDoom V1 Frame buffer Audio buffer SFML Video SFML Audio 1 iteration du Doom Engine Mise à jour de l'état du jeu wad DOOM.wad sf2 SoundFont.sf2
  39. Boucle for (boucle de jeu) Touches appuyées (keyup) Touches relâchées

    (keydown) ['Z', 'Q'] ['space'] Architecture de ManagedDoom V1 Frame buffer Audio buffer SFML Video SFML Audio 1 iteration du Doom Engine Mise à jour de l'état du jeu wad DOOM.wad sf2 SoundFont.sf2
  40. Boucle for (boucle de jeu) Touches appuyées (keyup) Touches relâchées

    (keydown) ['Z', 'Q'] ['space'] Architecture de ManagedDoom V1 Frame buffer Audio buffer SFML Video SFML Audio 1 iteration du Doom Engine Mise à jour de l'état du jeu .NET WASM ~70% OK .NET WASM wad DOOM.wad sf2 SoundFont.sf2
  41. Architecture de Blazor Doom wad DOOM.wad requestAnimationFrame Canvas Appel JS

    Appel JS Frame buffer Audio buffer Audio Context 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 Appel .Net / WASM
  42. <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> 1 2 3 4 5 6 7 8 9 10 Point d'entrée
  43. <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> 1 2 3 4 5 6 7 8 9 10 <!-- Sets .Net interop and starts the game loop --> <script type="module" src="./main.js"></script> <html> 1 <head> 2 3 4 </head> 5 <body> 6 <canvas id="canvas" width="320" height="200" 7 style="image-rendering: pixelated" /> 8 </body> 9 </html> 10 Point d'entrée
  44. <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> 1 2 3 4 5 6 7 8 9 10 <!-- Sets .Net interop and starts the game loop --> <script type="module" src="./main.js"></script> <html> 1 <head> 2 3 4 </head> 5 <body> 6 <canvas id="canvas" width="320" height="200" 7 style="image-rendering: pixelated" /> 8 </body> 9 </html> 10 <canvas id="canvas" width="320" height="200" style="image-rendering: pixelated" /> <html> 1 <head> 2 <!-- Sets .Net interop and starts the game loop --> 3 <script type="module" src="./main.js"></script> 4 </head> 5 <body> 6 7 8 </body> 9 </html> 10 Point d'entrée
  45. Exécution de la boucle de jeu 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); } 1 2 3 4 5 6 7 8 9 10 11 12 main.js
  46. Exécution de la boucle de jeu 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 main.js
  47. Exécution de la boucle de jeu 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 function gameLoop(timestamp) { requestAnimationFrame(gameLoop); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 11 } 12 main.js
  48. Exécution de la boucle de jeu 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 function gameLoop(timestamp) { requestAnimationFrame(gameLoop); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 11 } 12 exports.BlazorDoom.MainJS.UpdateGameState(keys); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 main.js
  49. Exécution de la boucle de jeu 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); } 1 2 3 4 5 6 7 8 9 10 11 12 import { dotnet } from "./dotnet.js"; const { getAssemblyExports, getConfig } = await dotnet.create(); const exports = await getAssemblyExports(getConfig().mainAssemblyName); await dotnet.run(); 1 2 3 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 function gameLoop(timestamp) { requestAnimationFrame(gameLoop); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 exports.BlazorDoom.MainJS.UpdateGameState(keys); 9 } 10 11 } 12 exports.BlazorDoom.MainJS.UpdateGameState(keys); import { dotnet } from "./dotnet.js"; 1 const { getAssemblyExports, getConfig } = await dotnet.create(); 2 const exports = await getAssemblyExports(getConfig().mainAssemblyName); 3 await dotnet.run(); 4 5 function gameLoop(timestamp) { 6 if (timestamp - lastFrameTimestamp >= 1000 / 30) { 7 lastFrameTimestamp = timestamp; 8 9 } 10 requestAnimationFrame(gameLoop); 11 } 12 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); } 1 2 3 4 5 6 7 8 9 10 11 12 main.js
  50. 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); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 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); } 1 2 3 4 5 6 7 8 9 10 11 12 main.js Exécution de la boucle de jeu
  51. 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

    Echantillon 1 / fréquence d'échantillonage + Fréquence d'échantillonage
  52. 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
  53. 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 🔊
  54. Gestion des bruitages void PlayCurrentFrameSound(SoundBuffer soundBuffer) { int[] samples =

    Array.ConvertAll(soundBuffer.samples, Convert.ToInt32); BlazorDoom.Renderer.playSoundOnJS(samples, (int)soundBuffer.sampleRate); } 1 2 3 4 5 namespace BlazorDoom { [SupportedOSPlatform("browser")] public partial class Renderer { [JSImport("playSound", "blazorDoom/renderer.js")] internal static partial string playSoundOnJS( int[] samples, int sampleRate ); } } 1 2 3 4 5 6 7 8 9 10 11 12
  55. 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++) { // noralize 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
  56. 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++) { // noralize 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const length = samples.length; const audioBuffer = audioContext.createBuffer( 1, length, sampleRate ); export function playSound(samples, sampleRate) { 1 audioContext = new AudioContext({ 2 sampleRate: sampleRate, 3 }); 4 5 6 7 8 9 10 11 var channelData = audioBuffer.getChannelData(0); 12 for (let i = 0; i < length; i++) { 13 // noralize the sample to be between -1 and 1 14 channelData[i] = samples[i] / 0xffff; 15 } 16 17 var source = audioContext.createBufferSource(); 18 source.buffer = audioBuffer; 19 source.connect(audioContext.destination); 20 source.start(); 21 } 22
  57. 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++) { // noralize 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(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const length = samples.length; const audioBuffer = audioContext.createBuffer( 1, length, sampleRate ); export function playSound(samples, sampleRate) { 1 audioContext = new AudioContext({ 2 sampleRate: sampleRate, 3 }); 4 5 6 7 8 9 10 11 var channelData = audioBuffer.getChannelData(0); 12 for (let i = 0; i < length; i++) { 13 // noralize the sample to be between -1 and 1 14 channelData[i] = samples[i] / 0xffff; 15 } 16 17 var source = audioContext.createBufferSource(); 18 source.buffer = audioBuffer; 19 source.connect(audioContext.destination); 20 source.start(); 21 } 22 var channelData = audioBuffer.getChannelData(0); for (let i = 0; i < length; i++) { // noralize the sample to be between -1 and 1 channelData[i] = samples[i] / 0xffff; } export function playSound(samples, sampleRate) { 1 audioContext = new AudioContext({ 2 sampleRate: sampleRate, 3 }); 4 const length = samples.length; 5 const audioBuffer = audioContext.createBuffer( 6 1, 7 length, 8 sampleRate 9 ); 10 11 12 13 14 15 16 17 var source = audioContext.createBufferSource(); 18 source.buffer = audioBuffer; 19 source.connect(audioContext.destination); 20 source.start(); 21 } 22
  58. Streaming de la musique Temps Temps AudioContext 😢 émet des

    glitchs sur des petits extraits + ne gère pas le streaming nativement sf2
  59. 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
  60. 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; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Inconvénient : la musique se lance avec un retard (le temps de remplir le premier tampon)
  61. 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; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // On lit le tampon audio s'il est assez rempli if (this.#currentMusicBufferIndex >= this.#musicBuffer.length) { // Remplissage du tampon avec l'extrait courant for (let i = 0; i < samples.length; i++) { this.#currentChannelData[this.#currentMusicBufferIndex + i] = samples[i] / 32767; } function playMusic(samples) { 1 2 3 const currentTime = this.#audioContext.currentTime; 4 const duration = this.#currentMusicBufferIndex / this.musicSampleRate; 5 // On planifie l'extrait pour qu'il se lance après le précédent 6 source.start(this.expectedBufferEndTime, 0, duration); 7 this.expectedBufferEndTime = currentTime + duration; 8 } 9 10 11 12 13 14 this.#currentMusicBufferIndex += samples.length; 15 } 16 Inconvénient : la musique se lance avec un retard (le temps de remplir le premier tampon)
  62. 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; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // On lit le tampon audio s'il est assez rempli if (this.#currentMusicBufferIndex >= this.#musicBuffer.length) { // Remplissage du tampon avec l'extrait courant for (let i = 0; i < samples.length; i++) { this.#currentChannelData[this.#currentMusicBufferIndex + i] = samples[i] / 32767; } function playMusic(samples) { 1 2 3 const currentTime = this.#audioContext.currentTime; 4 const duration = this.#currentMusicBufferIndex / this.musicSampleRate; 5 // On planifie l'extrait pour qu'il se lance après le précédent 6 source.start(this.expectedBufferEndTime, 0, duration); 7 this.expectedBufferEndTime = currentTime + duration; 8 } 9 10 11 12 13 14 this.#currentMusicBufferIndex += samples.length; 15 } 16 // On planifie l'extrait pour qu'il se lance après le précédent source.start(this.expectedBufferEndTime, 0, duration); function playMusic(samples) { 1 // On lit le tampon audio s'il est assez rempli 2 if (this.#currentMusicBufferIndex >= this.#musicBuffer.length) { 3 const currentTime = this.#audioContext.currentTime; 4 const duration = this.#currentMusicBufferIndex / this.musicSampleRate; 5 6 7 this.expectedBufferEndTime = currentTime + duration; 8 } 9 // Remplissage du tampon avec l'extrait courant 10 for (let i = 0; i < samples.length; i++) { 11 this.#currentChannelData[this.#currentMusicBufferIndex + i] 12 = samples[i] / 32767; 13 } 14 this.#currentMusicBufferIndex += samples.length; 15 } 16 Inconvénient : la musique se lance avec un retard (le temps de remplir le premier tampon)
  63. 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 12 * 4 bytes (r, g, b, a) chaque pixel contient l'id de la couleur
  64. 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 12 * 4 bytes (r, g, b, a) chaque pixel contient l'id de la couleur
  65. 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 12 * 4 bytes (r, g, b, a) chaque pixel contient l'id de la couleur
  66. 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 12 * 4 bytes (r, g, b, a) chaque pixel contient l'id de la couleur
  67. 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 12 * 4 bytes (r, g, b, a) chaque pixel contient l'id de la couleur
  68. 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 12 * 4 bytes (r, g, b, a) chaque pixel contient l'id de la couleur
  69. 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 12 * 4 bytes (r, g, b, a) Pixels distribués de haut en bas puis et de gauche à droite chaque pixel contient l'id de la couleur
  70. 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); } } 1 2 3 4 5 6 7 8 9 10 export function drawOnCanvas(screenData, colors) { // } 1 2 3 public class DoomRenderer { // Called by updateGameState private void Display(uint[] colors) { BlazorDoom.Renderer.renderOnJS(screen.Data, (int[])((object)colors)); } } 1 2 3 4 5 6 7 8
  71. 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 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
  72. 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 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (y >= height - 1) { y = 0; x += 1; } else { y += 1; export function drawOnCanvas(screenData, colors) { 1 const context = getCanvas().context; 2 const imageData = context.createImageData(320, 200); 3 let y = 0; 4 let x = 0; 5 for (let i = 0; i < screenData.length; i += 1) { 6 const dataIndex = (y * width + x) * 4; 7 setSinglePixel(imageData, dataIndex, colors, screenData[i]); 8 9 10 11 12 13 } 14 } 15 context.putImageData(imageData, 0, 0); 16 } 17 18 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 19 const color = colors[colorIndex]; 20 imageData.data[dataIndex] = color & 0xff; // R 21 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G 22 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B 23 imageData.data[dataIndex + 3] = 255; // Alpha 24 } 25
  73. 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 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (y >= height - 1) { y = 0; x += 1; } else { y += 1; export function drawOnCanvas(screenData, colors) { 1 const context = getCanvas().context; 2 const imageData = context.createImageData(320, 200); 3 let y = 0; 4 let x = 0; 5 for (let i = 0; i < screenData.length; i += 1) { 6 const dataIndex = (y * width + x) * 4; 7 setSinglePixel(imageData, dataIndex, colors, screenData[i]); 8 9 10 11 12 13 } 14 } 15 context.putImageData(imageData, 0, 0); 16 } 17 18 function setSinglePixel(imageData, dataIndex, colors, colorIndex) { 19 const color = colors[colorIndex]; 20 imageData.data[dataIndex] = color & 0xff; // R 21 imageData.data[dataIndex + 1] = (color >> 8) & 0xff; // G 22 imageData.data[dataIndex + 2] = (color >> 16) & 0xff; // B 23 imageData.data[dataIndex + 3] = 255; // Alpha 24 } 25 setSinglePixel(imageData, dataIndex, colors, screenData[i]); 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 } export function drawOnCanvas(screenData, colors) { 1 const context = getCanvas().context; 2 const imageData = context.createImageData(320, 200); 3 let y = 0; 4 let x = 0; 5 for (let i = 0; i < screenData.length; i += 1) { 6 const dataIndex = (y * width + x) * 4; 7 8 if (y >= height - 1) { 9 y = 0; 10 x += 1; 11 } else { 12 y += 1; 13 } 14 } 15 context.putImageData(imageData, 0, 0); 16 } 17 18 19 20 21 22 23 24 25
  74. Fire / Tirer Valider / validate Open / Ouvrir Se

    déplacer Sélectionner un autre WAD
  75. 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