Dienstag, 23. Mai 2017

DIY: Emulator Teil 2.1_1: Der Emulator Bildschirm

In diesem Artikel haben wir die grundlegende Grafik aufgesetzt. Nun geht es darum, dass der Emulator einen an sich angepassten Bildschirm bekommt, welchen wir jedoch auf unserem Graphikgerüst beliebig skalieren können.

Also ist das Ziel, auf einer Textur, welche die Grösse des Emulator Bildschirms hat, die einzelnen Pixel zu bearbeiten. Diese Textur wird dann skaliert auf unserem Bildschirm angezeigt.

Render-To-Texture so wie ich das wollte scheint nicht wirklich zu gehen. Array-to-Texture, das wärs gewesen. Naja...müssen wir das halt anders machen.

Also generell wird im Internetz geraten, so was auf einem eigenen HTML5 Canvas zu zeichnen. Dies könnte jedoch, nach Angaben, Konflikte mit WebGL geben. Per Zufall bin ich dann auf das Graphics-Objekt gestossen. Da kann man einfach etwas zeichnen und dies dann als Textur generieren.

Da hier einiges an Code abgehandelt wird, wird dieser Artikel in verschiedene Teile gegliedert.
In diesem Teil geht es um die Erstellung einer neuen Klasse und dann um die Initialisierungs-Funktion des Bildschirms. Die weiteren dazu benötigten Funktionen kommen in den folgenden Artikeln.

Theorie

Der Emulatorbildschirm besteht aus einer bestimmten Anzahl Pixel, welche auf dem PIXI-Bildschirm skaliert angezeigt werden sollten. Dazu wird ein PIXI-Container erstellt und daran werden in x und y Richtung reihenweise kleine farbige Rechtecke angehängt. Ein Rechteck stellt einen Pixel dar. Da der Aufbau der Rechtecke recht lange dauert, werden wir dies nur einmal machen. Danach werden wir nur noch die Grundfarbe der Textur verändern mit dem tint-Wert. Deshalb wird die Textur selbst weiss sein (nicht schwarz).

Dies alles wird zwei mal gemacht, so dass wir in dem einen Container "zeichnen" können, während der andere angezeigt wird.  Wenn fertig gezeichnet wurde, können wir einfach das visible-Flag der beiden Container wechseln, und schon wird der vorher gezeichnete Container angezeigt. (Das ist ein sogenannter Double-Buffer) Somit können wir ein "flackern" während des Bildaufbaus verhindern.

Mit PIXI.Graphics können wir Texturen erstellen, ohne dafür ein Bild zu laden. Chrome zum Beispiel motzt blöd rum und verweigert das Laden von Texturen als file:///. (Also in der lokalen Entwicklungsumgebung.)...lassen wir das doch gleich ganz.

Code

Der Code verändert sich mit jedem Part. Eventuell musst du (wie hier in index.html) in schon bearbeiteten Dateien "herumpfuschen". Jetzt kommt ziemlich viel Code. Ich werde den Code in Abschnitten unterbrechen um ihn zu erklären. So lange es nicht anders beschrieben wird, kannst du einfach dort weiterschreiben, wo du vorher warst.

Im Ordner js erstellen wir erst mal eine neue Datei: EmuGraphicsAdapter.js

EmuGraphicsAdapter.js:

