COOKIES

This site may be using cookies to melk you with your own data. I, ben0bi, am not the owner of this web service and I also do not maintain their servers. But the EU and the owner of this service think, that the user (me) has the responsibility to inform the consumer (you), that this website uses cookies. Again: I, ben0bi, NEVER use cookies. I am not responsible for the setup of this web service. I just present some information here and do not intend to spy on you for whatever reason ever. But (also again), I do not host this website nor do I maintain any servers related to this website nor do I benefit from using the cookies maintained from this service. I hereby give the responsibility for using cookies on blogspot back to the owners of blogspot.

Mittwoch, 23. Oktober 2019

Das Buch der Helden und Abenteuer

Das Net(te)Buch der Helden und Abenteuer

Zutaten:

  • 1x Netbook im A4-Format. (Meins: Acer Spin 1)
  • 1x Buch oder Comic im A4-Format mit festem Einband, gebunden, innere Buchhöhe mindestens 8mm.
  • 1x Rolle Doppelklebeband, möglichst fest klebend.
  • 1x Cutter

Aufbau:

  • Man schneide die kleinen Gummiknubel unten am Netbook ab, oder reisse sie heraus, welche als Stand dienen.
  • Man schneide mit dem Cutter alle Seiten aus dem Buch heraus, und zwar so, dass die Bindung noch ganz bleibt wenn möglich.
  • Man gucke, wie das Netbook am besten in die Hülle passt.
  • Man beklebe die Ober- und Unterseite des Netbooks mit Doppelklebeband, und zwar in Flicken, damits nicht gleich alles abreisst, wenns was abreisst.
  • Und wickle dann die Hülle drum herum. Und presse sie an. Man habe nun weiterhin Spass mit dem Netbook, auch wenn man es nicht mehr ganz umdrehen kann (Spin ist ein Convertible).



PS: Für mich ist das ein guter Trick, damit ich nicht immer den Bildschirm aufmachen will und es trotzdem in Sichtweite haben kann. PPS: Ich habe das vier Freunden jeweils in die Hand gedrückt mit "Guck mal, das ist das beste Buch ever..." und die haben erst gemerkt, dass da ein Laptop drin ist, als sie es jeweils aufgemacht haben. Obwohl sie es auch halb von der Seite angeguckt haben und vorher mindestens 1 Minute in den Pfoten hatten...perfekte Tarnung. :)

Samstag, 19. Oktober 2019

DIY Emulator Teil 2.1.1: Refining EmuGraphicsAdapter

DIY Emulator Teil 2.1.1: Refining EmuGraphicsAdapter

in Unity

Nachdem ich in diesem Artikel gelernt habe, dass schon kleinste Änderungen am Code enorm mehr Speed herausholen können, werden nun erstmal ein paar Speed-Optimierungen gemacht.

(Ich mache dann (eventuell :) ) schon mal einen "richtigen" Emulator aber ich muss erst noch rausfinden, wie man einen emulierten (schon programmierten) Chip überhaupt einbaut und benutzt und davor hab ich schon ein bisschen Bammel weil ich wirklich absolut keine Ahnung davon habe. Bitte entschuldigt das andauernde Aufgeschiebe. Diese Tutorials sollen ja auch für mich sein, nicht nur für dich... ;) )

Darum wurde dort die MarkPixelToChangeByIndex-Funktion im EmuGraphicsAdapter (der Klasse aus diesem Artikel) hinzugefügt.

Um noch mehr Speed herauszuholen - denn millionenfache Funktionsaufrufe brauchen dann doch ein bisschen Zeit - werden wir nun noch eine Funktion generieren, die gleich eine gesamte Map (Display) mit der zugehörigen Palette direkt auf der Textur verrechnet, ohne dazu immer extra pro Pixel eine Funktion aufrufen zu müssen.*

[edit]: Wenn dein Emulator Display genau gleich gross ist, wie die Textur und auf 0,0 positioniert wird (also keine "hidden Pixel" hat), ist MarkPixelToChangeByIndex immer noch ein bisschen schneller als die folgende Funktion.*

 [edit 2]: Ich bin grüble darüber nach, ob und wie man das mit Shadern "richtig schnell" machen kann. Anders gesagt: Auch wenn hierbei kein Emulator rauskommt, hast du dann trotzdem eine richtig flotte 2D-Pixelzeichungs-Engine parat.

*Desweiteren wird diese Funktion auch das Display des Emulators "korrekt" auf der Textur platzieren:
Einige Systeme haben einen grösseren Display-Speicher als auf dem Bildschirm angezeigt. Eine oder mehrere Linien oder Zeilen sind ausserhalb des sichtbaren Bereiches. Diese "hidden pixel" werden vor allem bei Scrolling-Funktionen benutzt, damit es kein Geflacker gibt.

Wir könnten entweder die Texturgrösse anpassen, oder einfach diese Pixel auslassen. Ich habe mich erstmal für zweiteres entschieden.

Schlussendlich gibt es noch einen paletteModifier, so dass man nicht immer vorher die gesamte Map mit neuen Indexen aufbauen muss, sondern nur diesen Modifier verändert. (Wird durch den plasmaIndex im Placeholder_Emulator bestimmt.)

