Die Rückseite des Neuromancers. Teil 3: Fertiges Rendern, mach das Spiel


Hallo, dies ist der dritte Teil einer Reihe meiner Veröffentlichungen, die sich mit der umgekehrten Entwicklung von Neuromancer befassen - einer Videospiel-Verkörperung des gleichnamigen Romans von William Gibson.


Die Rückseite des Neuromancers. Teil 1: Sprites
Die Rückseite des Neuromancers. Teil 2: Schriftart rendern

Dieser Teil mag etwas chaotisch erscheinen. Tatsache ist, dass das meiste, was hier beschrieben wird, zum Zeitpunkt des Schreibens des vorherigen fertig war. Da seitdem bereits zwei Monate vergangen sind und ich leider nicht die Gewohnheit habe, Arbeitsnotizen zu führen, habe ich einfach einige Details vergessen. Aber wie es ist, lass uns gehen.




[Nachdem ich gelernt hatte, Zeilen zu drucken, wäre es logisch, die Konstruktion von Dialogfeldern weiter umzukehren. Aber aus irgendeinem Grund, der mir entgangen ist, habe ich mich stattdessen vollständig mit der Analyse des Renderingsystems befasst.] Wieder konnte ich den Anruf lokalisieren, der zuerst etwas auf dem Bildschirm anzeigt: seg000:0159: call sub_1D0B2 . "Alles" ist in diesem Fall der Cursor und das Hintergrundbild des Hauptmenüs:




Es ist bemerkenswert, dass die Funktion sub_1D0B2 [im Folgenden - render ] keine Argumente hat. Dem ersten Aufruf gehen jedoch zwei, fast identische Codeabschnitte voraus:


 loc_100E5: loc_10123: mov ax, 2 mov ax, 2 mov dx, seg seg009 mov dx, seg seg010 push dx push dx push ax push ax mov ax, 506Ah mov ax, 5076h ; "cursors.imh", "title.imh" push ax push ax call load_imh call load_imh ; load_imh(res, offt, seg) add sp, 6 add sp, 6 sub ax, ax sub ax, 0Ah push ax push ax call sub_123F8 call sub_123F8 ; sub_123F8(0), sub_123F8(10) add sp, 2 add sp, 2 cmp word_5AA92, 0 mov ax, 1 jz short loc_10123 push ax sub ax, ax mov ax, 2 push ax mov dx, seg seg010 mov ax, 2 push dx mov dx, seg seg009 push ax push dx sub ax, ax push ax push ax mov ax, 64h push ax push ax mov ax 0Ah mov ax, 0A0h push ax push ax call sub_1CF5B ; sub_1CF5B(10, 0, 0, 2, seg010, 1) sub ax, ax add sp, 0Ch push ax call render call sub_1CF5B ; sub_1CF5B(0, 160, 100, 2, seg009, 0) add sp, 0Ch 

Vor dem Aufruf von render werden die Cursor ( cursors.imh ) und der Hintergrund ( title.imh ) in den Speicher ( load_imh ist das umbenannte sub_126CB aus dem ersten Teil ) in das neunte bzw. zehnte Segment entpackt. Eine oberflächliche Untersuchung der Funktion sub_123F8 brachte mir keine neuen Informationen, aber ich habe nur die Argumente von sub_1CF5B und die folgenden Schlussfolgerungen gezogen:


  • Die Argumente 4 und 5 stellen zusammen die Adresse des dekomprimierten Sprites dar ( segment:offset ).
  • Die Argumente 2 und 3 sind wahrscheinlich Koordinaten, da diese Zahlen mit dem Bild korrelieren, das nach dem Aufruf von render angezeigt wird.
  • Das letzte Argument kann das Flag für die Deckkraft des Hintergrunds des Sprites sein, da die entpackten Sprites einen schwarzen Hintergrund haben und wir den Cursor ohne diesen auf dem Bildschirm sehen.

