El reverso del neuromancer. Parte 3: renderizado terminado, haz el juego


Hola, esta es la tercera parte de una serie de mis publicaciones dedicadas al desarrollo inverso de Neuromancer, una realización de videojuegos de la novela del mismo nombre de William Gibson.


El reverso del neuromancer. Parte 1: Sprites
El reverso del neuromancer. Parte 2: Renderizar fuente

Esta parte puede parecer algo caótica. El hecho es que la mayoría de lo que se describe aquí estaba listo al momento de escribir el anterior . Ya han pasado dos meses desde entonces y, desafortunadamente, no tengo la costumbre de guardar notas de trabajo, simplemente olvidé algunos detalles. Pero tal como está, vámonos.




[Después de que aprendí a imprimir líneas, sería lógico continuar invirtiendo la construcción de cuadros de diálogo. Pero, por alguna razón se me escapó, en lugar de eso, entré completamente en el análisis del sistema de renderizado.] Una vez más, caminando por la pantalla main , pude localizar la llamada que primero muestra algo en la pantalla: seg000:0159: call sub_1D0B2 . "Cualquier cosa", en este caso, es el cursor y la imagen de fondo del menú principal:




Es de destacar que la función sub_1D0B2 [en adelante - render ] no tiene argumentos, sin embargo, su primera llamada está precedida por dos secciones de código casi idénticas:


 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 

Antes de llamar a render , los cursores ( cursors.imh ) y el fondo ( title.imh ) se desempaquetan en la memoria ( load_imh es el sub_126CB renombrado de la primera parte ), en los segmentos noveno y décimo, respectivamente. Un estudio superficial de la función sub_123F8 no me trajo ninguna información nueva, pero, solo mirando los argumentos de sub_1CF5B , saqué las siguientes conclusiones:


  • los argumentos 4 y 5, tomados en conjunto, representan la dirección del sprite descomprimido ( segment:offset );
  • los argumentos 2 y 3 son probablemente coordenadas, ya que estos números se correlacionan con la imagen mostrada después de llamar a render ;
  • El último argumento puede ser la bandera de la opacidad del fondo del sprite, porque los sprites desempaquetados tienen un fondo negro, y vemos el cursor en la pantalla sin él.

Con el primer argumento [y al mismo tiempo con el renderizado en general], todo quedó claro después de rastrear sub_1CF5B . El hecho es que en el segmento de datos, comenzando con la dirección 0x3BD4 , se 0x3BD4 una matriz de 11 estructuras del siguiente tipo:


 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; 

Yo llamo a este concepto cadena de sprites. De hecho, la función sub_1CF5B (en adelante add_sprite_to_chain ) agrega el sprite seleccionado a la cadena. En una máquina de 16 bits, tendría aproximadamente la siguiente firma:


 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); 

Funciona así:


  • el primer argumento es el índice en la matriz g_sprite_chain ;
  • los argumentos left y top se escriben en los g_sprite_chain[index].left y g_sprite_chain[index].top respectivamente;
  • el encabezado de sprite (los primeros 8 bytes ubicados en el segment:offset ) se copia en el campo g_sprite_chain[index].sprite_hdr , escriba imh_hdr_t (renombrado rle_hdr_t de la primera parte):

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

  • el campo g_sprite_chain[index].sprite_segment registra el valor del segment ;
  • en el campo g_sprite_chain[index].sprite_pixels , se g_sprite_chain[index].sprite_pixels un valor igual a offset + 8 , por lo que sprite_segment:sprite_pixels es la dirección de mapa de bits del sprite agregado;
  • los sprite_hdr , sprite_segment y sprite_pixels duplican en _sprite_hdr , _sprite_segment y _sprite_pixels respectivamente [¿por qué? - No tengo idea, y este no es el único caso de tal duplicación de campos] ;
  • en el campo g_sprite_chain[index].flags valor igual a 1 + (opaque << 4) . Este registro significa que el primer bit del valor de las flags indica la "actividad" de la capa "actual", y el quinto bit indica la opacidad de su fondo. [Mis dudas sobre la bandera de transparencia se disiparon después de probar experimentalmente su efecto en la imagen mostrada. Cambiando el valor del quinto bit en tiempo de ejecución, podemos observar estos artefactos]:


Como ya mencioné, la función de render no tiene argumentos, pero no la necesita: funciona directamente con la matriz g_sprite_chain , transfiriendo alternativamente las "capas" a la memoria VGA , desde la última ( g_sprite_chain[10] - fondo) a la primera ( g_sprite_chain[0] - primer plano). La estructura sprite_layer_t tiene todo lo necesario para esto y aún más. Estoy hablando de la update campos no dleft , dleft y dtop .


De hecho, la función de render redibuja NO TODOS los sprites en cada cuadro. Un valor distinto de cero del campo g_sprite_chain.update indica que el sprite actual necesita ser redibujado. Supongamos que movemos el cursor ( g_sprite_chain[0] ), entonces algo así sucederá en el controlador de movimiento del mouse:


 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; } 

Cuando el control pasa a la función de render , esta última, habiendo alcanzado la g_sprite_chain[0] , ve que debe actualizarse. Entonces:


  • se calculará y dibujará la intersección del área ocupada por el sprite del cursor antes de actualizar con todas las capas anteriores;
  • Se actualizarán las coordenadas del sprite:

 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; 

  • el sprite se dibujará en las coordenadas actualizadas.

Esto minimiza el número de operaciones realizadas por la función de render .




No fue difícil implementar esta lógica, aunque la simplifiqué bastante. Teniendo en cuenta el poder informático de las computadoras modernas, podemos redibujar los 11 sprites de cadena en cada cuadro, debido a esto se g_sprite_chain.update los g_sprite_chain.update , .dleft , .dtop y todo el procesamiento asociado con ellos. Otra simplificación se refiere al manejo de la bandera de opacidad. En el código original, para cada píxel transparente en el sprite, se busca la intersección con el primer píxel opaco en las capas inferiores. Pero uso el modo de video de 32 bits y, por lo tanto, solo puedo cambiar el valor del byte de transparencia en el esquema RGBA . Como resultado, obtuve tales funciones de agregar (eliminar) un sprite a (de) una cadena (s):


Código
 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)); } 

La función de transferir una capa a un búfer VGA es la siguiente:


 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); } 

La función draw_to_vga es la función del mismo nombre descrito en la segunda parte , pero con un argumento adicional que indica la transparencia del fondo de la imagen. Agregue la llamada draw_sprite_to_vga al comienzo de la función de render (el resto de su contenido migró de la segunda parte ):


 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]); } ... } 

También escribí una función que actualiza la posición del cursor del cursor, dependiendo de la posición actual del puntero del mouse ( update_cursor ), y un simple administrador de recursos. Hacemos que todo esto funcione juntos:


 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



De acuerdo, para un menú principal completo, el menú en sí no es suficiente. Es hora de volver a la inversión de los cuadros de diálogo. [La última vez, draw_frame función draw_frame , que forma el cuadro de diálogo y, en parte, la función draw_string , tomando solo la lógica de representación de texto desde allí.] Al draw_frame la nueva función draw_frame , vi que la función add_sprite_to_chain se usaba allí, nada sorprendente, solo agregando un cuadro de diálogo en cadena de sprites. Era necesario ocuparse del posicionamiento del texto dentro del cuadro de diálogo. Déjame recordarte cómo es la llamada a draw_string :


  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) 

y la estructura que rellena draw_frame [aquí está un poco más adelante, ya que draw_frame nombre de la mayoría de los elementos después de descubrir completamente draw_string . Por cierto, aquí, como en el caso de sprite_layer_t , hay una duplicación de campos] :


 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; 

En lugar de explicar qué hay aquí, cómo y por qué, solo dejo esta imagen:



Las variables x_offt e y_offt son el segundo y el tercer argumento de la función draw_string respectivamente. Según esta información, fue fácil crear sus propias versiones de draw_frame y draw_text , después de renombrarlas a build_dialog_frame y 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); ... } 


La principal diferencia entre mis versiones y las originales es que uso valores absolutos de tamaños de píxeles, es más fácil.




Incluso entonces, estaba seguro de que la sección de código inmediatamente después de la llamada a build_dialog_text responsable de crear los botones:


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