Also, hier die Funktion: MarkPalettedMapToChange1D in der EmuGraphicsAdapter-Klasse
Die Funktion hat ziemlich viele Parameter, Sorry dafür.

  /* Parameters:
     * map: 1-dimensional array with palette indexes.
     * mapwidth: width (max-x) of the map to make it 2-dimensional.
     * mapheight: height (max-y) of the map to make it 2-dimensional.
                  mapheight can be calculated in-function but that wastes processing power.
     * palette: 1-dimensional array with colors arranged in the right order.
     * paletteModifier: add this modifier to the palette index on the map to get the right color.
     *                  used to not create the whole map with each palette change.
     * posX: start-position of the map on the display. used for displays with "hidden pixels".
     * posY: start-position of the map on the display. used for displays with "hidden pixels".
     */
    // define the variables once instead of in each function call.
    private int tmp_texx, tmp_texy, tmp_texidx, tmp_emux, tmp_emuy, tmp_emuidx;
    private int tmp_palsize, tmp_palindex;
    private int tmp_spritewidth, tmp_spriteheight, tmp_mapsize;
    public void MarkPalettedMapToChange1D(int[] map, int mapwidth, int mapheight,
                                          Color[] palette, int paletteModifier = 0,
                                          int posX = 0, int posY = 0)
    {
        // get spritewidth and height.
        tmp_spritewidth = (int)m_drawable_sprite.rect.width;
        tmp_spriteheight = (int)m_drawable_sprite.rect.height;
        tmp_mapsize = map.Length;
        tmp_palsize = palette.Length;
        // go through x and y instead of only the index.
        // for better calculation of the emulator-display-position.
        // go through the DISPLAYED texture array:
        for(tmp_texy=0;tmp_texy < tmp_spriteheight;tmp_texy++)
        {
            tmp_emuy = tmp_texy - posY;
            // y not in bounds, break or continue.
            if (tmp_emuy < 0) // < 0 = continue
                continue;
            if(tmp_emuy >= mapheight) // > height = break
                break;
            for (tmp_texx = 0; tmp_texx < tmp_spritewidth; tmp_texx++)
            {
                // calculate position on the EMULATOR display:
                tmp_emux = tmp_texx - posX;
                // x is not in bounds, break or continue.
                if (tmp_emux < 0)
                    continue; // continue if smaller.
                if (tmp_emux >= mapwidth)
                    break; // break if bigger.

                // calculate the real index on the map.
                // because x and y are in boundsl, we do not need to check the index itself.
                tmp_emuidx = (int)((tmp_emuy * mapwidth) + tmp_emux);

                // get the palette index
                tmp_palindex = map[tmp_emuidx] + paletteModifier;
                // maybe we need to adjust the palette index to get it into bounds.
                while (tmp_palindex >= tmp_palsize)
                    tmp_palindex -= tmp_palsize;

                // finally set the new color.
                tmp_texidx = tmp_texy * tmp_spritewidth + tmp_texx;
                m_cur_colors[tmp_texidx] = palette[tmp_palindex];
            }
         }
    }

Alle tmp-Variablen werden im Scope der Klasse erstellt, damit sie nicht bei jedem Aufruf der Funktion neu erstellt werden müssen. Die Parameter der Funktion sind die folgenden:
  • map: 1-dimensionales Array mit Palette-Indexen.
  • mapwidth: Breite einer Zeile der map (x)
  • mapheight: Höhe der map (y) (Könnte man auch in der Funktion jedesmal neu berechnen, was jedoch unnötig Prozessorpower verbraucht.)
  • palette: 1-dimensionales Array mit den Farben (Color) in der richtigen Reihenfolge.
  • paletteModifier: Addiere diese Zahl zu jedem Index auf der map.
  • posX: Startposition der map auf der Textur in Pixeln (x).
  • posY: Startposition der map auf der Textur in Pixeln (y).
Es wird, obwohl die Arrays eindimensional sind, trotzdem 2-dimensional durch das Textur-Array hindurch gegangen. Dabei wird jeweils die Position des Pixels auf der map berechnet, und ob diese Position überhaupt auf der map vorhanden ist. Dazu braucht es mapwidth und mapheight. Wenn x oder y auf der map kleiner als 0 ist, wird die Schleife mit continue sofort fortgesetzt, wenn die jeweilige Variable jedoch zu gross ist, wird die zugehörige Schleife sofort mit break gleich ganz verlassen. PS: Einige Checks habe ich wieder herausgenommen, da sie nur unnötig Prozessorzeit verbrauchten und nie "erfüllt" wurden. Dies hier ist die aktuellste Version.

Nun können wir noch die copyDisplay-Funktion im Placeholder_Emulator ändern:

    // copy the paletted display colors to the real color array.
    protected void copyDisplay()
    {
        // alt: for(q = 0; ...
        gfx.MarkPalettedMapToChange1D(m_display, disp_width, disp_height,
                                      m_palette, (int)m_plasmaIndex, 0, 0);
    }

Uuund....weiteres folgt bald. Eventuell.

Das Projekt befindet sich in diesem Git-Repository, im JUMPEE-Verzeichnis.

Freitag, 18. Oktober 2019

SEGA Router

Ich hatte noch die Hülle eines Master-Systems zur Verfügung.


Und eine ausgebaute Karte von einem DLINK GO-RT N300.

Von diesem Modell habe ich 2 Stück, also baue ich die Karte mal in den SEGA rein und mache das Ding in meine Garage.