Mit dem ersten Argument [und gleichzeitig mit dem Rendern im Allgemeinen] wurde alles klar, nachdem sub_1CF5B . Tatsache ist, dass sich im Datensegment, beginnend mit der Adresse 0x3BD4 , ein Array von 11 Strukturen des folgenden Typs befindet:


 typedef struct sprite_layer_t { uint8_t flags; uint8_t update; uint16_t left; uint16_t top; uint16_t dleft; uint16_t dtop; imh_hdr_t sprite_hdr; uint16_t sprite_segment; uint16_t sprite_pixels; imh_hdr_t _sprite_hdr; uint16_t _sprite_segment; uint16_t _sprite_pixels; } sprite_layer_t; 

Ich nenne dieses Konzept Sprite-Kette. Tatsächlich fügt die Funktion sub_1CF5B (im Folgenden add_sprite_to_chain ) das ausgewählte Sprite zur Kette hinzu. Auf einem 16-Bit-Computer hätte es ungefähr die folgende Signatur:


 sprite_layer_t g_sprite_chain[11]; void add_sprite_to_chain(int index, uint16_t left, uint16_t top, uint16_t offset, uint16_t segment, uint8_t opaque); 

Es funktioniert so:


  • Das erste Argument ist der Index im Array g_sprite_chain .
  • Das left und das top Argument werden in die g_sprite_chain[index].left und g_sprite_chain[index].top .
  • Der Sprite-Header (die ersten 8 Bytes im segment:offset ) wird in das g_sprite_chain[index].sprite_hdr kopiert. g_sprite_chain[index].sprite_hdr imh_hdr_t (vom ersten Teil in rle_hdr_t umbenannt):

 typedef struct imh_hdr_t { uint32_t unknown; uint16_t width; uint16_t height; } imh_hdr_t; 

  • Das Feld g_sprite_chain[index].sprite_segment zeichnet den Wert des segment .
  • In das g_sprite_chain[index].sprite_pixels wird ein Wert geschrieben, der dem offset + 8 sprite_segment:sprite_pixels ist die Adresse der Bitmap des hinzugefügten Sprites.
  • Die sprite_hdr , sprite_segment und sprite_pixels in _sprite_hdr , _sprite_segment und _sprite_pixels [warum? - Ich habe keine Ahnung, und dies ist nicht der einzige Fall einer solchen Vervielfältigung von Feldern .
  • In das Feld g_sprite_chain[index].flags Wert gleich 1 + (opaque << 4) geschrieben 1 + (opaque << 4) . Dieser Datensatz bedeutet, dass das erste Bit des flags Werts die "Aktivität" der "aktuellen" Ebene "und das fünfte Bit die Deckkraft ihres Hintergrunds angibt. [Meine Zweifel an der Transparenzflagge wurden ausgeräumt, nachdem ich ihre Wirkung auf das angezeigte Bild experimentell getestet hatte. Wenn Sie den Wert des fünften Bits zur Laufzeit ändern, können Sie diese Artefakte beobachten]:


Wie bereits erwähnt, hat die g_sprite_chain keine Argumente, benötigt sie jedoch nicht - sie arbeitet direkt mit dem Array g_sprite_chain und überträgt abwechselnd die "Ebenen" vom letzten ( g_sprite_chain[10] - Hintergrund) zum ersten ( g_sprite_chain[0] ) in den VGA- Speicher g_sprite_chain[0] - Vordergrund). Die sprite_layer_t Struktur bietet alles Notwendige dafür und noch mehr. Ich spreche über die nicht überprüften Felder update , dleft und dtop .


Tatsächlich zeichnet die Renderfunktion NICHT ALLE Sprites in jedem Frame neu. Ein Wert ungleich Null im Feld g_sprite_chain.update gibt an, dass das aktuelle Sprite neu gezeichnet werden muss. Angenommen, wir bewegen den Cursor ( g_sprite_chain[0] ), dann passiert so etwas im Mausbewegungshandler:


 void mouse_move_handler(...) { ... g_sprite_chain[0].update = 1; g_sprite_chain[0].dleft = mouse_x - g_sprite_chain[0].left; g_sprite_chain[0].dtop = mouse_y - g_sprite_chain[0].top; } 