Se trata de estos comentarios generados: 'n' y 'l' , que obviamente son las primeras letras de las palabras "New" y "load" . Además, si argumentamos por analogía con build_dialog_text , los primeros cuatro argumentos de sub_181A3 (en adelante - build_dialog_item ) pueden ser factores de coordenadas y tamaños [de hecho, los primeros tres argumentos, el cuarto, como resultó, sobre el otro] . Todo converge si superpone estos valores en la imagen de la siguiente manera:



Las variables x_offt , y_offt y width en la imagen son, respectivamente, los primeros tres argumentos de la función build_dialog_item . La altura de este rectángulo es siempre igual a la altura del símbolo: ocho. Después de mirar muy de cerca el build_dialog_item , descubrí que lo que neuro_dialog_t en la estructura neuro_dialog_t como padding (ahora items ) es una matriz de 16 estructuras de la siguiente forma:


 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; 

Y el campo neuro_dialog_t.unknown (ahora - neuro_dialog_t.items_count ) es el contador del número de elementos en el menú:


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

El campo dialog_item_t.unknown inicializa con el cuarto argumento de la función build_dialog_item . Quizás este sea el índice del elemento en la matriz, pero parece que este no es siempre el caso y, por lo tanto, se unknown . El campo dialog_item_t.letter inicializa con el quinto argumento de la función build_dialog_item . De nuevo, es posible que en el controlador de clic izquierdo el juego verifique las coordenadas del puntero del mouse en el área de uno de los elementos (simplemente ordenándolos en orden, por ejemplo), y si hay un golpe, entonces se selecciona el controlador deseado para hacer clic en un botón específico de este campo. [No sé cómo se hace esto realmente, pero implementé exactamente esa lógica.]


Esto es suficiente para crear un menú principal completo, sin mirar hacia atrás en el código original, sino simplemente repitiendo su comportamiento observado en el juego.


Main_Menu.GIF



Si miraste el gif anterior hasta el final, probablemente notaste la pantalla de inicio del juego en los últimos cuadros. De hecho, ya tengo todo para dibujarlo. Solo tómalo y descarga los sprites necesarios y agrégalos a la cadena de sprites. Sin embargo, al colocar el sprite del personaje principal en el escenario, hice un descubrimiento importante relacionado con la estructura imh_hdr_t .


En el código original, la función add_sprite_to_chain , que agrega la imagen del protagonista a la cadena, se llama con las coordenadas 156 y 110. Esto es lo que vi, repitiendo esto yo mismo:



Después de descubrir qué es qué, obtuve el siguiente tipo de estructura imh_hdr_t :


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

Lo que solía ser un campo unknown resultó ser valores de compensación que se restan de las coordenadas correspondientes (durante la representación) almacenadas en la cadena de sprites.




Por lo tanto, la coordenada real de la esquina superior izquierda del sprite dibujado se calcula aproximadamente de la siguiente manera:


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

Aplicando esto en mi código, obtuve la imagen correcta, y después de eso comencé a revivir al personaje principal. De hecho, escribí todo el código relacionado con el control del personaje (mouse y teclado), su animación y movimiento por mi cuenta, sin mirar atrás al original.


Moonwalk.gif



Tengo una introducción de texto para el primer nivel. Permítame recordarle que los recursos de cadena se almacenan en archivos .BIH . .BIH archivos .BIH consisten en un encabezado de tamaño variable y una secuencia de cadenas terminadas en nulo. Al examinar el código original que reproduce la introducción, descubrí que el desplazamiento del comienzo de la parte de texto en el archivo .BIH está contenido en la cuarta palabra del encabezado. La primera línea es la introducción:


 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; 

Además, confiando en el original, implementé la división de la cadena original en subcadenas para que encajen en el área de salida de texto, desplazándose a través de estas líneas y esperando la entrada antes de emitir el siguiente lote.


Introducción.GIF



En el momento de la publicación, además de lo que ya se ha descrito en tres partes, descubrí la reproducción del sonido. Hasta ahora esto solo está en mi cabeza y tomará algún tiempo darme cuenta de esto en mi proyecto. Por lo tanto, es probable que la cuarta parte sea completamente sobre el sonido. También planeo contar un poco sobre la arquitectura del proyecto, pero veamos cómo va.


El reverso del neuromancer. Parte 4: Sonido, Animación, Huffman, Github

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


All Articles