DLink bietet bei diesem Modell noch eine Gratis DynDns-Adresse an pro Gerät. Ich weiss nicht ob das bei neueren Modellen immer noch so ist. Nur deswegen läuft mein Server seit Jahren von zuhause aus. Ich kann das Ding nur empfehlen: Das einzige Problem bis jetzt ist der Powerknopf, der bei beiden Modellen ein bisschen wackelig ist. (Bei der Karte kommt er zB. nicht mehr aus der Fassung raus, da muss man rausziehen, und bei meinem anderen Modell ist es auch recht fitzelig.) Dafür läuft das Teil aber schon seit c.a. 2012, also...

Falls du das auch machen willst:
Schraube den SEGA auf und werkel alles raus, so dass nur noch die Hülle (oben und unten) sowie der Kartenhalter im Slot auf der Oberseite übrig bleibt. Auch die Metallplatte am Boden muss raus, bis nur noch Plastik übrig ist.

Den Kartenhalter-Slot dran lassen.
Schraube den Router auf und nimm die Karte heraus.

Die Karte passt mit den Anschlüssen und dem Power-Knopf GENAU in den Kartenslot vom SEGA rein.
 


Du musst nur noch das Kabel unten anlöten, damit der Stecker nicht die Slot-Abdeckung blockiert.

Zum Glück sind die Lötstellen ziemlich gross.

Knoten rein und durchs Loch für mehr Stabilität.



Das Kabel vom Dlink-Router passt mit dem Stecker schön durch das Loch hinten auf der Unterseite.
([edit] Den Stecker brauchts ja gar nicht mehr...hihihi)



Nun alles wieder zusammengeschraubt....



Du kannst im Kartenslot die LAN-Kabel einstecken und den Power-Knopf bedienen. Ich finds super.



Im laufenden Betrieb sieht man durch die Löcher von den Gamepads vorne die grünen Lichter leuchten. Das Foto folgt, wenn das Scheissteil richtig läuft, denn zur Zeit bringe ich es noch nicht richtig in mein LAN rein, da hakt was...aber als Haupt-Router ginge es. :)


Mittwoch, 16. Oktober 2019

DIY Emulator Teil 2.1: Der Placeholder-Emulator

DIY Emulator Teil 2.1: Der Placeholder-Emulator

in Unity


(Interlude - Dieser Artikel ist für den weiteren Verlauf der Tutorials nicht relevant.)

Du solltest das Framework des vorigen Artikels nun aufgesetzt haben...

Nachdem wir in den vorigen zwei Artikeln ein kleines 2D-Framework für Emulatoren in Unity3D aufgesetzt haben, soll dieses Framework nun auch einmal etwas anzeigen.

Wie schon beschrieben ist der Placeholder-Emulator kein wirklicher Emulator sondern hat nur die selbe Klasse als Basis. Der Placeholder-Emulator läuft im Hauptmenü, wenn kein anderer Emulator geladen ist. Dies dient der Vereinfachung: Im System ist immer ein Emulator am laufen und somit muss kein Extracode für den Fall geschrieben werden, wenn eben kein Emulator geladen ist. Desweiteren kann man hier verschiedenste Sachen austesten, ohne gross etwas kaputt zu machen.

In diesem Artikel werden wir einen Plasma-Effekt generieren.

Dazu wird eine Farbpalette erstellt.
Dann wird das Display des Emulators mit zufälligen Indexen dieser Palette gefüllt.
Danach wird das Display "refined", also alle Pixel werden vom Wert her an die benachbarten angeglichen. Das gibt schöne "Hügel und Täler".

Dieses Bild aus Paletten-Indexen dient nun als Ausgangsbasis.

In Jedem Frame werden die Paletten-Indexe des Displays als Farbe (Color) aus der Palette geholt und auf die Textur "gemalt". Dabei wird ein "plasmaIndex" dazu gerechnet, welcher das Plasma...plasmieren lässt.

Alles was man pro Frame ändern muss ist der plasmaIndex, und dann natürlich noch das Display in Colors umrechnen.