Wenn die Steuerung an die g_sprite_chain[0] wird, sieht diese nach Erreichen der g_sprite_chain[0] , dass sie aktualisiert werden muss. Dann:


  • Der Schnittpunkt des Bereichs, den das Cursor-Sprite vor der Aktualisierung mit allen vorherigen Ebenen einnimmt, wird berechnet und gezeichnet.
  • Sprite-Koordinaten werden aktualisiert:

 g_sprite_chain[0].update = 0; g_sprite_chain[0].left += g_sprite_chain[0].dleft g_sprite_chain[0].dleft = 0; g_sprite_chain[0].top += g_sprite_chain[0].dtop g_sprite_chain[0].dtop = 0; 

  • Das Sprite wird an den aktualisierten Koordinaten gezeichnet.

Dies minimiert die Anzahl der von der render Operationen.




Es war nicht schwierig, diese Logik zu implementieren, obwohl ich sie ziemlich vereinfacht habe. Unter Berücksichtigung der Rechenleistung moderner Computer können wir es uns leisten, alle 11 Ketten-Sprites in jedem Frame neu zu zeichnen. .dleft .dtop g_sprite_chain.update , .dleft , .dtop und die damit verbundene Verarbeitung. Eine weitere Vereinfachung betrifft die Behandlung des Opazitätsflags. Im ursprünglichen Code wird für jedes transparente Pixel im Sprite der Schnittpunkt mit dem ersten undurchsichtigen Pixel in den unteren Ebenen gesucht. Ich verwende jedoch den 32-Bit-Videomodus und kann daher nur den Wert des Transparenzbytes im RGBA- Schema ändern. Als Ergebnis habe ich folgende Funktionen zum Hinzufügen (Löschen) eines Sprites zu (von) einer Kette (n) erhalten:


Code
 typedef struct sprite_layer_t { uint8_t flags; uint16_t left; uint16_t top; imh_hdr_t sprite_hdr; uint8_t *sprite_pixels; imh_hdr_t _sprite_hdr; uint8_t *_sprite_pixels; } sprite_layer_t; sprite_layer_t g_sprite_chain[11]; void add_sprite_to_chain(int n, uint32_t left, uint32_t top, uint8_t *sprite, int opaque) { assert(n <= 10); sprite_layer_t *layer = &g_sprite_chain[n]; memset(layer, 0, sizeof(sprite_layer_t)); layer->left = left; layer->top = top; memmove(&layer->sprite_hdr, sprite, sizeof(imh_hdr_t)); layer->sprite_pixels = sprite + sizeof(imh_hdr_t); memmove(&layer->_sprite_hdr, &layer->sprite_hdr, sizeof(imh_hdr_t) + sizeof(uint8_t*)); layer->flags = ((opaque << 4) & 16) | 1; } void remove_sprite_from_chain(int n) { assert(n <= 10); sprite_layer_t *layer = &g_sprite_chain[n]; memset(layer, 0, sizeof(sprite_layer_t)); } 

Die Funktion zum Übertragen einer Schicht in einen VGA- Puffer ist wie folgt:


 void draw_to_vga(int left, int top, uint32_t w, uint32_t h, uint8_t *pixels, int bg_transparency); void draw_sprite_to_vga(sprite_layer_t *sprite) { int32_t top = sprite->top; int32_t left = sprite->left; uint32_t w = sprite->sprite_hdr.width * 2; uint32_t h = sprite->sprite_hdr.height; uint32_t bg_transparency = ((sprite->flags >> 4) == 0); uint8_t *pixels = sprite->sprite_pixels; draw_to_vga(left, top, w, h, pixels, bg_transparency); } 

Die Funktion draw_to_vga ist die gleichnamige Funktion, die im zweiten Teil beschrieben wurde , jedoch mit einem zusätzlichen Argument, das die Transparenz des Bildhintergrunds angibt. Fügen draw_sprite_to_vga am Anfang der draw_sprite_to_vga Aufruf draw_sprite_to_vga (der Rest des Inhalts wurde aus dem zweiten Teil migriert):


 static void render() { for (int i = 10; i >= 0; i--) { if (!(g_sprite_chain[i].flags & 1)) { continue; } draw_sprite_to_vga(&g_sprite_chain[i]); } ... } 

