GNU/Linux >> Linux Esercitazione >  >> Panels >> Docker

Un emulatore GameBoy lato server multigiocatore scritto in .NET Core e Angular

Una delle grandi gioie della condivisione e della scoperta del codice online è quando ti imbatti in qualcosa di così veramente epico, così sorprendente , che devi approfondire. Vai su https://github.com/axle-h/Retro.Net e chiediti perché questo progetto GitHub ha solo 20 stelle?

Alex Haslehurst ha creato alcune librerie hardware retrò in .NET Core open source con un front-end angolare!

Traduzione?

Un emulatore di Game Boy multiplayer lato server. Epico.

Puoi eseguirlo in pochi minuti con

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

Quindi vai su http://localhost:2500 e gioca a Tetris sul GameBoy originale!

Mi piace questo per una serie di motivi.

Innanzitutto, amo il suo punto di vista:

Controlla il mio emulatore GameBoy scritto in .NET Core; Retro.Net . Sì, un emulatore GameBoy scritto in .NET Core. Come mai? Perché no. Ho intenzione di scrivere alcuni articoli sulla mia esperienza con questo progetto. Primo:perché è stata una cattiva idea.

  1. Emulazione su .NET
  2. Emulare la CPU del GameBoy su .NET

Il problema più grande che si ha nel tentativo di emulare una CPU con una piattaforma come .NET è la mancanza di un timing affidabile e di alta precisione. Tuttavia, gestisce una bella emulazione da zero del processore Z80, modellando cose di basso livello come i registri in C# di livello molto alto. Adoro il fatto che GameBoyFlagsRegister di classe pubblica sia una cosa.;) Ho fatto cose simili quando ho portato una "Tiny CPU" di 15 anni su .NET Core/C#.

Assicurati di controllare la spiegazione estremamente dettagliata di Alex su come ha modellato il microprocessore Z80.

Fortunatamente la CPU GameBoy, uno Sharp LR35902, è derivata dal popolare e molto ben documentato Zilog Z80, un microprocessore che è incredibilmente ancora in produzione oggi, oltre 40 anni dopo la sua introduzione.

Lo Z80 è un microprocessore a 8 bit, il che significa che ogni operazione viene eseguita nativamente su un singolo byte. Il set di istruzioni ha alcune operazioni a 16 bit, ma queste vengono eseguite semplicemente come cicli multipli di logica a 8 bit. Lo Z80 ha un bus di indirizzi a 16 bit, che rappresenta logicamente una mappa di memoria da 64K. I dati vengono trasferiti alla CPU su un bus dati largo 8 bit, ma ciò è irrilevante per la simulazione del sistema a livello di macchina a stati. Lo Z80 e l'Intel 8080 da cui deriva hanno 256 porte I/O per l'accesso a periferiche esterne ma la CPU del GameBoy non ne ha, preferendo invece l'I/O mappato in memoria

Non ha solo creato un emulatore - ce ne sono molti - ma lo esegue in modo univoco sul lato server consentendo controlli condivisi in un browser. "Tra ogni frame univoco, tutti i client connessi possono votare quale dovrebbe essere il prossimo input di controllo. Il server sceglierà quello con il maggior numero di voti... il più delle volte." GameBoy online multigiocatore di massa! Quindi esegue lo streaming del fotogramma successivo! "Il rendering della GPU viene completato sul server una volta per frame univoco, compresso con LZ4 e trasmesso in streaming a tutti i client connessi tramite websocket".

Questo è un ottimo repository di apprendimento perché:

  • ha una logica di business complessa sul lato server, ma il front-end utilizza Angular e Web-Socket e tecnologie Web aperte.
  • È anche bello che abbia un Dockerfile multifase completo che è di per sé un ottimo esempio di come creare app .NET Core e Angular in Docker.
  • Ampi (migliaia) di unit test con il Shouldly Assertion Framework e il Moq Mocking Framework.
  • Grandi esempi di utilizzo della programmazione reattiva
  • Unit Testing su server E client, utilizzando Karma Unit Testing per Angular

Ecco alcuni eleganti frammenti di codice preferiti in questo enorme repository.

Il pulsante reattivo preme:

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

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

I GameBoy Frame sono composti sul lato server, quindi compressi e inviati al client tramite WebSocket. Ha background e sprite funzionanti e c'è ancora del lavoro da fare.

Il Raw LCD è una tela 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>

Amo questo intero progetto perché ha tutto. TypeScript, tela JavaScript 2D, giochi retrò e molto altro!

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

Ti incoraggerei ad andare STAR e CLONE https://github.com/axle-h/Retro.Net e provarlo con Docker! È quindi possibile utilizzare Visual Studio Code e .NET Core per compilarlo ed eseguirlo in locale. Sta cercando aiuto con il suono del GameBoy e un debugger.

Sponsor: Ottieni l'ultimo JetBrains Rider per il debug di codice .NET di terze parti, Smart Step Into, ulteriori miglioramenti del debugger, C# Interactive, nuova procedura guidata di progetto e formattazione del codice nelle colonne.


Docker
  1. Come installare .NET Core su Debian 10

  2. Installazione di .Net Core in Ubuntu 20.04 - Una guida passo passo?

  3. Rilevamento che un'app .NET Core è in esecuzione in un contenitore Docker e SkippableFacts in XUnit

  4. Visual Basic è supportato da .NET Core in Linux?

  5. NuGet per .NET Core in Linux

Creazione, esecuzione e test di .NET Core e ASP.NET Core 2.1 in Docker su un Raspberry Pi (ARM32)

Provare nuove immagini Docker Alpine .NET Core

.NET e Docker

Esplorazione di ASP.NET Core con Docker in entrambi i contenitori Linux e Windows

Afferma le tue ipotesi:.NET Core e sottili problemi di localizzazione con WSL Ubuntu

Installazione di PowerShell Core su un Raspberry Pi (con tecnologia .NET Core)