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

Made with Slides.com