Game development

How it started

Game development

After this talk

Avec

Starring

@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

Play video games

Start a "side-project" related to 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

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

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 :
    1. Par des bouchons jusqu'à ce que ça compile
    2. Implémenter les bouchons
    3. 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

[TechForum Germany] How I ported Doom to the browser

By yostane

[TechForum Germany] How I ported Doom to the browser

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 👍.

  • 3