Game development

How it started

Game development

After this talk

Avec

Starring

@yostane

#TechAtWorldline

Yassine

Benabbas

TechSquad core team

Teacher / member of Android Lille GDG

Retro-gamer

😍 Kotlin, WASM, AI, ...

Narrator

blog.worldline.tech

@TechAtWorldline

@techatworldline.bsky.social

@worldlinetech

WebAssembly (Wasm)

🔟 Portable binary instruction format

🧑‍🧑‍🧒‍🧒 W3C standard

🛠️ Runs everywhere (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.....|

wat format

Human readable text format

https://github.com/WebAssembly/wabt

Browser

Wasm

runtime

JS engine

based on: https://wasmlabs.dev/articles/docker-without-containers/

Wasm on the Web

Source code

...

WASM Compiler / Toolchain

wasm-pack

...

Wasm binary

Glue code

Play video games

Start a "side-project" related to WASM

.NET & WASM

.NET : OSS app framework for C#

Multi-platform and Fullstack

Wasm is a target of .Net :

  • Blazor: frontend framework
  • wasm-tools: Wasm compiler
@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

🌍 Web Framework: HTML + CSS + C# (instead of JS / TS)

💻 Supports client-side rendeering (C# is compiled to 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

  • Install .net
  • Install Wasm :
    • $ sudo dotnet workload install wasm-tools wasm-experimental
  • ​Create a dotnet + Wasm project :
    • $ dotnet new wasmbrowser
  • Run dev serer :
    • $ dotnet run
  • Tutorial: https://www.youtube.com/watch?v=OHu0GpczOT8

Demo: .Net + Wasm web app

Demo: .Net + Wasm web app

I have to port a game written in .NET to the browser!

🎮 Make a game run in platforms other than its original ones

🧑‍💻 By rewriting the source code for the new platform(s)

❌ Not porting: virtual machine or emulator

MVG YouTube channel

Game porting

Released in 1993 for DOS

🌟 Doom is portable by design🌟

+

Engine

Resources

sf2

wad

http://mrglitchsreviews.blogspot.com/2012/09/doom-console-ports.html

Official Doom ports

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

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

youtube.com/watch?v=0mCsluv5FXA

Doom with typescript types

ManagedDoom

  • .NET Port of LinuxDoom (official source code)
  • ManagedDoom V1 Uses SFML (graphics + audio + input)
    • V2 uses silk.net (released 27 Dec 2022)
  • My work is a fork of ManagedDoom V1

id-Software/DOOM

sinshu/managed-doom

yostane/MangedDoom-Blazor

Strategy

  • Clone sininshu/managed-doom
  • Change the build target to Wasm
  • Re-implement SFML code:
    1. With mocks until the project compiles
    2. Then replace mocks with correct implementations
  • Implemenet minimal JS (gameloop, audio and video rendering)
using SFML.Audio;

namespace ManagedDoom.Audio
{
    public sealed class SfmlSound : ISound, IDisposable
    {
        private SoundBuffer[] buffers;
    }
}

Example: a class that depends on SFML

Not implemented for Wasm target:

❌ SFML.Audio namespace

❌ SoundBuffer SFML classe

namespace SFML.Audio
{
    public class SoundBuffer
    {
        public SoundBuffer(short[] samples, int v, uint sampleRate)
        {
        	// TODO: implement
        }

        internal void Dispose()
        {
            // TODO: implement
        }
    }
}

So, let's implement it

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; }
    }
}

Final code

❌ While loop will hang the browser (single threaded)

👉 requestAnimationFrame() is the recommended alternative

Game loop in C#

while (waitForNextFrame()){
  const input = getPlayerInput();
  const { frame, audio } 
  	      = UpdateGameState(input, WAD);
  render(frame);
  play(audio);
}
function gameLoop(){
  if (canAdvanceFrame()){
    const input = getPlayerInput();
    const { frame, audio } 
  	      = UpdateGameState(input);
    render(frame);
    play(audio);
  }
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);