Wir nehmen den Placeholder-Emulator und erweitern diesen (mehrere Abschnitte):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Placeholder_Emulator : EmulatorBase
{
    protected int[] m_display; // pos = y * disp_width + x
    protected Color[] m_palette;
    protected int m_palSize = 0xFF; // palsize is one byte. :)

    // this index is added to the actual map palette index to "move" the colors.
    protected float m_plasmaIndex = 0.0f;

    // we need to slow down the things a little.
    protected int colorsPerSecond = 20;

    public Placeholder_Emulator(GameObject g) : base(g)
    {
        Debug.Log("EPlaceholder_Constructor");
        disp_width = 320;
        disp_height = 240;
        m_display = new int[disp_width * disp_height];

        createPalette();
        createPlasmaField();
    }

    public override void Update()
    {
        copyDisplay();

        m_plasmaIndex+=Time.deltaTime*colorsPerSecond;
        if ((int)m_plasmaIndex >= m_palSize)
            m_plasmaIndex = (float)m_palSize-m_plasmaIndex;
    }

Der Placeholder_Emulator ist von der vorher erstellten EmulatorBase abgeleitet.
  • m_display ist das der Textur entsprechende Array mit den Palette-Indexen für das Plasma-Bild.
  • m_palette ist das Array mit den Colors für die Indexe in m_display.
  • m_palSize ist die Grösse der Palette.
  • m_plasmaIndex ist der aktuelle Index, welcher auf jeden Index in m_display aufgerechnet wird.
  • colorsPerSecond zeigt an, wieviele Farbwechsel (Änderung von m_plasmaIndex) es pro Sekunde geben soll.
  • Im Konstruktor wird das GameObject (von EmuCreator) an den Basis-Konstruktor übergeben.
    Die Grösse des Displays wird auf 320x240 (PC-SCREEN 13) eingestellt und m_display wird initialisiert. Dann werden die Palette und das Plasmabild erstellt mit createPalette und createPlasmaField.
  • Schliesslich wird in jedem Update das Display per copyDisplay auf die Textur "gerendert". Dann wird der plasmaIndex um frameTime * colorsPerSecond erhöht.

createPalette()

Erst mal die createPalette-Funktion:

    // create a palette.
    protected void createPalette()
    {
        m_palette = new Color[m_palSize];

        float r = 0.0f;
        float g = 0.0f;
        float b = 0.0f;
        float eight = m_palSize / 8.0f;
        for(int i=0;i < m_palSize;i++)
        {
            // first red to yellow
            if (i<=eight)
            {
                r = 1.0f;
                g = 1.0f / eight * i;
                b = 0.0f;
            }

            // not to white because it's to bright.
            // then yellow to black
            if(i>eight && i<=eight*2)
            {
                r = 1.0f - (1.0f / eight * (i - (eight * 1))); // reverse;
                g = 1.0f - (1.0f / eight * (i - (eight * 1))); // reverse;
                b = 0.0f;
            }

            // then black to turkis
            if (i > eight*2 && i <= eight * 3)
            {
                r = 0.0f;
                g = 1.0f / eight * (i - (eight * 2));
                b = 1.0f/eight*(i-(eight*2));
            }

            // then turkis to green
            if (i > eight * 3 && i <= eight * 4)
            {
                r = 0.0f;
                g = 1.0f;
                b = 1.0f - (1.0f / eight * (i - (eight * 3)));  
            }

            // then green to black
            if (i > eight * 4 && i <= eight * 5)
            {
                r = 0.0f;
                g = 1.0f - (1.0f / eight * (i - (eight * 4))); // reverse
                b = 0.0f;
            }

            // then black to blue
            if (i > eight * 5 && i <= eight * 6)
            {
                r = 0.0f;
                g = 0.0f;
                b = 1.0f / eight * (i - (eight * 5));
            }

            // then blue to magenta
            if (i > eight * 6 && i <= eight * 7)
            {
                r = 1.0f / eight * (i - (eight * 6));
                g = 0.0f;
                b = 1.0f;
            }

            // then magenta to red
            if (i > eight * 7)
            {
                r = 1.0f;
                g = 0.0f;
                b = 1.0f - (1.0f / eight * (i - (eight * 7))); // reverse
            }

            m_palette[i] = new Color(r, g, b);
        }
    }


Diese Palette geht durch alle Farben ausser Weiss (das war zu hell, ich habe es durch Schwarz ersetzt. Rate, wo. ;) ). Darum braucht es acht Abschnitte. Die Variable eight ist ein Achtel der Gesamtpalette. So kann man in jedem Unterabschnitt mit eight und dem Index von 0.0f bis 1.0f oder umgekehrt gehen.

Zunehmende Farbe ist: 1.0f/eight * i
Abnehmende Farbe ist: 1.0f-(1.0f/eight * i)

Dazu muss man von i jeweils noch ein paar Achtel abziehen, damit i immer im Bereich von 0 bis eight ist.

Die Reihenfolge der Farben ist die folgende:  Rot, Gelb, Schwarz, Türkis, Grün, Schwarz, Blau, Magenta, Rot

createPlasmaField()

Nun die createPlasmaField-Funktion:
Erst wird das gesamte Display mit Zufallswerten von 0 bis zur Palettengrösse gefüllt.
Dann wird vier mal durch das Display durchgegangen. Die umliegenden und der aktuelle Pixel werden zusammengezählt und der Durchschnitt des Ergebnisses ausgerechnet. Der Dividor für den Durchschnitt wird jedesmal neu berechnet, da es am Rand weniger Pixel hat.

    protected void createPlasmaField()
    {
         // initialize the display array.
         int m_display = new int[disp_width * disp_height];
         // fill the display with random values.
         for(int i=0;i<m_display.Length;i++)
              m_display[i] = (int)Random.Range(0, m_palSize);

         // go several times through the whole display and
         // smoothen the pixel color(-indexes)
         // 4 steps are appropriate: less do it carvy, more flatten it out to one color.
         int dividor = 0;
 
         int newcol = 0; // color is a palette index, not a color.
         for(int steps=0;steps < 4;steps++)
         {
             // go through x and y instead of mapIndexes because...
             for(int y = 0;y < disp_height; y++)
             {
                for(int x = 0;x < disp_width; x++)
                {
                    dividor = 0;
                    // .. we need to get the right position here.
                    for(int yp= -1;yp<=1;yp++)
                    {
                       for(int xp= -1;xp<=1;xp++)
                       {
                          // get x and y of the pixel to add
                          int newx = x + xp;
                          int newy = y + yp;
                          // check if it is in bounds.
                          if(newx >= 0 && newx < disp_width &&
                             newy >= 0 && newy < disp_height)
                           {
                              dividor++;
                              int idx = newy*disp_height + newx;
                              newcol += m_display[idx];
                           }   
                       }
                    }
                    // divide by dividor.
                    newcol = (int)newcol / dividor;
                    // set new, "smooth" palette index.
                    m_display[y*disp_width +x]=newcol;
                }
             }
         }
    }

