GNU/Linux >> Tutoriels Linux >  >> Panels >> Docker

Un émulateur GameBoy côté serveur multi-joueurs écrit en .NET Core et Angular

L'une des grandes joies du partage et de la découverte de code en ligne est lorsque vous tombez sur quelque chose de vraiment épique, de si incroyable , que vous devez creuser. Rendez-vous sur https://github.com/axle-h/Retro.Net et demandez-vous pourquoi ce projet GitHub n'a que 20 étoiles ?

Alex Haslehurst a créé des bibliothèques matérielles rétro en open source .NET Core avec un frontal angulaire !

Traduction ?

Un émulateur Game Boy multijoueur côté serveur. Épique.

Vous pouvez l'exécuter en quelques minutes avec

docker run -p 2500:2500 alexhaslehurst/server-side-gameboy

Ensuite, accédez simplement à http://localhost:2500 et jouez à Tetris sur la GameBoy d'origine !

J'aime cela pour plusieurs raisons.

Tout d'abord, j'aime son point de vue :

Veuillez consulter mon émulateur GameBoy écrit en .NET Core ; Retro.Net . Oui, un émulateur GameBoy écrit en .NET Core. Pourquoi? Pourquoi pas. Je prévois de faire quelques articles sur mon expérience avec ce projet. Premièrement :pourquoi c'était une mauvaise idée.

  1. Émulation sur .NET
  2. Émulation du processeur GameBoy sur .NET

Le plus gros problème que l'on rencontre en essayant d'émuler un processeur avec une plate-forme comme .NET est le manque de synchronisation fiable de haute précision. Cependant, il gère une belle émulation à partir de zéro du processeur Z80, modélisant des choses de bas niveau comme des registres en C # de très haut niveau. J'adore cette classe publique GameBoyFlagsRegister est une chose.;) J'ai fait des choses similaires lorsque j'ai porté un "Tiny CPU" de 15 ans sur .NET Core/C#.

Assurez-vous de consulter l'explication extrêmement détaillée d'Alex sur la façon dont il a modélisé le microprocesseur Z80.

Heureusement, le processeur GameBoy, un Sharp LR35902, est dérivé du populaire et très bien documenté Zilog Z80 - Un microprocesseur qui est incroyablement encore en production aujourd'hui, plus de 40 ans après son introduction.

Le Z80 est un microprocesseur 8 bits, ce qui signifie que chaque opération est effectuée nativement sur un seul octet. Le jeu d'instructions comporte des opérations 16 bits, mais celles-ci sont simplement exécutées sous forme de plusieurs cycles de logique 8 bits. Le Z80 dispose d'un bus d'adresse large de 16 bits, ce qui représente logiquement une carte mémoire de 64 Ko. Les données sont transférées vers le processeur via un bus de données de 8 bits de large, mais cela n'a aucun rapport avec la simulation du système au niveau de la machine d'état. Le Z80 et l'Intel 8080 dont il dérive ont 256 ports d'E/S pour accéder aux périphériques externes, mais le processeur GameBoy n'en a pas - favorisant les E/S mappées en mémoire à la place

Il n'a pas simplement créé un émulateur - il y en a beaucoup - mais il l'exécute uniquement côté serveur tout en permettant des contrôles partagés dans un navigateur. "Entre chaque trame unique, tous les clients connectés peuvent voter sur ce que devrait être la prochaine entrée de contrôle. Le serveur choisira celle avec le plus de votes… la plupart du temps." GameBoy en ligne massivement multijoueur ! Puis il diffuse l'image suivante ! "Le rendu GPU est terminé sur le serveur une fois par image unique, compressé avec LZ4 et diffusé en continu vers tous les clients connectés via des websockets."

Il s'agit d'un excellent référentiel d'apprentissage car :

  • il a une logique métier complexe côté serveur, mais le front-end utilise Angular, des sockets Web et des technologies Web ouvertes.
  • C'est également bien qu'il dispose d'un Dockerfile complet en plusieurs étapes qui est lui-même un excellent exemple de la façon de créer à la fois des applications .NET Core et Angular dans Docker.
  • Vaste (des milliers) de tests unitaires avec le framework d'assertion Shouldly et le framework de simulation Moq.
  • Excellents exemples d'utilisation de la programmation réactive
  • Tests unitaires sur le serveur ET le client, à l'aide des tests unitaires Karma pour Angular

Voici quelques extraits de code élégants préférés dans cet immense référentiel.

Le bouton réactif appuie :

_joyPadSubscription = _joyPadSubject
    .Buffer(FrameLength)
    .Where(x => x.Any())
    .Subscribe(presses =>
                {
                    var (button, name) = presses
                        .Where(x => !string.IsNullOrEmpty(x.name))
                        .GroupBy(x => x.button)
                        .OrderByDescending(grp => grp.Count())
                        .Select(grp => (button: grp.Key, name: grp.Select(x => x.name).First()))
                        .FirstOrDefault();
                    joyPad.PressOne(button);
                    Publish(name, $"Pressed {button}");

                    Thread.Sleep(ButtonPressLength);
                    joyPad.ReleaseAll();
                });

Le moteur de rendu GPU :