Ich habe auch eine Funktion geschrieben, die die Position des Cursor-Sprites abhängig von der aktuellen Position des Mauszeigers ( update_cursor ) update_cursor , und einen einfachen Ressourcenmanager. Wir machen all diese Arbeit zusammen:


 typedef enum spite_chain_index_t { SCI_CURSOR = 0, SCI_BACKGRND = 10, SCI_TOTAL = 11 } spite_chain_index_t; uint8_t g_cursors[399]; /* seg009 */ uint8_t g_background[32063]; /* seg010 */ int main(int argc, char *argv[]) { ... assert(resource_manager_load("CURSORS.IMH", g_cursors)); add_sprite_to_chain(SCI_CURSOR, 160, 100, g_cursors, 0); assert(resource_manager_load("TITLE.IMH", g_background)); add_sprite_to_chain(SCI_BACKGRND, 0, 0, g_background, 1); while (sfRenderWindow_isOpen(g_window)) { ... update_cursor(); render(); } ... } 

Cursor.GIF



Okay, für ein vollwertiges Hauptmenü reicht das Menü selbst eigentlich nicht aus. Es ist Zeit, zur Umkehrung der Dialogfelder zurückzukehren. [Beim letzten Mal habe ich draw_frame Funktion draw_frame , die das Dialogfeld bildet, und teilweise die Funktion draw_string , wobei nur die draw_string von dort übernommen wurde.] Als ich die add_sprite_to_chain Funktion draw_frame , stellte ich fest, dass dort die Funktion add_sprite_to_chain verwendet wurde - nichts Überraschendes, nur ein Dialogfeld hinzuzufügen in Sprite-Kette. Es war notwendig, sich mit der Positionierung des Textes innerhalb des Dialogfelds zu befassen. Ich draw_string Sie daran erinnern, wie der Aufruf von draw_string aussieht:


  sub ax, ax push ax mov ax, 1 push ax mov ax, 5098h ; "New/Load" push ax call draw_string ; draw_string("New/Load", 1, 0) 