In dieser Funktion hat es ziemlich viele for-s, doch das macht nichts, da sie nur einmal aufgerufen wird. Bei Update allerdings musste ich mehrmals "drüber", da schon kleinste Multiplikationen (zB. index aus x,y ausrechnen pro Pixel) einen enormen Einfluss auf den Verarbeitungs-Speed haben (bei einem Array in der Grösse eines Displays). Im EmuGraphicsAdapter hat es dafür eine neue Funktion MarkPixelToChangeByIndex. Eventuell kommt später noch eine "CopyPaletteToTexture(map, palette)" in den EmuGraphicsAdapter, um nur einen Funktions-Call zu machen statt xTausend, doch bis jetzt reicht das.

Was genau gemacht wird, ist oben schon erklärt. Mit den innersten Schleifen werden die Pixel um x und y herum ausgelesen.

copyDisplay():

Schliesslich noch die copyDisplay-Funktion. Da das Display hier gleich gross ist wie die Textur, kann man direkt mit dem Index arbeiten.

    protected void copyDisplay()
    {
       // speed up the things a little with direct indexing.
       for(int i=0; i &lt m_display.Length; i++)
       {
          gfx.MarkPixelToChangeByIndex(i, m_palette[m_display[i]]);
       }
    }
} // end the class here.

Die vielen MarkPixelToChange-Aufrufe pro Frame sind nicht so der Bringer, das wird noch refined.

Desweiteren muss ja eigentlich nicht in jedem Frame das Bild neu gelöscht, aufgebaut und angezeigt werden, sondern nur wenn sich der plasmaIndex ändert (also alle 3 Frames, zur Zeit: 20 Änderungen auf 60 Frames pro Sekunde verteilt). Dazu müsste man switchBuffers aus dem EmuGraphicsAdapter-Update herausnehmen und im Emulator selbst aufrufen.

Ich lerne das Zeug auch alles gleich neu, da wird eventuell noch einiges geändert in älteren Artikeln.
[änderungen folgen] ;)

Jetzt musst du noch.....Play drücken. :)

Das Projekt findest du auch auf github, und zwar hier:
https://github.com/ben0bi/EmulatroniX
im Ordner "JUMPEE".

JUMPEE heisst "JavaScript & Unity Multi Purpose Emulator Environment"
naja, J müsste man rausnehmen, es ist ja jetzt C#. Aber dann tönts nicht mehr so gut....

Den Plasmaeffekt in JavaScript findest du zum angucken hier:
https://ben0bi.github.io/EmulatroniX

Dienstag, 15. Oktober 2019

DIY Emulator Teil 2.1: Plugin-Emulator (UNITY)

DIY Emulator Teil 2.1: Plugin-Emulator-Framework

in Unity

Nachdem wir in diesem Artikel ein grafisches Grundgerüst aufgesetzt haben, wird nun hier das Grundgerüst für die Emulatoren selbst aufgesetzt.

Die Applikation soll mehrere Emulatoren unterstützen können und mit "Plugins" aufgerüstet werden können. Um die Dinge zu vereinfachen, wird "immer" ein Emulator laufen, es gibt keinen spezifischen Hintergrund für die Applikation selbst. Jegliche Grafik bis auf das UI wird durch einen "Emulator" dargestellt. Das heisst, dass, während KEIN Emulator geladen ist, ein "Platzhalter-Emulator" läuft, welcher eine beliebige Grafikausgabe machen kann und auch nicht wirklich ein Emulator ist. Er hat nur die selbe Basisklasse wie ein Emulator.

Dazu gibt es mehrere Scripts:
  • Ein EmuCreator-Script, welches den Emulator auswählt, erstellt und initialisiert. Dieses wird an das GameObject mit dem EmuGraphicsAdapter (das Script aus dem vorigen Artikel) angehängt.
  • Eine Emulator-Basis von welcher alle Emulator-Klassen abgeleitet sind.
  • Eine abgeleitete Emulator-Klasse zu Testzwecken und später als Platzhalter-Emulator.
Erstellen wir also erstmal den EmuCreator und hängen dieses Script an das GameObject an. (In der Szene sollte nur ein GameObject vorhanden sein bis jetzt.)

EmuCreator.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(EmuGraphicsAdapter))]
public class EmuCreator : MonoBehaviour
{
    protected EmuGraphicsAdapter gfx;
    protected EmulatorBase m_emulator=null;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("EmuCreator started.");
        gfx = gameObject.GetComponent<EmuGraphicsAdapter>();

        // create an emulator just for testing.
        createEmulator(new Placeholder_Emulator(gameObject));
    }

    // Update is called once per frame
    void Update()
    {
        if (m_emulator!=null)
            m_emulator.Update();
    }

    // create an emulator and initialize it.
    void createEmulator(EmulatorBase emu)
    {
        m_emulator = emu;
        gfx.setEmuScreenSize(emu.getDisplayWidth(), emu.getDisplayHeight());
    }
}

  • gfx ist die Referenz auf die Graphik-Komponente, welche im vorigen Artikel erstellt wurde.
  • m_emulator ist die Instanz des aktuell laufenden Emulators.
Da dieses Script an das GameObjekt angehängt wird, werden Start und Update hier automatisch aufgerufen. Beim Emulator selbst gibt es keine Start-Funktion sondern einen Konstruktor und Update  muss von "hier" aus manuell ausgeführt werden.

