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.
Posts mit dem Label unity werden angezeigt. Alle Posts anzeigen
Posts mit dem Label unity werden angezeigt. Alle Posts anzeigen

Montag, 18. November 2019

Unity: NavMesh-Debugging

Um ein Waypoint-System zu bauen habe ich mich informiert, wie man eine Linie in Unity zeichnen kann. Das kann man auch NUR IM EDITOR, was sehr hilfreich ist.

Dazu gibt es die Funktionen "OnDrawGizmos" und "OnDrawGizmosSelected".

Hier ist der Code, welcher eine Linie vom aktuellen zum nächsten NavMesh-zieht.

Erstelle dieses Script, hänge es an ein GameObject ran und ziehe dir ein Prefab davon:

public class NavCon
{
   public GameObject nextWaypoint;
   protected Color actualColor = Color.blue;

   private void OnDrawGizmos()
   {
        if(nextWaypoint)
        {
            Gizmos.color = actualColor;
            Gizmos.DrawLine(gameObject.transform.position, nextWaypoint.transform.position, actualColor);
        }
        // reset the color.
        actualColor = Color.blue
   }

   private void OnDrawGizmosSelected() {actualColor = Color.green;}
}


Wenn du nun ein anderes GameObject auf das "nextWaypoint"-Feld im Inspector ziehst, wird im Editor eine blaue oder grüne Linie zwischen den beiden GameObjects gezeichnet. Da es keine "OnDrawGizmosUnselected"-Funktion gibt, muss die Farbe nach jedem Zeichnen ge-resetted werden.

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.

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. :)