DevRel @ Worldline
Teacher
Yassine
Benabbas
yostane
YCoding
MVG's video is a great source of inspiration
@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++; }
}
http://mrglitchsreviews.blogspot.com/2012/09/doom-console-ports.html
Ported to a LOT of platforms
https://www.link-cable.com/top-10-weird-doom-ports/
Source: https://www.yahoo.com/news/1995-bill-gates-gave-crazy-180900044.html
id-Software/DOOM
sinshu/managed-doom
yostane/MangedDoom-Blazor
creator.nightcafe.studio
while (waitForNextFrame()){
const input = getPlayerInput();
const { frame, audio }
= UpdateGameState(input, WAD);
render(frame);
play(audio);
}
pressed keys (keyup)
release keys (keydown)
['Z', 'Q']
['space']
Frame
Audio
UpdateGameState
Blazor
~70% OK
Browser
SFML Audio
SFML Video
DOOM.wad
wad
while loop
pressed keys (keyup)
release keys (keydown)
['Z', 'Q']
['space']
wad
DOOM.wad
requestAnimationFrame
Audio buffer
Frame buffer
Canvas
Audio Context
UpdateGameState
pressed keys (keyup)
release keys (keydown)
['Z', 'Q']
['space']
wad
DOOM.wad
requestAnimationFrame
Audio buffer
Frame buffer
Canvas
Audio Context
UpdateGameState
BlazorDoom component
pressed keys (keyup)
release keys (keydown)
['Z', 'Q']
['space']
wad
DOOM.wad
requestAnimationFrame
DotNet.invokeMethod
Audio buffer
Frame buffer
Canvas
Audio Context
IJSRuntime.Invoke
IJSRuntime.InvokeUnmarshalled
UpdateGameState
BlazorDoom component
IJSRuntime.InvokeVoid
https://doom.fandom.com/wiki/Cacodemon/Doom
<canvas id="canvas" image-rendering: pixelated;" ...>
</canvas>
@code {
// Entry point of the game
private async Task StartGame()
{
// steup the game object
app = new ManagedDoom.DoomApplication(...);
// JS method that calls "requestAnimationFrame"
jsProcessRuntime.InvokeVoid("gameLoop");
}
}
Entry point and frame pacing
window.gameLoop = function (timestamp) {
// Check the pacing
if (timestamp - lastFrameTimestamp >= frameTime) {
lastFrameTimestamp = timestamp;
// Updates the game state (advances a frame)
DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys);
}
// This replaces the for loop in a traditional game
// Request the browser to notify us when we do the next iteration
window.requestAnimationFrame(window.gameLoop);
}
Entry point and frame pacing
window.gameLoop = function (timestamp) {
// Check the pacing
if (timestamp - lastFrameTimestamp >= frameTime) {
lastFrameTimestamp = timestamp;
// Updates the game state (advances a frame)
DotNet.invokeMethod('BlazorDoom', 'UpdateGame', downKeys, upKeys);
}
// This replaces the for loop in a traditional game
// Request the browser to notify us when we do the next iteration
window.requestAnimationFrame(window.gameLoop);
}
// The code that I showed earlier
[JSInvokable("GameLoop")]
public static void UpdateGame(uint[] downKeys, uint[] upKeys)
{
app.Run(downKeys, upKeys);
}
Audio playback
| 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
playByteArray(samples, sampleRate) {
const audioBuffer = this.context.createBuffer(
1, length, this.context.sampleRate
);
var channelData = audioBuffer.getChannelData(0);
// JS receives a weird "samples" array
for (let i = 0; i < length; i += 2) {
// Scale the value to between -1 and 1
channelData[i] = samples[i] / 0xffff;
}
// Play the audio
var source = this.context.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.context.destination);
source.start();
}
Audio playback
// Somewhere in the Doom Engine's audio module
DoomApplication.WebAssemblyJSRuntime.Invoke<object>(
"playSound", new object[] { samples, sampleRate, 0, Position }
);
| 0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
|---|
| 0 | 1 | 2 | 3 |
|---|
From 1D frame to a 2D frame
| 2 | 2 | 0 | 1 |
|---|
Frame data
Color palette
Frame
C# Byte Array to JS, considerations
| 0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 |
|---|
| 0 | 1 | 2 | 3 |
|---|
| 2 | 2 | 0 | 1 |
|---|
Frame data
Color palette
0123
1123
2201
0123
IJSRuntime.InvokeUnmarshalled
window.renderWithColorsAndScreenDataUnmarshalled = (screenData, colors) => {
// JS receives an array with 4 bytes per item
for (var i = 0; i < (width * height) / 4; i += 1) {
// Gets the array sent from C#
const screenDataItem = BINDING.mono_array_get(screenData, i);
for (var mask = 0; mask <= 24; mask += 8) {
let dataIndex = y * (width * 4) + x;
setSinglePixel(imageData, dataIndex, colors,
(screenDataItem >> mask) & 0xff);
// Build the image from top to bottom, left to right
if (y >= height - 1) { y = 0; x += 4; } else { y += 1; }
}
}
context.putImageData(imageData, 0, 0);
};
function setSinglePixel(imageData, dataIndex, colors, colorIndex) {
const color = BINDING.mono_array_get(colors, colorIndex);
imageData.data[dataIndex] = color & 0xff;
imageData.data[dataIndex + 1] = (color >> 8) & 0xff;
imageData.data[dataIndex + 2] = (color >> 16) & 0xff;
imageData.data[dataIndex + 3] = 255;
}
Frame rendering
// Somwhere in the Doom Engine's graphics module
var args = new object[] { screen.Data, colors, 320, 200 };
// Send the frame buffer to JS
DoomApplication.WebAssemblyJSRuntime.InvokeUnmarshalled<byte[], uint[], int>
("renderWithColorsAndScreenDataUnmarshalled", screen.Data, colors);
window.requestAnimationFrame allows to pace the frames
Browsers require interaction with the page to play audio
export function setLocalStorage(todosJson) {
window.localStorage.setItem('dotnet-wasm-todomvc', todosJson);
}
static partial class Interop
{
[JSImport("setLocalStorage", "todoMVC/store.js")]
internal static partial void _setLocalStorage(string json);
}
public partial class MainJS
{
[JSExport]
public static void OnHashchange(string url)
{
controller?.SetView(url);
}
}
const exports = await getAssemblyExports(
getConfig().mainAssemblyName);
exports.TodoMVC.MainJS.OnHashchange(document.location.hash);
Call JS from .Net
Call .Net from JS
Any questions ?
Source code
Slides