Mit createEmulator(emulator) wird m_emulator gesetzt und dann die Bildschirmgrösse auf das Emulatordisplay angepasst.

Mehr braucht es hier gerade nicht. In Start wird der Platzhalter-Emulator gestartet, welcher im übernächsten Script beschrieben wird. Doch dafür brauchen wir erst die EmulatorBase-Klasse, welche als Basis für alle unsere Emulator-Plugins dienen wird.

EmulatorBase.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EmulatorBase
{
    protected GameObject gameObject;
    protected EmuGraphicsAdapter gfx;

    // Change those values and...go. ;)
    protected int disp_width = 20;
    protected int disp_height = 20;

    public int getDisplayWidth() { return disp_width; }
    public int getDisplayHeight() { return disp_height; }

    public EmulatorBase(GameObject g)
    {
        Debug.Log("EBASE Constructor");
        gameObject = g;
        gfx = g.GetComponent();
    }

    // Update is called once per frame
    public virtual void Update() { }
}

  • gfx ist nochmal/wieder die Referenz auf die Graphik-Komponente, welche im vorigen Artikel erstellt wurde.
  • gameObject ist das gameObject welches vom EmuCreator übergeben wird, also das gameObject, in welchem der EmuCreator als Komponente registriert ist.
  • disp_width und disp_height ist die native Grösse des Displays (in Pixeln) des jeweiligen Systems. Stelle diese Werte im Konstruktor ein.
Jede von dieser Basisklasse abgeleitete Klasse kann als Emulator in den EmuCreator "gestöpselt" werden. Update wird "normal" aufgerufen, also in jedem Frame. Update ist virtuell und muss in abgeleiteten Klassen mit override überschrieben werden. Im Konstruktor deines Emulators musst du nur noch disp_width und disp_height richtig einstellen und schon geht alles.

Dieser Platzhalter-"Emulator" setzt einfach einen Pixel. Später folgt noch ein schöner Grafikeffekt. ;)

Placeholder_Emulator.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Placeholder_Emulator : EmulatorBase
{
    public Placeholder_Emulator(GameObject g) : base(g)
    {
        Debug.Log("EPlaceholder_Constructor");
        disp_height = 50;
        disp_width = 50;
    }

    public override void Update()
    {
        gfx.MarkPixelToChange(19, 19, Color.white);
    }
}

Somit können nun verschiedenste Systeme über dieselbe Schnittstelle mit Unity "kommunizieren".

Ich hoffe, das hilft bei deiner Gesundung. :)

Das komplette Projekt befindet sich in diesem Git-Repository, im JUMPEE-Verzeichnis.

Weiter gehts mit ein bisschen Auflockerung: Ein Grafik-Effekt (ohne Shader - wenn mir das jemand in Shadern machen könnte, also das Display, das wär was.)

Freitag, 11. Oktober 2019

DIY Emulator Teil 2: Setup Basisgerüst (UNITY)

DIY: Emulator Teil 2: Setup Basisgerüst

in Unity

Früher hatte ich erwähnt, dass ich den Emulator am liebsten mit Unity3D machen würde, jedoch RTT (Render-To-Texture) damals nicht möglich war mit der frei verfügbaren Edition.

Nun, jetzt gibt es eine Möglichkeit.

Nachdem ich dieses Tutorial gefunden hatte,
habe ich erst mal das neueste Unity installiert: 2018.4.11f
und das Tutorial ausgetestet.

(Hier gehts zum vorigen Artikel: Teil 1: Research (deutsch)
Dabei ging ich noch von Javascript aus, dazu hat es auch ein paar Artikel.
JS war jedoch nur eine Notlösung, siehe oben...)

Nun gut, das läuft..

...machen wir was draus.

Wie schon beschrieben bin ich ein grafischer Mensch also wird erst mal ein Basis-Grafikgerüst aufgesetzt.

Mit dem Setup unten wird am Ende ein Bild auf dem Bildschirm dargestellt, auf welchem der Emulator sein Display zeichnet. Das Bild hat die Pixelgrösse des Emulator-Systems und wird auf Fullscreen hochskaliert. Wenn das keepAspectRatio-Flag gesetzt ist, wird das Bildseitenverhältnis des Original-Displays beibehalten. Dabei gibt es eventuell schwarze Balken an den Seiten oder oben und unten.

Der Emulator kann mit setEmuScreenSize die Displaygrösse anpassen und mit MarkPixelToChange die Farbe eines Pixels ändern.

Das Bild wird gebuffert, das heisst, im Hintergrund wird das Bild gezeichnet während im Vordergrund der "alte" Buffer angezeigt wird.

Man kann mehr als zwei Buffer-Images bestimmen oder auch nur eines: Dann wird direkt auf die sichtbare Textur gezeichnet.

Los gehts...

Lösche alles bis auf die Main-Kamera.

* Erstelle ein leeres GameObjekt auf Position 0,0,0
* Erstelle ein neues Script und hänge es an das GameObjekt an.