und die Struktur, die draw_frame [hier ist ein wenig voraus, da ich die meisten Elemente umbenannt habe, nachdem ich draw_string vollständig herausgefunden draw_string . Übrigens gibt es hier, wie im Fall von sprite_layer_t , eine Duplizierung von Feldern] :


 typedef struct neuro_dialog_t { uint16_t left; // word[0x65FA]: 0x20 uint16_t top; // word[0x65FC]: 0x98 uint16_t right; // word[0x65FE]: 0x7F uint16_t bottom; // word[0x6600]: 0xAF uint16_t inner_left; // word[0x6602]: 0x28 uint16_t inner_top; // word[0x6604]: 0xA0 uint16_t inner_right; // word[0x6604]: 0xA0 uint16_t inner_bottom; // word[0x6608]: 0xA7 uint16_t _inner_left; // word[0x660A]: 0x28 uint16_t _inner_top; // word[0x660C]: 0xA0 uint16_t _inner_right; // word[0x660E]: 0x77 uint16_t _inner_bottom; // word[0x6610]: 0xA7 uint16_t flags; // word[0x6612]: 0x06 uint16_t unknown; // word[0x6614]: 0x00 uint8_t padding[192] // ... uint16_t width; // word[0x66D6]: 0x30 uint16_t pixels_offset; // word[0x66D8]: 0x02 uint16_t pixels_segment; // word[0x66DA]: 0x22FB } neuro_dialog_t; 

Anstatt zu erklären, was hier ist, wie und warum, lasse ich einfach dieses Bild:



Die Variablen x_offt und y_offt sind das zweite und dritte Argument für die Funktion draw_string . Basierend auf diesen Informationen war es einfach, eigene Versionen von draw_frame und draw_text , nachdem diese in build_dialog_frame und build_dialog_text :


 void build_dialog_frame(neuro_dialog_t *dialog, uint16_t left, uint16_t top, uint16_t w, uint16_t h, uint16_t flags, uint8_t *pixels); void build_dialog_text(neuro_dialog_t *dialog, char *text, uint16_t x_offt, uint16_t y_offt); ... typedef enum spite_chain_index_t { SCI_CURSOR = 0, SCI_DIALOG = 2, ... } spite_chain_index_t; ... uint8_t *g_dialog = NULL; neuro_dialog_t g_menu_dialog; int main(int argc, char *argv[]) { ... assert(g_dialog = calloc(8192, 1)); build_dialog_frame(&g_menu_dialog, 32, 152, 96, 24, 6, g_dialog); build_dialog_text(&g_menu_dialog, "New/Load", 8, 0); add_sprite_to_chain(SCI_DIALOG, 32, 152, g_dialog, 1); ... } 


Der Hauptunterschied zwischen meinen Versionen und den Originalversionen besteht darin, dass ich absolute Werte für die Pixelgröße verwende - es ist einfacher.




Schon damals war ich mir sicher, dass der Codeabschnitt unmittelbar nach dem Aufruf von build_dialog_text für die Erstellung der Schaltflächen verantwortlich war:


  ... mov ax, 5098h ; "New/Load" push ax call build_dialog_text ; build_dialog_text("New/Load", 1, 0) add sp, 6 mov ax, 6Eh ; 'n' -  push ax sub ax, ax push ax mov ax, 3 push ax sub ax, ax push ax mov ax, 1 push ax call sub_181A3 ; sub_181A3(1, 0, 3, 0, 'n') add sp, 0Ah mov ax, 6Ch ; 'l' -      push ax mov ax, 1 push ax mov ax, 4 push ax sub ax, ax push ax mov ax, 5 push ax call sub_181A3 ; sub_181A3(5, 0, 4, 1, 'l') 

Es geht nur um diese generierten Kommentare - 'n' und 'l' , die offensichtlich die ersten Buchstaben in den Wörtern "New" und "load" . Wenn wir analog zu build_dialog_text argumentieren, können die ersten vier Argumente von sub_181A3 (im Folgenden - build_dialog_item ) Faktoren von Koordinaten und Größen sein [tatsächlich die ersten drei Argumente, das vierte, wie sich herausstellte, über das andere] . Alles konvergiert, wenn Sie diese Werte wie folgt über das Bild legen:



Die Variablen x_offt , y_offt und width im Bild sind jeweils die ersten drei Argumente der Funktion build_dialog_item . Die Höhe dieses Rechtecks ​​entspricht immer der Höhe des Symbols - acht. Nach einem build_dialog_item Blick auf build_dialog_item stellte ich fest, dass das, was ich in der neuro_dialog_t Struktur als padding (jetzt items ) bezeichnet habe, ein Array von 16 Strukturen der folgenden Form ist:


 typedef struct dialog_item_t { uint16_t left; uint16_t top; uint16_t right; uint16_t bottom; uint16_t unknown; /* index? */ char letter; } dialog_item_t; 

Und das Feld neuro_dialog_t.unknown (jetzt - neuro_dialog_t.items_count ) ist der Zähler für die Anzahl der Elemente im Menü:


 typedef struct neuro_dialog_t { ... uint16_t flags; uint16_t items_count; dialog_item_t items[16]; ... } neuro_dialog_t; 

Das Feld dialog_item_t.unknown mit dem vierten Argument für die Funktion dialog_item_t.unknown initialisiert. Vielleicht ist dies der Index des Elements im Array, aber es scheint, dass dies nicht immer der Fall ist und daher unknown . Das Feld dialog_item_t.letter mit dem fünften Argument für die Funktion dialog_item_t.letter initialisiert. Auch hier ist es möglich, dass das Spiel im Linksklick-Handler im Bereich eines der Elemente nach den Koordinaten des Mauszeigers sucht (z. B. nur nach Reihenfolge sortiert). Wenn ein Treffer erfolgt, wird aus diesem Feld der gewünschte Handler zum Klicken auf eine bestimmte Schaltfläche ausgewählt. [Ich weiß nicht, wie das tatsächlich gemacht wird, aber ich habe genau diese Logik implementiert.]


Dies reicht aus, um ein vollwertiges Hauptmenü zu erstellen, das nicht auf den ursprünglichen Code zurückblickt, sondern lediglich das im Spiel beobachtete Verhalten wiederholt.


Main_Menu.GIF



Wenn Sie das vorherige GIF bis zum Ende gesehen haben, haben Sie wahrscheinlich den Startbildschirm auf den letzten Frames bemerkt. Tatsächlich habe ich schon alles, um es zu zeichnen. Nehmen Sie es einfach und laden Sie die erforderlichen Sprites herunter und fügen Sie sie der Sprite-Kette hinzu. Als ich jedoch das Sprite der Hauptfigur auf die Bühne brachte, machte ich eine wichtige Entdeckung im Zusammenhang mit der Struktur imh_hdr_t .


Im ursprünglichen Code wird die Funktion add_sprite_to_chain , die das Bild des Protagonisten zur Kette hinzufügt, mit den Koordinaten 156 und 110 aufgerufen. add_sprite_to_chain ich gesehen und dies selbst wiederholt:



Nachdem ich herausgefunden hatte, was was ist, bekam ich die folgende Art von Struktur imh_hdr_t :


 typedef struct imh_hdr_t { uint16_t dx; uint16_t dy; uint16_t width; uint16_t height; } imh_hdr_t; 

Was früher ein unknown Feld war, stellte sich als Versatzwerte heraus, die von den entsprechenden Koordinaten (während des Renderns) abgezogen wurden, die in der Sprite-Kette gespeichert waren.




Somit wird die reale Koordinate der oberen linken Ecke des gezeichneten Sprites ungefähr so ​​berechnet:


 left = sprite_layer_t.left - sprite_layer_t.sprite_hdr.dx top = sprite_layer_t.top - sprite_layer_t.sprite_hdr.dy 

Wenn ich dies in meinem Code anwendete, bekam ich das richtige Bild und danach begann ich, die Hauptfigur wiederzubeleben. Tatsächlich habe ich den gesamten Code für die Steuerung des Charakters (Maus und Tastatur), seine Animation und Bewegung selbst geschrieben, ohne auf das Original zurückzublicken.


Moonwalk.gif



Habe ein Text-Intro für das erste Level. Ich .BIH Sie daran erinnern, dass Zeichenfolgenressourcen in .BIH Dateien gespeichert sind. .BIH Dateien bestehen aus einem Header mit variabler Größe und einer Folge von nullterminierten Zeichenfolgen. Bei der Untersuchung des Originalcodes, der das Intro .BIH , stellte ich fest, dass der Versatz des Anfangs des Textteils in der .BIH Datei in der vierten Überschrift enthalten ist. Die erste Zeile ist das Intro:


 typedef struct bih_hdr_t { uint16_t unknown[3]; uint16_t text_offset; } bih_hdr_t; ... uint8_t r1_bih[12288]; assert(resource_manager_load("R1.BIH", r1_bih)); bih_hdr_t *hdr = (bih_hdr_t*)r1_bih; char *intro = r1_bih + hdr->text_offset; 

Ausgehend vom Original habe ich die Aufteilung der Originalzeichenfolge in Teilzeichenfolgen implementiert, sodass sie in den Bereich für die Textausgabe passen, durch diese Zeilen scrollen und auf die Eingabe warten, bevor der nächste Stapel ausgegeben wird.


Intro.GIF



Zum Zeitpunkt der Veröffentlichung habe ich zusätzlich zu dem, was bereits in drei Teilen beschrieben wurde, die Wiedergabe von Ton herausgefunden. Bisher ist dies nur in meinem Kopf und es wird einige Zeit dauern, dies in meinem Projekt zu realisieren. Im vierten Teil geht es also wahrscheinlich nur um den Klang. Ich habe auch vor, ein wenig über die Architektur des Projekts zu erzählen, aber mal sehen, wie es geht.


Die Rückseite des Neuromancers. Teil 4: Sound, Animation, Huffman, Github

Source: https://habr.com/ru/post/de415555/


All Articles