GNU/Linux >> Tutoriales Linux >  >> Panels >> Docker

Un emulador de GameBoy del lado del servidor para varios jugadores escrito en .NET Core y Angular

Una de las grandes alegrías de compartir y descubrir código en línea es cuando te topas con algo verdaderamente épico, tan asombroso , que tienes que investigar. Dirígete a https://github.com/axle-h/Retro.Net y pregúntate por qué este proyecto de GitHub tiene solo 20 estrellas.

Alex Haslehurst ha creado algunas bibliotecas de hardware retro en .NET Core de código abierto con un front-end angular.

¿Traducción?

Un emulador de Game Boy del lado del servidor multijugador. Épico.

Puede ejecutarlo en minutos con

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

¡Luego simplemente navegue a http://localhost:2500 y juegue Tetris en el GameBoy original!

Me encanta esto por varias razones.

Primero, me encanta su perspectiva:

Consulte mi emulador de GameBoy escrito en .NET Core; Retro.Net . Sí, un emulador de GameBoy escrito en .NET Core. ¿Por qué? Por que no. Planeo hacer algunos artículos sobre mi experiencia con este proyecto. Primero:por qué fue una mala idea.

  1. Emulación en .NET
  2. Emular la CPU de GameBoy en .NET

El problema más grande que se tiene al tratar de emular una CPU con una plataforma como .NET es la falta de sincronización confiable de alta precisión. Sin embargo, logra una buena emulación desde cero del procesador Z80, modelando cosas de bajo nivel como registros en C# de muy alto nivel. Me encanta que la clase pública GameBoyFlagsRegister sea una cosa.;) Hice cosas similares cuando transfirí una "CPU diminuta" de 15 años a .NET Core/C#.

Asegúrese de revisar la explicación extremadamente detallada de Alex sobre cómo modeló el microprocesador Z80.

Afortunadamente, la CPU de GameBoy, una Sharp LR35902, se deriva del popular y muy bien documentado Zilog Z80:un microprocesador que increíblemente todavía está en producción hoy, más de 40 años después de su presentación.

El Z80 es un microprocesador de 8 bits, lo que significa que cada operación se realiza de forma nativa en un solo byte. El conjunto de instrucciones tiene algunas operaciones de 16 bits, pero estas solo se ejecutan como ciclos múltiples de lógica de 8 bits. El Z80 tiene un bus de direcciones de 16 bits de ancho, que lógicamente representa un mapa de memoria de 64K. Los datos se transfieren a la CPU a través de un bus de datos de 8 bits de ancho, pero esto es irrelevante para simular el sistema a nivel de máquina de estado. El Z80 y el Intel 8080 del que deriva tienen 256 puertos de E/S para acceder a periféricos externos, pero la CPU de GameBoy no tiene ninguno, favoreciendo la E/S mapeada en memoria

No solo creó un emulador, hay muchos, sino que lo ejecuta de manera única en el lado del servidor mientras permite controles compartidos en un navegador. "Entre cada cuadro único, todos los clientes conectados pueden votar sobre cuál debería ser la siguiente entrada de control. El servidor elegirá la que tenga más votos... la mayor parte del tiempo". ¡GameBoy en línea multijugador masivo! ¡Luego transmite el siguiente cuadro! "El renderizado de GPU se completa en el servidor una vez por cuadro único, se comprime con LZ4 y se transmite a todos los clientes conectados a través de websockets".

Este es un gran repositorio de aprendizaje porque:

  • Tiene una lógica de negocios compleja en el lado del servidor, pero el front-end usa Angular y web-sockets y tecnologías web abiertas.
  • También es bueno que tenga un Dockerfile completo de varias etapas que en sí mismo es un excelente ejemplo de cómo compilar aplicaciones .NET Core y Angular en Docker.
  • Amplia (miles) de pruebas unitarias con el Framework de aserción Shouldly y el Framework Moq Mocking.
  • Excelentes ejemplos de usos de la programación reactiva
  • Pruebas unitarias tanto en el servidor como en el cliente, utilizando Karma Unit Testing para Angular

Aquí hay algunos fragmentos de código elegantes favoritos en este enorme repositorio.

El botón reactivo presiona:

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

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

Los GameBoy Frames se componen en el lado del servidor, luego se comprimen y se envían al cliente a través de WebSockets. Tiene fondos y sprites funcionando, y aún queda trabajo por hacer.

Raw LCD es un lienzo 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>

Me encanta todo este proyecto porque lo tiene todo. TypeScript, 2D JavaScript Canvas, juegos retro y mucho más.

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

¡Te animo a usar STAR y CLONE https://github.com/axle-h/Retro.Net y probarlo con Docker! Luego puede usar Visual Studio Code y .NET Core para compilarlo y ejecutarlo localmente. Está buscando ayuda con el sonido de GameBoy y un depurador.

Patrocinador: Obtenga el último JetBrains Rider para depurar código .NET de terceros, Smart Step Into, más mejoras del depurador, C# Interactive, nuevo asistente de proyectos y formateo de código en columnas.


Docker
  1. Cómo instalar .NET Core en Debian 10

  2. Configurar .Net Core en Ubuntu 20.04:¿una guía paso a paso?

  3. Un microservicio completo de aplicaciones .NET Core en contenedores que es lo más pequeño posible

  4. Detectar que una aplicación .NET Core se está ejecutando en un Docker Container y SkippableFacts en XUnit

  5. Optimización de los tamaños de imagen de ASP.NET Core Docker

Creación, ejecución y prueba de .NET Core y ASP.NET Core 2.1 en Docker en una Raspberry Pi (ARM32)

Probar nuevas imágenes de .NET Core Alpine Docker

.NET y Docker

Exploración de ASP.NET Core con Docker en contenedores de Linux y Windows

Afirme sus suposiciones:.NET Core y problemas locales sutiles con WSL Ubuntu

Instalación de PowerShell Core en una Raspberry Pi (con tecnología de .NET Core)