Das ist der Code, er wird unten erklärt:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EmuGraphicsAdapter : MonoBehaviour
{
    // each display has the same sprite size.
    protected Vector2 sprite_size;
    protected Vector2 local_sprite_size;

    // The actual emulator display game object which is visible on the screen.
    protected GameObject m_display_image;
    protected SpriteRenderer m_display_renderer;

    // at least 2 sprites for the buffering process.
    public int Buffer_Size = 2;
    protected Sprite[] m_buffer_sprites;
    protected int m_actualBuffer = 0;

    // keep the emulator aspect ratio?
    public bool keepAspectRatio = false;

#if (UNITY_EDITOR)
    private bool m_oldKeepAspectRatio = false;
#endif

    protected Texture2D m_drawable_texture;
    protected Sprite m_drawable_sprite;
    Color32 Reset_Colour = new Color32(0,0,69,0xFF);
    Color32[] m_cur_colors;

    // Start is called before the first frame update
    void Awake()
    {
        if (Buffer_Size < 1)
            Buffer_Size = 1;

        Debug.Log("EmuGraphicsAdapter started.");

        // create the display image game object.
        m_display_image = new GameObject("DISPLAY");
        m_display_image.AddComponent<SpriteRenderer>();
        // get the renderer to not search for it in each frame.
        m_display_renderer = m_display_image.GetComponent<SpriteRenderer>();

        // set some settings on the renderer.
        m_display_renderer.flipY = true;
        m_display_renderer.receiveShadows = false;
        m_display_renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;

        setEmuScreenSize(50, 50);
    }

    // call this function after the screen has resized or the sprite has changed.
    protected void resized()
    {
        // resize.
        var sprr = m_display_image.GetComponent<SpriteRenderer>();

        // The real image size in pixels for later.
        sprite_size = sprr.sprite.rect.size;
        // and in local space units.
        local_sprite_size = sprite_size / sprr.sprite.pixelsPerUnit;

        // get the screen height & width in world space units
        float worldSpriteWidth = sprr.sprite.bounds.size.x;
        float worldSpriteHeight = sprr.sprite.bounds.size.y;

        float worldScreenHeight = Camera.main.orthographicSize * 2.0f;
        float worldScreenWidth = (worldScreenHeight / Screen.height) * Screen.width;

        // initialize new scale to the current scale
        Vector3 newScale = m_display_image.transform.localScale;

        // resize the local scale to the world scale.
        newScale.x = worldScreenWidth / worldSpriteWidth;
        newScale.y = worldScreenHeight / worldSpriteHeight;

        // maybe keep the aspect ratio
        if (keepAspectRatio)
        {
           // get the aspect ratio of the sprite image.
           float aspectRatio = worldSpriteWidth / worldSpriteHeight;
           Debug.Log("Aspect Ratio: " + aspectRatio);
           float newy = newScale.x/aspectRatio;
           float newx = newScale.x;
           float wrldy = newy * local_sprite_size.y;
           // new height is bigger than screen height, switch aspect ratio calculation.
           if (wrldy > worldScreenHeight)
           {
                newy = newScale.y;
                newx = newScale.y * aspectRatio;
           }
           newScale.x = newx;
           newScale.y = newy;
        }
        // set the new local scale on display gameobject.
        m_display_image.transform.localScale = newScale;

        Debug.Log("Emulator display size (Image Pixels): x" + sprite_size.x + " y" + sprite_size.y+" (P->LS) x"+local_sprite_size.x+" y"+local_sprite_size.y);
        Debug.Log("Emulator display size (LS): x" + (newScale.x*local_sprite_size.x)+" y"+(newScale.y*local_sprite_size.y));
        Debug.Log("Real display size (WS): x" + worldScreenWidth + " y" + worldScreenHeight);
    }

    // switch the buffer images.
    protected void switchBuffers()
    {
        // apply all pixel changes to the draw texture.
        ApplyMarkedPixelChanges();

        // set the old stuff to the displayed image.
        m_display_renderer.sprite = m_buffer_sprites[m_actualBuffer];

        // set next buffer image.
        m_actualBuffer++;
        if (m_actualBuffer >= Buffer_Size)
            m_actualBuffer = 0;

        // set the new drawables.
        m_drawable_sprite = m_buffer_sprites[m_actualBuffer];
        m_drawable_texture = m_drawable_sprite.texture;

        m_cur_colors = m_drawable_texture.GetPixels32();

        clearDrawArray();
    }

    // create new textures for a new display size.
    public void setEmuScreenSize(int width, int height)
    {
        m_buffer_sprites = new Sprite[Buffer_Size];

        // create the buffer sprites.
        for (int i = 0; i < Buffer_Size; i++)
        {
            Debug.Log("Creating buffer image #" + i);
            Texture2D tex = new Texture2D(width, height);
            tex.filterMode = FilterMode.Point;
            //tex.Apply(false);
            Sprite spr = Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f));
            m_buffer_sprites[i] = spr;
        }

        m_display_renderer.sprite = m_buffer_sprites[0];

        // resize the displays.
        resized();

        // initialize the buffers, set the draw image etc.
        switchBuffers();
    }

    // set or unset the keep aspect ratio flag by code.
    void setKeepAspectRatio(bool setflag)
    {
        keepAspectRatio = setflag;
        resized();
    }

    // Update is called once per frame
    void Update()
    {
#if (UNITY_EDITOR)
        // maybe the keep aspect ratio flag has changed.
        // this will only happen in editor when you click the flag box.
        // else, setKeepAspectRatio(bool setflag) should be used.
        if(keepAspectRatio!=m_oldKeepAspectRatio)
        {
            m_oldKeepAspectRatio = keepAspectRatio;
            resized();
        }
#endif
      // Pixel Test
      //  MarkPixelToChange(10, 10, Color.green);
      //  MarkPixelToChange(11, 11, Color.yellow);
      //  MarkPixelToChange(15, 15, Color.yellow);
 
       switchBuffers();        
    }

    // DRAW FUNCTIONS
    public void MarkPixelToChange(int x, int y, Color color)
    {
        // Need to transform x and y coordinates to flat coordinates of array
        int array_pos = y * (int)m_drawable_sprite.rect.width + x;

        // Check if this is a valid position
        if (array_pos > m_cur_colors.Length || array_pos < 0)
            return;

        m_cur_colors[array_pos] = color;
    }
 
    public void MarkPixelToChangeByIndex(int index, Color color)
    {
        if(index>=0 && index<m_cur_colors.Length)
             m_cur_colors[index]=color; 
    } 
 
    // apply all pixel changes.
    public void ApplyMarkedPixelChanges()
    {
        if (!m_drawable_texture)
            return;

        m_drawable_texture.SetPixels32(m_cur_colors);
        m_drawable_texture.Apply();
    }

    // clear the drawing image.
    public void clearDrawArray()
    {
        for(int i=0;i < m_cur_colors.Length; i++)
        {
            m_cur_colors[i]=Reset_Colour;
        }
    }
}

