GNU/Linux >> LINUX-Kenntnisse >  >> Panels >> Docker

Ein serverseitiger Multiplayer-GameBoy-Emulator, geschrieben in .NET Core und Angular

Eine der größten Freuden beim Teilen und Entdecken von Code online ist, wenn Sie auf etwas so wirklich Episches, so Erstaunliches stoßen , dass Sie sich vertiefen müssen. Gehen Sie zu https://github.com/axle-h/Retro.Net und fragen Sie sich, warum dieses GitHub-Projekt nur 20 Sterne hat?

Alex Haslehurst hat einige Retro-Hardwarebibliotheken in Open Source .NET Core mit einem Angular-Frontend erstellt!

Übersetzung?

Ein serverseitiger Multiplayer-Game Boy-Emulator. Episch.

Sie können es in wenigen Minuten mit

ausführen
docker run -p 2500:2500 alexhaslehurst/server-side-gameboy

Navigieren Sie dann einfach zu http://localhost:2500 und spielen Sie Tetris auf dem originalen GameBoy!

Ich liebe das aus mehreren Gründen.

Erstens liebe ich seine Perspektive:

Bitte sehen Sie sich meinen in .NET Core geschriebenen GameBoy-Emulator an; Retro.Net . Ja, ein in .NET Core geschriebener GameBoy-Emulator. Wieso den? Warum nicht. Ich habe vor, ein paar Berichte über meine Erfahrungen mit diesem Projekt zu schreiben. Erstens:warum es eine schlechte Idee war.

  1. Emulation auf .NET
  2. Emulation der GameBoy-CPU auf .NET

Das größte Problem beim Versuch, eine CPU mit einer Plattform wie .NET zu emulieren, ist das Fehlen eines zuverlässigen, hochpräzisen Timings. Er schafft jedoch eine nette Emulation des Z80-Prozessors von Grund auf neu, indem er Dinge auf niedriger Ebene wie Register in C# auf sehr hoher Ebene modelliert. Ich finde es toll, dass GameBoyFlagsRegister eine öffentliche Klasse ist.;) Ich habe ähnliche Dinge getan, als ich eine 15 Jahre alte "Tiny CPU" auf .NET Core/C# portiert habe.

Sehen Sie sich auf jeden Fall Alex' äußerst detaillierte Erklärung an, wie er den Z80-Mikroprozessor modelliert hat.

Glücklicherweise ist die GameBoy-CPU, ein Sharp LR35902, von dem beliebten und sehr gut dokumentierten Zilog Z80 abgeleitet – einem Mikroprozessor, der unglaublicherweise noch heute, über 40 Jahre nach seiner Einführung, in Produktion ist.

Der Z80 ist ein 8-Bit-Mikroprozessor, was bedeutet, dass jede Operation nativ auf einem einzelnen Byte ausgeführt wird. Der Befehlssatz hat einige 16-Bit-Operationen, aber diese werden nur als mehrere Zyklen von 8-Bit-Logik ausgeführt. Der Z80 hat einen 16 Bit breiten Adressbus, der logisch eine 64K-Speicherkarte darstellt. Die Datenübertragung zur CPU erfolgt über einen 8 Bit breiten Datenbus, was aber für die Simulation des Systems auf State-Machine-Ebene irrelevant ist. Der Z80 und der Intel 8080, von dem er abgeleitet ist, haben 256 E/A-Anschlüsse für den Zugriff auf externe Peripheriegeräte, aber die GameBoy-CPU hat keine - sie bevorzugen stattdessen speicherabgebildete E/A

Er hat nicht nur einen Emulator erstellt – es gibt viele davon –, sondern er führt ihn auf der Serverseite aus, während er gemeinsame Steuerungen in einem Browser zulässt. „Zwischen jedem einzelnen Frame können alle verbundenen Clients darüber abstimmen, was die nächste Steuereingabe sein soll. Der Server wählt diejenige mit den meisten Stimmen … meistens.“ Massive-Multiplayer-Online-GameBoy! Dann streamt er den nächsten Frame! „Das GPU-Rendering wird einmal pro Einzelbild auf dem Server abgeschlossen, mit LZ4 komprimiert und über Websockets an alle verbundenen Clients gestreamt.“

Dies ist ein großartiges Lernrepository, weil:

  • Es hat eine komplexe Geschäftslogik auf der Serverseite, aber das Frontend verwendet Angular und Web-Sockets und offene Webtechnologien.
  • Es ist auch schön, dass er eine vollständige mehrstufige Docker-Datei hat, die selbst ein großartiges Beispiel dafür ist, wie man sowohl .NET Core- als auch Angular-Apps in Docker erstellt.
  • Umfangreiche (Tausende) Einheitentests mit dem Shouldly Assertion Framework und dem Moq Mocking Framework.
  • Großartige Anwendungsbeispiele für die reaktive Programmierung
  • Unit Testing auf Server UND Client mit Karma Unit Testing für Angular

Hier sind ein paar elegante Codeschnipsel in diesem riesigen Repository.

Die reaktiven Tastendrücke:

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

Der GPU-Renderer:

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

Die GameBoy-Frames werden serverseitig zusammengesetzt, dann komprimiert und über WebSockets an den Client gesendet. Er hat Hintergründe und Sprites am Laufen, und es gibt noch viel zu tun.

Das Raw LCD ist eine HTML5-Leinwand:

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

Ich liebe dieses ganze Projekt, weil es alles hat. TypeScript, 2D JavaScript Canvas, Retro-Gaming und vieles mehr!

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

Ich würde Sie ermutigen, zu STAR und CLONE https://github.com/axle-h/Retro.Net zu gehen und es mit Docker zu versuchen! Anschließend können Sie Visual Studio Code und .NET Core verwenden, um es lokal zu kompilieren und auszuführen. Er sucht Hilfe mit GameBoy-Sound und einem Debugger.

Sponsor: Holen Sie sich den neuesten JetBrains Rider zum Debuggen von .NET-Code von Drittanbietern, Smart Step Into, weitere Debugger-Verbesserungen, C# Interactive, den Assistenten für neue Projekte und das Formatieren von Code in Spalten.


Docker
  1. So installieren Sie .NET Core unter Debian 10

  2. .Net Core in Ubuntu 20.04 einrichten - Eine Schritt-für-Schritt-Anleitung?

  3. Ein vollständiger containerisierter .NET Core-Anwendungsmikrodienst, der so klein wie möglich ist

  4. Erkennen, dass eine .NET Core-App in einem Docker-Container ausgeführt wird, und SkipableFacts in XUnit

  5. Optimieren der Größen von ASP.NET Core Docker-Images

Erstellen, Ausführen und Testen von .NET Core und ASP.NET Core 2.1 in Docker auf einem Raspberry Pi (ARM32)

Testen neuer .NET Core Alpine Docker-Images

.NET und Docker

Erkunden von ASP.NET Core mit Docker in Linux- und Windows-Containern

Bestätigen Sie Ihre Annahmen – .NET Core und subtile Gebietsschemaprobleme mit WSLs Ubuntu

Installieren von PowerShell Core auf einem Raspberry Pi (powered by .NET Core)