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).
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. :)
Keine Kommentare:
Kommentar veröffentlichen