Game loop in JS

ManagedDoom V1 architecture

For loop (game loop)

pressed keys (keyup)

release keys (keydown)

['Z', 'Q']

['space']

Frame buffer

Audio buffer

SFML Audio

SFML Video

 1 iteration of the Doom Engine

Updates the game state

.NET WASM

~70% OK

.NET WASM

wad

DOOM.wad

sf2

SoundFont.sf2

Blazor Doom architecture

pressed keys (keyup)

release keys (keydown)

['Z', 'Q']

['space']

wad

DOOM.wad

requestAnimationFrame

Run .Net

Audio buffer

Frame buffer

Canvas

Audio Context

Run JS

Run JS

 1 iteration of the Doom Engine

Updates the game state

sf2

SoundFont.sf2

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

Entry point

Gameloop

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);
}

Gameloop

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

Sample

1 / Sampling frequency

+ Sampling frequency

🔊

Sound effects

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
        );
    }
}

Sound effects

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();
}

Music

  • Doom, and many retro games, use SF2 (sound font)
  • List of possible sounds
  • ​Effects can be applied (pitch, volume, etc.)
  • A music is a combination of sounds + effects
  • Similar to text fonts:
    • Reusable glyphs
    • Can be displayed with effects (bold, italic, etc.)

Note 0

Note 1

... Note n

Music streaming

sf2

Time

Time

Streaming de la musique

Time

Time

sf2

AudioContext 😢

  • Glitchs when chaining small audio chunks

  • Does not handle streaming natively

Solution 🎶🥳

  • Group chunks into a big buffer

  • Schedule buffer playback chaining

  • Drawback: music start with delay (to fill the first buffer) 

Music streaming

Drawback: Music starts with a delay (time to fill first buffer)

function playMusic(samples) {
  	// Play the audio buffer when it's filled enough
    if (this.#currentMusicBufferIndex >= this.#musicBuffer.length) {
      const currentTime = this.#audioContext.currentTime;
      const duration = this.#currentMusicBufferIndex / this.musicSampleRate;
      // Schedule the playback to start after the end of the previous buffer
      source.start(this.expectedBufferEndTime, 0, duration);
      this.expectedBufferEndTime = currentTime + duration;
    }
	// Filling the buffer with the current chunk
    for (let i = 0; i < samples.length; i++) {
      this.#currentChannelData[this.#currentMusicBufferIndex + i] 
        = samples[i] / 32767;
    }
    this.#currentMusicBufferIndex += samples.length;
}
0 1 2 3 1 1 2 3
0 1 2 3

Frame rendering

2 2 0 1

Image

Color palette

Canvas

Each item (pixel) contains its color index

How will the image be rendered?

➡️ then ⬇️

Raycasting

source: raycasting by MeTH

1- Send rays evenly spread across the field of view

source: raycasting by MeTH

2 - Each ray generates a column based on the collision distance

3 - Thus, the image is built column by column

1- Send rays evenly spread across the field of view

Raycasting

0 1 2 3 1 1 2 3
0 1 2 3
2 2 0 1

Image

Canvas

Pixels are laid-out from top to bottom, then left to right

Frame rendering

Color palette

Each item (pixel) contains its color index

Send pixels from C# to 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));
  }
}

HTML 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

Move / Se déplacer

Select WAD / Sélection de WAD

C# code, the .Net runtime and librairies  are compiled to WASM

∞ WASM possibilities are infinite

💪 Game porting is accessible to all

🎮 Game dev is a great for learning while having fun

Credits

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

Narrated by

#TechAtWorldline

Special thanks

Starring

WebAssembly alias WASM

.Net

JavaScript alias JS

The doom guy

Credits

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

Narrated by

#TechAtWorldline

Special thanks

Starring

WebAssembly alias WASM

.Net

JavaScript alias JS

The doom guy

[TechForum Germany 2025] How I ported Doom to the browser

By yostane

[TechForum Germany 2025] 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 👍.

  • 24