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
id-Software/DOOM

sinshu/managed-doom
yostane/MangedDoom-Blazor














Strategy





❌
- Clone sininshu/managed-doom
- Change the build target to Wasm
- Re-implement SFML code:
- With mocks until the project compiles
- 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