{"id":2927,"date":"2021-02-12T00:04:40","date_gmt":"2021-02-12T00:04:40","guid":{"rendered":"http:\/\/blog.rastersoft.com\/?p=2927"},"modified":"2023-12-25T19:47:30","modified_gmt":"2023-12-25T19:47:30","slug":"pintando-en-el-spectrum-14","status":"publish","type":"post","link":"https:\/\/blog.rastersoft.com\/?p=2927","title":{"rendered":"Pintando en el Spectrum (14)"},"content":{"rendered":"\n<p>El siguiente paso es implementar la l\u00f3gica del juego. Se trata de la parte del c\u00f3digo que <em>hace que funcionen las cosas como deben funcionar<\/em>. Por ejemplo, se encarga de mover al personaje principal y a los secundarios, animar objetos, etc. Este c\u00f3digo se ejecutar\u00e1 justo despu\u00e9s de que hayamos pintado todos los gr\u00e1ficos en el <em>buffer<\/em> secundario y mientras esperamos a que comience el barrido de un nuevo <em>frame<\/em>.<\/p>\n\n\n\n<p>Esta parte se divide, al menos de momento, en dos piezas de c\u00f3digo: una que actualiza la posici\u00f3n de cualquier personaje, y otra que ejecuta las distintas tareas o <em>tasks<\/em>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Movimiento de personajes<\/h2>\n\n\n\n<p>Dado que los personajes est\u00e1n animados, es necesario tener en cuenta en qu\u00e9 posici\u00f3n se encuentran en cada momento, si est\u00e1n ya donde deben estar o no, la fase de la animaci\u00f3n, etc. Adem\u00e1s, para ahorrar memoria, tenemos por un lado el cuerpo en s\u00ed y por otro las piernas, siendo estas \u00faltimas la \u00fanica parte animada realmente. Eso significa que cada personaje est\u00e1 compuesto realmente por dos sprites, pero s\u00f3lo uno es el que tiene secuencias de animaci\u00f3n. Adem\u00e1s, cada bloque est\u00e1 repetido <em>en espejo<\/em>, para cuando el personaje tiene que moverse hacia el lado opuesto.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/animacion.png\" rel=\"lightbox-0\"><img loading=\"lazy\" decoding=\"async\" width=\"640\" height=\"480\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/animacion.png\" alt=\"\" class=\"wp-image-2930\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/animacion.png 640w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/02\/animacion-300x225.png 300w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><figcaption>Sprites que forman a un personaje. Arriba est\u00e1n los dos cuerpos, uno para cada lado, y debajo de cada uno est\u00e1n las secuencias de animaci\u00f3n de las piernas que corresponden a cada uno de los dos cuerpos.<\/figcaption><\/figure><\/div>\n\n\n\n<p>Cada personaje est\u00e1 definido en una estructura como la siguiente:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted meme\">    DEFB 0x01 ; bit 0: 1-&gt; es el personaje principal\n              ; bit 1: 1-&gt; es un personaje secundario\n              ; bit 2: 1-&gt; est\u00e1 caminando; 0-&gt; no camina\n              ; bit 5: 1-&gt; mira hacia la izquierda; 0-&gt; a la derecha\n              ; bit 6-7: etapa de animaci\u00f3n\n              ; si el byte vale cero, es FIN DE LA TABLA\n    DEFB NOWX ; coordenada X actual\n    DEFB NOWY ; coordenada Y actual\n    DEFB DESX ; coordenada X final\n    DEFB DESY ; coordenada Y final\n    DEFW sprites_table ; puntero al sprite para el cuerpo\n    DEFW (sprites_table + 6) ; puntero al sprite para los pies<\/pre>\n\n\n\n<p>Vemos que tenemos, por un lado, la etapa actual de la animaci\u00f3n y a qu\u00e9 lado est\u00e1 mirando actualmente el personaje, por otro lado dos juegos de coordenadas, y por \u00faltimo dos punteros a dos entradas de la tabla de sprites, uno apuntando a la entrada que muestra el cuerpo del personaje y otra que apunta a la entrada de las piernas. Cuando queremos que un personaje camine desde donde est\u00e1 actualmente (cuyo valor est\u00e1 almacenado en NOWX, NOWY) hasta otro sitio, debemos simplemente escribir las coordenadas de destino en DESX, DESY, y la rutina de movimiento de personajes se encargar\u00e1 del resto. Esta rutina se ejecuta justo despu\u00e9s de pintar todo el <em>buffer secundario<\/em>, y lo que hace es comparar la coordenada X actual con la X final, y lo mismo con la Y. Si son iguales, pondr\u00e1 el bit 2 a 0 para indicar que el personaje est\u00e1 en su destino y pasar\u00e1 a la siguiente entrada. Sin embargo, si alguna de las dos coordenadas actuales no es igual a su hom\u00f3loga final, la rutina le sumar\u00e1 o restar\u00e1 1, en funci\u00f3n de lo que sea necesario para acercar al personaje a su posici\u00f3n final. A continuaci\u00f3n copiar\u00e1 las coordenadas actuales en cada una de las dos entradas de la tabla de sprites. Luego incrementar\u00e1 en uno la etapa de animaci\u00f3n, y en base al valor de \u00e9sta decidir\u00e1 qu\u00e9 sprite concreto se mostrar\u00e1 para los pies (modificando para ello la entrada correspondiente de la tabla de sprites), adem\u00e1s de ajustar la coordenada Z (porque nuestros personajes se mueven a saltos). Tras hacer todo esto, saltar\u00e1 a la siguiente entrada de la tabla y repetir\u00e1 estas operaciones hasta llegar al final.<\/p>\n\n\n\n<p>Vemos, pues, que esta rutina nos simplifica mucho el mover un personaje desde una zona del mapa hasta otra, pues s\u00f3lo tenemos que poner las coordenadas finales y esperar a que el bit 2 cambie de 1 a 0 para saber que ha llegado.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Gestor de tareas<\/h2>\n\n\n\n<p>La rutina anterior nos ayuda un poco, pero todav\u00eda nos queda por implementar absolutamente toda la l\u00f3gica del juego en s\u00ed. La primera idea consiste en escribir una funci\u00f3n que se llame justo despu\u00e9s de terminar de actualizar las posiciones de los personajes (esto es, justo des pu\u00e9s de la rutina anterior), y que, en base al estado actual del juego (donde est\u00e1 el protagonista, el resto de personajes, los objetos, etc) decida cual debe ser el nuevo estado (donde estar\u00e1 el protagonista y el resto de personajes, si alg\u00fan objeto aparece o desaparece&#8230;), tras lo cual se pintar\u00e1 de nuevo el mapa y se repetir\u00e1 el proceso.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"206\" style=\"aspect-ratio: 288 \/ 206;\" width=\"288\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/07\/calculating.mp4\"><\/video><\/figure>\n\n\n\n<p>Por desgracia, hacer algo as\u00ed complica sobremanera la tarea, y adem\u00e1s hace que cualquier cambio que se quiera hacer pueda afectar a otras partes, por lo que el c\u00f3digo probablemente quedar\u00eda muy complejo. Por eso decid\u00ed seguir la idea que <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Ron_Gilbert\" target=\"_blank\">Ron Gilbert<\/a> y compa\u00f1\u00eda utilizaron en <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/SCUMM\" target=\"_blank\">Scumm<\/a>, la m\u00e1quina virtual que desarroll\u00f3 para el juego <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Maniac_Mansion\" target=\"_blank\">Maniac Mansion<\/a>: permitir crear m\u00faltiples tareas que se ejecuten en paralelo y de manera independiente.<\/p>\n\n\n\n<p>La idea b\u00e1sica es que cada personaje u objeto se maneje desde una tarea independiente, y que como mucho se a\u00f1adan puntos de sincronizaci\u00f3n entre ellas. Las tareas se ejecutan mediante multitarea cooperativa, y se ejecutan una vez por <em>frame<\/em> del juego. Cuando una tarea ha terminado de hacer lo que tenga que hacer, simplemente tiene que hacer una llamada a una subrutina concreta para avisar, cediendo el control a la siguiente. Cuando todas las tareas se han ejecutado, se pintar\u00e1 el siguiente <em>frame<\/em>, se actualizar\u00e1 la posici\u00f3n de los personajes, y volver\u00e1n a ejecutarse las tareas una a una, pero con el detalle de que cada una continuar\u00e1 su ejecuci\u00f3n justo donde se qued\u00f3 en el <em>frame<\/em> anterior, y adem\u00e1s la pila o <em>stack<\/em> conservar\u00e1 todos los valores que la tarea hubiese almacenado en su ejecuci\u00f3n previa.<\/p>\n\n\n\n<p>Veamos c\u00f3mo es la implementaci\u00f3n:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">task_list:\n    DEFW $+TASK_DATA_SIZE+2\n    DEFS TASK_DATA_SIZE\n    DEFW tarea1\n    DEFW $+TASK_DATA_SIZE+2\n    DEFS TASK_DATA_SIZE\n    DEFW tarea2\n    DEFW 0 ; fin de la tabla de tareas\n\n\ntask_run:\n    ld (old_stack), SP ; preservamos la pila global\n    ld iy, task_list\ntask_loop:\n    ld l, (iy+0)\n    ld h, (iy+1)\n    ld a, h\n    or l\n    jr z, task_end\n    ld sp, hl ; asignamos a SP la pila de la tarea a ejecutar\n    ret ; y saltamos al c\u00f3digo de la tarea\n\ntask_yield:\n    ld hl, 0\n    add hl, sp ; almacena en HL el stack actual\n    ld (iy+0), l\n    ld (iy+1), h ; y lo guarda en la entrada de la tabla de tareas\n    ld de, TASK_ENTRY_SIZE\n    add iy, de\n    jr task_loop ; siguiente entrada de la tabla de tareas\n\ntask_end:\n    ld SP, (old_stack) ; restauramos la pila global\n    ret<\/pre>\n\n\n\n<p>Vemos que la tabla est\u00e1 dimensionada para dos tareas, y cada entrada de la tabla de tareas est\u00e1 compuesta de dos partes:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Los dos primeros bytes contienen la direcci\u00f3n de la pila o <em>stack<\/em> de esta tarea (recordemos que $ es \u00abla direcci\u00f3n de esta l\u00ednea\u00bb)<\/li><li>El resto de bytes es donde se almacena la pila o <em>stack <\/em> de esta tarea, con un total de TASK_DATA_SIZE+2 bytes<\/li><\/ul>\n\n\n\n<p>Vemos que en el c\u00f3digo que puse arriba, los dos primeros bytes apuntan a los dos \u00faltimos bytes del bloque de la pila de cada tarea, y \u00e9stos contienen la direcci\u00f3n de entrada del c\u00f3digo de cada tarea. De esta manera, lo que hacemos es inicializar la pila de cada tarea de manera que contenga un \u00fanico dato: la direcci\u00f3n inicial de entrada para esa tarea.<\/p>\n\n\n\n<p>Si nos vamos al c\u00f3digo que viene justo a continuaci\u00f3n, vemos que el punto de entrada es <em>task_run<\/em>. Esta es la funci\u00f3n que se llama cada vez que se ha terminado de pintar un <em>frame<\/em>. Vemos que lo primero que hace es guardar la direcci\u00f3n del <em>stack<\/em>. Esto es necesario porque vamos a modificarlo para cada tarea, por lo que cuando terminemos, necesitaremos restaurarlo al valor inicial, o no podremos retornar.<\/p>\n\n\n\n<p>A continuaci\u00f3n cargamos en IY la direcci\u00f3n inicial de la tabla de tareas, pues vamos a utilizar dicho registro \u00edndice para recorrerla. Las tareas no deben modificar este registro bajo ninguna circunstancia.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"192\" style=\"aspect-ratio: 320 \/ 192;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/working.mp4\"><\/video><\/figure>\n\n\n\n<p>Entramos ahora en el bucle principal, en el que simplemente tomamos los dos primeros bytes, vemos si ambos valen cero (lo que significar\u00eda que hemos llegado al final de la tabla), y si no es as\u00ed, cargamos su valor en SP.<\/p>\n\n\n\n<p>Ahora el puntero de pila apunta a la pila de la primera tarea, y si recordamos c\u00f3mo inicializamos la tabla de tareas, el valor que hay en ella ser\u00e1 la direcci\u00f3n de inicio de dicha tarea, con lo que al hacer un RET, el procesador sacar\u00e1 ese valor de la pila y saltar\u00e1 a \u00e9l.<\/p>\n\n\n\n<p>Ahora la primera tarea empezar\u00e1 a ejecutarse. Har\u00e1 todo lo que tenga que hacer, y cuando haya terminado, simplemente har\u00e1 un <em>CALL task_yield<\/em>. Esto significa que ahora la pila de la tarea ya no contiene la direcci\u00f3n inicial, sino justo la direcci\u00f3n desde la que se hizo esa llamada, lo que significa que cuando volvamos a llamar a <em>task_run<\/em> tras el siguiente <em>frame<\/em>, al ejecutar el RET se saltar\u00e1 justo a la siguiente instrucci\u00f3n tras el <em>CALL<\/em> en lugar de al principio, por lo que la tarea continuar\u00e1 su ejecuci\u00f3n justo en el punto en el que lo dej\u00f3.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"144\" style=\"aspect-ratio: 258 \/ 144;\" width=\"258\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/busstop.mp4\"><\/video><\/figure>\n\n\n\n<p>En <em>task_yield<\/em> vemos que lo que hacemos es almacenar el valor de SP en los dos primeros bytes de la entrada de la <em>tabla de tareas<\/em>. Esto es necesario porque la tarea podr\u00eda haber almacenado alg\u00fan valor antes de llamar a <em>task_yield<\/em>, lo que significa que la direcci\u00f3n de retorno ya no est\u00e1 al final de la pila, sino antes.<\/p>\n\n\n\n<p>Tras ello, sumamos a IY el tama\u00f1o de una entrada de la <em>tabla de tareas<\/em> para as\u00ed saltar a la siguiente, y repetimos el proceso hasta llegar al final de la tabla.<\/p>\n\n\n\n<p>Veamos un ejemplo de una tarea, en concreto la que gestiona el movimiento del personaje amarillo:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">task2:\n    ld ix, entrada_en_tabla_de_personajes\ntask2b:\n    ld (ix+3), 40\n    ld (ix+4), 42 ; cargamos las nuevas coordenadas a donde debe ir\n    call task_wait_for_walk ; esperamos a que llegue\n    ld (ix+3), 28\n    ld (ix+4), 34 ; cargamos las nuevas coordenadas a donde debe ir\n    call task_wait_for_walk ; esperamos a que llegue a su destino\n    jr task2b\n\n\ntask_wait_for_walk:\n    push ix         ; guardamos en la pila el personaje\n    call task_yield ; y cedemos el control\n    pop ix          ; en el siguiente frame, recuperamos el personaje\n    bit 2, (ix+0)   ; vemos si est\u00e1 caminando\n    ret z           ; est\u00e1 parado? retornamos\n    jr task_wait_for_walk ; y si a\u00fan no llego al destino, repetimos<\/pre>\n\n\n\n<p>Vemos que lo primero que hacemos es cargar en IX la direcci\u00f3n de la tabla de personajes correspondiente al personaje amarillo, y justo a continuaci\u00f3n entramos en el bucle principal.<\/p>\n\n\n\n<p>El personaje, al arrancar el programa, est\u00e1 en las coordenadas 28,34 (pues as\u00ed lo definimos en la tabla de personajes, no por otra cosa), as\u00ed que lo que hacemos es poner 40,42 como coordenadas de destino, y llamamos a <em>task_wait_for_walk<\/em>. Esta funci\u00f3n, como vemos, lo primero que hace es preservar IX y devolver el control para que se ejecuten el resto de tareas y se repinte la pantalla. Cuando, en el siguiente frame, se ejecute de nuevo esta tarea, la ejecuci\u00f3n comenzar\u00e1 justo en el <em>POP<\/em> situado despu\u00e9s de la llamada a <em>task_yield<\/em>. Ning\u00fan registro se ha preservado, pero la pila s\u00ed se conserva, as\u00ed que retiramos el valor de IX y comprobamos su bit 2 (que es el que nos dice si a\u00fan est\u00e1 caminando para llegar a las nuevas coordenadas, o si ya ha llegado). En este caso todav\u00eda no habr\u00e1 llegado, por lo que volvemos a guardar IX en la pila y cedemos de nuevo el control. As\u00ed hasta que, tras varios <em>frames<\/em>, por fin el bit vale cero, momento en el que directamente retornamos al c\u00f3digo que llam\u00f3 a <em>task_wait_for_walk<\/em>. Y como ahora ya estamos en 40,42, lo que hacemos es poner ahora como destino 28,34 de nuevo y esperar a que llegue a su destino. De esta manera el personaje lo que har\u00e1 ser\u00e1 caminar desde 28,34 hasta 40,42 y vuelta, una y otra vez.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"216\" style=\"aspect-ratio: 288 \/ 216;\" width=\"288\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/phew.mp4\"><\/video><\/figure>\n\n\n\n<p>Vemos que el hecho de que la pila y las direcciones de retorno se conserven simplifica much\u00edsimo el trabajo, pues el c\u00f3digo de la tarea puede ceder el control para esperar a que todo el sistema <em>avance<\/em> sabiendo que continuar\u00e1 ejecut\u00e1ndose en el mismo punto <em>como si nada hubiese pasado<\/em>. Bueno, o casi nada, pues es cierto que los registros no se conservan entre llamdas a <em>task_yield<\/em>. Pero para eso basta con que la rutina preserve en la pila todo lo que necesite.<\/p>\n\n\n\n<p>Por supuesto, esto significa que hay que dejar espacio suficiente en la pila para todos los registros que se quieran preservar a la vez, adem\u00e1s de todas las direcciones de retorno&#8230; \u00a1y una direcci\u00f3n extra! Pues nos interesa que las interrupciones est\u00e9n habilitadas mientras ejecutamos las tareas, porque de esta manera podemos aprovechar para ejecutarlas un tiempo que, si no us\u00e1semos interrupciones, simplemente tirar\u00edamos a la basura; pero eso significa que puede metersenos un dato extra en la pila con el que, a lo mejor, no cont\u00e1bamos al dimensionarla.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Adaptando la ISR<\/h2>\n\n\n\n<p>Por desgracia, esto nos supone un problema extra, pues la <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Interrupt_handler\" target=\"_blank\">ISR<\/a> tiene que guardar en la pila todos los registros que vaya a utilizar. Esto significa que si dejamos nuestro c\u00f3digo <em>tal cual<\/em>, necesitar\u00edamos reservar en la pila de cada tarea el espacio suficiente para guardar no s\u00f3lo todos los datos de la tarea, sino tambi\u00e9n 22 bytes extra, los necesarios para guardar la direcci\u00f3n de retorno de la interrupci\u00f3n y los diez pares de registros del procesador que utilizamos en la ISR (AF, BC, DE, HL, AF&#8217;, BC&#8217;, DE&#8217;, HL&#8217;, IX e IY).<\/p>\n\n\n\n<p>Hacer esto ser\u00eda un verdadero desperdicio de memoria, as\u00ed que la soluci\u00f3n es tan sencilla como reservar una zona de 22 bytes extra en memoria, y nada m\u00e1s entrar en la ISR, guardar el valor de SP al principio de esa zona, cambiarlo para que apunte al final m\u00e1s uno (recordemos que PUSH primero decrementa y luego almacena) del bloque, y s\u00f3lo entonces guardar los diez pares de registros. Al salir, por supuesto, tras restaurar los registros hay que restaurar tambi\u00e9n SP, y s\u00f3lo entonces retornar.<\/p>\n\n\n\n<p>En la pr\u00f3xima entrada hablar\u00e9 de la impresi\u00f3n de textos.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>El siguiente paso es implementar la l\u00f3gica del juego. Se trata de la parte del c\u00f3digo que hace que funcionen las cosas como deben funcionar. Por ejemplo, se encarga de mover al personaje principal y a los secundarios, animar objetos, etc. Este c\u00f3digo se ejecutar\u00e1 justo despu\u00e9s de que hayamos pintado todos los gr\u00e1ficos en &hellip; <a href=\"https:\/\/blog.rastersoft.com\/?p=2927\" class=\"more-link\">Seguir leyendo <span class=\"screen-reader-text\">Pintando en el Spectrum (14)<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,17,7],"tags":[20],"class_list":["post-2927","post","type-post","status-publish","format-standard","hentry","category-programacion","category-retrocomputacion","category-tutoriales","tag-pintando-en-el-spectrum"],"_links":{"self":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2927","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2927"}],"version-history":[{"count":6,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2927\/revisions"}],"predecessor-version":[{"id":2941,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2927\/revisions\/2941"}],"wp:attachment":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2927"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2927"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2927"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}