private void Paint()
{
    var renderSettings = new RenderSettings(_gpuRegisters);

    var backgroundTileMap = _tileRam.ReadBytes(renderSettings.BackgroundTileMapAddress, 0x400);
    var tileSet = _tileRam.ReadBytes(renderSettings.TileSetAddress, 0x1000);
    var windowTileMap = renderSettings.WindowEnabled ? _tileRam.ReadBytes(renderSettings.WindowTileMapAddress, 0x400) : new byte[0];

    byte[] spriteOam, spriteTileSet;
    if (renderSettings.SpritesEnabled) {
        // If the background tiles are read from the sprite pattern table then we can reuse the bytes.
        spriteTileSet = renderSettings.SpriteAndBackgroundTileSetShared ? tileSet : _tileRam.ReadBytes(0x0, 0x1000);
        spriteOam = _spriteRam.ReadBytes(0x0, 0xa0);
    }
    else {
        spriteOam = spriteTileSet = new byte[0];
    }

    var renderState = new RenderState(renderSettings, tileSet, backgroundTileMap, windowTileMap, spriteOam, spriteTileSet);

    var renderStateChange = renderState.GetRenderStateChange(_lastRenderState);
    if (renderStateChange == RenderStateChange.None) {
        // No need to render the same frame twice.
        _frameSkip = 0;
        _framesRendered++;
        return;
    }

    _lastRenderState = renderState;
    _tileMapPointer = _tileMapPointer == null ? new TileMapPointer(renderState) : _tileMapPointer.Reset(renderState, renderStateChange);
    var bitmapPalette = _gpuRegisters.LcdMonochromePaletteRegister.Pallette;
    for (var y = 0; y < LcdHeight; y++) {
        for (var x = 0; x < LcdWidth; x++) {
            _lcdBuffer.SetPixel(x, y, (byte) bitmapPalette[_tileMapPointer.Pixel]);

            if (x + 1 < LcdWidth) {
                _tileMapPointer.NextColumn();
            }
        }

        if (y + 1 < LcdHeight){
            _tileMapPointer.NextRow();
        }
    }
    
    _renderer.Paint(_lcdBuffer);
    _frameSkip = 0;
    _framesRendered++;
}

Les GameBoy Frames sont composées côté serveur puis compressées et envoyées au client via WebSockets. Il a des arrière-plans et des sprites qui fonctionnent, et il reste encore du travail à faire.

Le LCD brut est un canevas HTML5 :

<canvas #rawLcd [width]="lcdWidth" [height]="lcdHeight" class="d-none"></canvas>
<canvas #lcd
        [style.max-width]="maxWidth + 'px'"
        [style.max-height]="maxHeight + 'px'"
        [style.min-width]="minWidth + 'px'"
        [style.min-height]="minHeight + 'px'"
        class="lcd"></canvas>

J'aime tout ce projet parce qu'il a tout. TypeScript, JavaScript Canvas 2D, rétro-gaming et bien plus encore !

const raw: HTMLCanvasElement = this.rawLcdCanvas.nativeElement;
const rawContext: CanvasRenderingContext2D = raw.getContext("2d");
const img = rawContext.createImageData(this.lcdWidth, this.lcdHeight);

for (let y = 0; y < this.lcdHeight; y++) {
  for (let x = 0; x < this.lcdWidth; x++) {
    const index = y * this.lcdWidth + x;
    const imgIndex = index * 4;
    const colourIndex = this.service.frame[index];
    if (colourIndex < 0 || colourIndex >= colours.length) {
      throw new Error("Unknown colour: " + colourIndex);
    }

    const colour = colours[colourIndex];

    img.data[imgIndex] = colour.red;
    img.data[imgIndex + 1] = colour.green;
    img.data[imgIndex + 2] = colour.blue;
    img.data[imgIndex + 3] = 255;
  }
}
rawContext.putImageData(img, 0, 0);

context.drawImage(raw, lcdX, lcdY, lcdW, lcdH);

Je vous encourage à aller STAR et CLONE https://github.com/axle-h/Retro.Net et à essayer avec Docker ! Vous pouvez ensuite utiliser Visual Studio Code et .NET Core pour le compiler et l'exécuter localement. Il cherche de l'aide avec le son GameBoy et un débogueur.

Parrain : Obtenez le dernier JetBrains Rider pour le débogage du code .NET tiers, Smart Step Into, d'autres améliorations du débogueur, C# Interactive, un nouvel assistant de projet et le formatage du code dans les colonnes.


Docker
  1. Un microservice d'application .NET Core conteneurisé complet et aussi petit que possible

  2. Détecter qu'une application .NET Core est en cours d'exécution dans un conteneur Docker et SkippableFacts dans XUnit

  3. Visual Basic est-il pris en charge par .NET Core sous Linux ?

  4. NuGet pour .NET Core sous Linux

  5. .NET core X509Store sur Linux

Création, exécution et test de .NET Core et ASP.NET Core 2.1 dans Docker sur un Raspberry Pi (ARM32)

Essayer de nouvelles images Docker Alpine .NET Core

.NET et Docker

Explorer ASP.NET Core avec Docker dans les conteneurs Linux et Windows

Affirmez vos hypothèses - .NET Core et problèmes de paramètres régionaux subtils avec WSLs Ubuntu

Installation de PowerShell Core sur un Raspberry Pi (alimenté par .NET Core)