EmuGraphicsAdapter = function(newwidth, newheight, bgcolor, bordercolor)
{

//  ...

In JavaScript kann man "Klassen" über Funktionen erstellen. Alle Parameter sind optional, deshalb werden wir prüfen, ob sie gegeben wurden. IN dieser Funktion werden nun folgende Variablen und Funktionen erstellt:


var PIXIStage = RSTAGE(); // oder RBACKSTAGE() oder RHUDSTAGE()

var emuBorderColor = 0x111133; // Dunkles Dunkelblau.
var emuBackgroundColor = 0x000000; // Normalerweise Schwarz.

// Breite des Randes.
var emuBorderWidth = 10;

// Grösse des Emulator Bildschirms
var emuScreenWidth = 40;
var emuScreenHeight = 40;

// Die doppelte Grösse des Emulator Bildschirms.
var emuDrawWidth = emuScreenWidth * 2;
var emuDrawHeight = emuScreenHeight * 2;

// Double Buffering.
var doubleBufferIndex = 0; // 0 oder 1.
var screenArray = [];


Mit var erstellt man private Variablen oder Funktionen in einer JS-Klasse. Öffentliche Variablen erstellt man mit this (z.B. this.positionX = 5;) und spricht sie auch innerhalb der Klasse mit this an.

  • PIXIStage: Referenz zum PIXI-"Root"-Container. RUNPIXI stellt dafür drei Container zur Verfügung: RSTAGE() ist der mittlere, welcher dank RUNPIXI mit Ctrl+Pfeiltasten gescrollt werden kann. Du kannst auch RBACKSTAGE() für den hintersten oder RHUDSTAGE() für den vordersten nehmen. Diese werden jedoch nicht gescrollt.
  • emuBorderColor: Die Farbe des Rahmens. Der Rahmen wird so oder so erstellt, da es "unsichtbare" Linien geben könnte. Wir werden diese später an die Hintergrundfarbe des PIXI-Screens anpassen.
  • emuBackgroundColor: Die standard Hintergrundfarbe des emulierten Systems. Bei einigen kann das Blau sein (Atari 800XL zum Beispiel), bei einigen Hellgrau (GameBoy) und bei den meisten Systemen jedoch schwarz.
  • emuBorderWidth: Die Breite des oben genannten Rahmens (unskaliert).
  • emuScreenWidth / emuScreenHeight: Die Grösse des Emulator Bildschirms.
  • emuDrawWidth / emuDrawHeight: Die doppelte Grösse des Emulator Bildschirms. Ein Emulator-Pixel ist 2x2 "reale" Pixel gross, deshalb wird der Bildschirm verdoppelt und dann um die Häfte runterskaliert um das Originalbild zu erhalten.
  • doubleBufferIndex: Der Index des Buffers auf welchem aktuell gezeichnet wird. Der jeweils andere Buffer wird aktuell angezeigt.
  • screenArray: Dieses Array enthält wieder zwei Arrays welche Referenzen auf die Pixel der Buffer enthalten. Die Container selbst werden ausserhalb der Klasse global erstellt, so dass wir sie aus der PIXI-Hierarchie löschen können wenn ein neuer Bildschirm initialisiert wird.

Initialisierungs-Funktion:


this.initialize = function(width, height, bgColor, borderColor)
{
         if(width)
                  emuScreenWidth = width;
         if(height)
                   emuScreenHeight = height;

         if(bgColor)
                   emuBackgroundColor = bgColor;
         if(borderColor)
                    emuBorderColor = borderColor;

         emuDrawWidth = emuScreenWidth * 2;
         emuDrawHeight = emuScreenHeight * 2;

         if(EmuGraphicsAdapter.containers.length > 0)
         {
                 for(var i = 0; i < EmuGraphicsAdapter.containers.length; i++)
                 {
                       PIXIStage.removeChild(EmuGraphicsAdapter.containers[i]);
                 }
         }

         // eventuell die Pixel Textur erstellen.
        EmuGraphicsAdapter.createOriginalPixelTex();

  • Mit den ifs am Anfang wird überprüft, ob ein Parameter vorhanden ist. Dann wird die zugehörige Variable gesetzt. Danach wird die "reale" Bildschirmgrösse neu berechnet.
  • EmuGraphicsAdapter.containers ist eine globale statische Variable und wird später erstellt. Hier wird überprüft, ob schon etwas vorhanden ist. Wenn ja, wird es von der PIXIStage gelöst.
  • EmuGraphicsAdapter.createOriginalPixelTex() ist eine statische globale Funktion, welche die weisse 2x2px Textur erstellt. Wenn sie schon vorhanden ist, muss sie nicht neu erstellt werden.
Nun wird eine Hintergrundtextur und die Rahmentextur erstellt. Die Hintergrundtextur braucht es nicht wirklich, ich erstelle sie jedoch trotzdem.


    // Hintergrund Textur
   var gbg = new PIXI.Graphics();
   gbg.beginFill(emuBackgroundColor, 1.0);
       gbg.drawRect(0,0, emuDrawWidth, emuDrawHeight);
   gbg.endFill();

   // Rahmen
   var gbord = new PIXI.Graphics();
   gbord.beginFill(emuBorderColor, 1.0);
      gbord.drawRect(0, 0, emuDrawWidth + emuBorderWidth * 2,  emuBorderWidth);
      gbord.drawRect(0, 0, emuBorderWidth, emuDrawHeight + emuBorderWidth * 2);
      gbord.drawRect(emuDrawWidth + emuBorderWidth, 0, emuBorderWidth, emuDrawHeight + emuBorderWidth * 2);
       gbord.drawRect(0, emuDrawHeight + emuBorderWidth, emuDrawWidth + emuBorderWidth * 2, emuBorderWidth);
   gbord.endFill();

   // "ziehe" Texturen davon.
   var backgroundTex = gbg.createCanvasTexture();
   var borderTex = gbord.createCanvasTexture();

   // Nun werden alle "Sprites" generiert.
   // Für jeden Buffer einmal. Zur Sicherheit.
   var backgroundSprite1 = new PIXI.Sprite(backgroundTex);
   var backgroundSprite2 = new PIXI.Sprite(backgroundTex);

    var borderSprite1 = new PIXI.Sprite(borderTex);
    var borderSprite2 = new PIXI.Sprite(borderTex);

    // zwei neue PIXI.Container mit dem zusammengesetzten Bild..
    var container1 = new PIXI.Container();
    var container2 = new PIXI.Container();

    // und das jeweilig zugehörige array.
    var arr1 = [];
    var arr2 = [];

    // ..und erstmal den Hintergrund hinzufügen.
    container1.addChild(backgroundSprite1);
    container2.addChild(backgroundSprite2);


Wir malen erstmal auf zwei PIXI.Graphics()-Objekten herum und generieren uns Texturen daraus.
Texturen kann man nicht direkt anzeigen, dazu werden Sprites benötigt. Eine Textur kann für mehrere Sprites verwendet werden. Sprites haben eine Position, Grösse und Rotation.

Am Schluss wird das Hintergrundsprite an die Buffer-Container angehängt.

Weiter oben haben wir die Funktion EmuGraphicsAdapter.createOriginalPixelTex() aufgerufen. Mit der daraus generierten Textur können wir nun endlich die Pixel generieren...


    for(var y=0; y<emuScreenHeight; y++)
    {
        for(var x=0; x<emuScreenWidth; x++)
        {
            var pixel1 = new PIXI.Sprite(EmuGraphicsAdapter.originalPixelTex);
            pixel1.x = x*2;
            pixel1.y = y*2;

            var pixel2 = new PIXI.Sprite(EmuGraphicsAdapter.originalPixelTex);
            pixel2.x = x*2;
            pixel2.y = y*2;

            container1.addChild(pixel1);
            container2.addChild(pixel2);

            arr1.push(pixel1);
            arr2.push(pixel2);
        }
    }


Wir haben die Pixel generiert und an die richtige Position verschoben an die Container angehängt sowie auch an das zugehörige Array. Um eine x/y-Position im Array zu finden, brauchen wir folgende Formel: index = y*screenX + x
Nun muss nur noch der Rahmen an die Container angehängt werden. Dieser wird noch ein bisschen nach links oben verschoben. Dann wird das Ganze noch zusammengesetzt und schon haben wir unseren Emulator-Bildschirm:


    borderSprite1.x = - emuBorderWidth;
    borderSprite1.y = - emuBorderWidth;
    borderSprite2.x = - emuBorderWidth;
    borderSprite2.y = - emuBorderWidth;
    container1.addChild(borderSprite1);
    container2.addChild(borderSprite2);

    // Buffer-Arrays neu initialisieren und dann aufbauen.
    EmuGraphicsAdapter.containers = [];
    EmuGraphicsAdapter.containers.push(container1);
    EmuGraphicsAdapter.containers.push(container2);

    screenArray = [];
    screenArray.push(arr1);
    screenArray.push(arr2);

    // Buffer index resetten.
    doubleBufferIndex = 0;

    // Schliesslich noch an den Root-Container anhängen.
    PIXIStage.addChild(container1);
    PIXIStage.addChild(container2);


Schliesslich und endlich wird der Bildschirm noch zentriert und dann der eine Buffer unsichtbar gemacht. Dann werden die Buffer noch mit der Hintergrundfarbe "gefüllt".


    this.reposition();
    this.switchBuffers();
    fillBuffer(emuBackgroundColor, 0);
    fillBuffer(emuBackgroundColor, 1);

    console.log("EmuGraphicsAdapter: Screen with size "+emuScreenWidth+"x"+emuScreenHeight+" created.");

}


Das war die Initialisierungsfunktion. Dies war so ziemlich die komplexeste Funktion, alle anderen werden einfacher sein. Es fehlen noch einige Funktionen welche hier benutzt werden.

Weiter gehts mit dem Rest vom grundlegenden Graphikaufbau (Teil 2.1_2).

Keine Kommentare:

Kommentar veröffentlichen