Die Member-Variablen: 

  • sprite_size : Die Grösse des Emulator-Displays in Pixeln.
  • local_sprite_size: Die Grösse des Emulator-Displays in Units.
  • m_display_image: Das GameObjekt, in welchem der sichtbare Content dargestellt wird.
  • m_display_renderer: Die "SpriteRenderer"-Komponente von m_display_image. Damit muss man nicht immer wieder GetComponent aufrufen.
  • Buffer_Size: Wie viele Bilder werden gebuffert? Doublebuffer = 2, Triplebuffer = 3, etc.
  • m_buffer_sprites: Array mit der Anzahl an Buffer-Sprites welche in Buffer_Size angegeben ist.
  • m_actualBuffer: Der Index des aktuellen Buffer-Sprites in m_buffer_sprites.
  • keepAspectRatio: Das Bild wird auf den gesamten Bildschirm skaliert, wenn dieses Flag auf False ist. Ansonsten wird das Bildverhältnis (x/y) beibehalten und es gibt eventuell schwarze Balken am Rand. Dafür werden die Pixel korrekt dargestellt.
  • m_drawable_texture: Die aktuelle Textur, auf welcher gezeichnet wird.
  • m_drawable_sprite: Das zugehörige Sprite zu m_drawable_texture.
  • Reset_Colour: Die Hintergrundfarbe des Displays.
  • m_cur_colors: Array, auf welchem die Farben geändert werden können. Wird nach jedem Frame neu erstellt für den nächsten Buffer. Dieses Array wird später mit texture.setPixels(...) und texture.Apply() auf die Textur geladen. Wenn man das für jede Pixel-Änderung einzeln machen würde, wäre es wohl sehr langsam.

Die Funktionen:

  • Awake(): Diese Funktion wird VOR Start() aufgerufen und dient der Initialisierung "dieses" Objekts. Hier wird ein neues GameObjekt namens DISPLAY erstellt, welches die sichtbare Grafik darstellt. Dazu wird eine "SpriteRenderer"-Komponente an das "DISPLAY" angehängt und einige Werte eingestellt. Dann wird mit setEmuScreenSize(w, h) das Display initialisiert.
  • resized(): Wenn die Bildschirm- oder Displaygrösse geändert wird, sollte diese Funktion aufgerufen werden. Sie skaliert das Display auf die richtige Grösse.
  • switchBuffers(): Ruft ApplyMarkedPixelChanges() auf, setzt dann den neuen Buffer zum drauf zeichnen und zeigt den vorherigen (fertig gezeichneten) Buffer an.
  • setEmuScreenSize(width, height): Setze die Grösse des Emulator-Bildschirms in Pixeln. Danach wird resized() und switchBuffers() einmal aufgerufen.
  • setKeepAspectRatio(bool): Wenn dieses Flag gesetzt ist, wird das originale Seitenverhältnis beibehalten, ansonsten wird alles (x und y) auf den Bildschirm hochskaliert.
  • Update() : Update wird pro Frame ein mal aufgerufen. Home of switchBuffers() :)
  • MarkPixelToChange(x,y,color): "SetPixel" mit anderem Namen weil ApplyMarkedPixelChanges() aufgerufen werden muss, damit die Änderungen erkannt werden.
  • MarkPixelToChangeByIndex(index, color): Da das Emulator-Display normalerweise gleich gross ist wie das anzuzeigende Bild, kann man hier direkt mit dem Index arbeiten. Das verschnellert den Code ein bisschen.
  • ApplyMarkedPixelChanges(): Überträgt das mit MarkPixelToChange() modifizierte Color-Array auf die aktuelle Buffer-Textur.
  • clearDrawArray(): Setzt alle Pixel des Displays auf die angegebene Hintergrundfarbe (Reset_Colour).
Die Variablen zwischen #if(UNITY_EDITOR) und #endif werden im Spiel nicht gebraucht: Damit wird gecheckt, ob der Kasten im Editor angeklickt wurde.

Als nächstes wird ein Framework für die Emulatoren selbst erstellt:
DIY Emulator Teil 2.1: Plugin-Emulator (UNITY)

Das komplette Projekt befindet sich in diesem Git-Repository, im JUMPEE-Verzeichnis.

Ich hoffe, das gefällt. :)