¡¡Nuestro compañero Chema no está quieto!! Vale. Como he estado liado y el poco tiempo que tuve lo empleé en arreglar bugs y otras cosillas menores, voy a ver si puedo ir contando cómo estoy haciendo las cosas. Alguno lo pidió, así que no se me quejen del enorme ladrillo que se avecina. ¡Y por capítulos! Eso sí, el código que pongo sigue el formato del ensamblador del OSDK, el XA. Supongo que si no se conoce pueda despistar un poco, pero seguro que basta para hacerse una idea. Se me ha quedado algún comentario en inglés, igual que los nombres de algunas variables. Lo suelo hacer así para que la gente de Defence Force siga mejor el código. Cualquier comentario será bienvenido, claro. Allá vamos: El problema El Oric no tiene un chip de video como tal, ni ninguna capacidad hardware para manejar sprites, hacer doble búfer, modificar el área de memoria donde se almacenan la pantalla, etc. Esto hace que todo deba realizarse por software, usando el 6502 (que tampoco da para mucho) y teniendo en cuenta la organización del modo de video (donde sólo se utilizan 6 bits de cada byte), así que la actualización de los gráficos en pantalla es todo un reto. En “Space:1999” ya se hace un uso intensivo del procesador para el renderizado isométrico y se actualiza sólo la zona de la pantalla donde hay movimiento. Aunque el código se podría haber optimizado más, es difícil obtener una animación rápida en cuanto hay unos pocos sprites moviéndose en la pantalla simultáneamente. En 1337 la cosa es peor porque hay que actualizar toda la pantalla (el área de juego) continuamente, no sólo por trozos: hay un campo de estrellas, hay objetos en 3D, disparos, etc. potencialmente en toda la pantalla, de modo que es inútil intentar actualizar sólo las áreas necesarias. El implementar un doble buffer aquí representa como un 30% del tiempo total de pintado (incluyendo todo el cálculo 3D), por mucha optimización que se aplique. Esto hace que a veces el framerate baje casi hasta 5 fps (aunque la mayoría del tiempo es mucho mayor). Por supuesto es posible pasar del doble búfer y utilizar otras técnicas (pintado de líneas con eor para borrar las anteriores, como hace el Elite original), pero eso produce un parpadeo, a mi juicio, muy feo. Y el resultado con doble búfer es perfectamente jugable y relativamente suave de todas formas. En “Skool Daze” el reto era diferente. Aquí hay muchos sprites moviéndose por toda la pantalla, pero el fondo es bastante estático, así que basta con marcar las áreas que necesitan re-dibujado y optimizar al máximo el mismo. En este caso todo el juego estaba implementado usando “tiles” de 6x8, así que se tiene un campo de bits que indica cuándo un tile necesita ser redibujado. En cada frame se recorre el campo y cuando se encuentra un bit a 1 se calcula el gráfico de 6x8 que se debe volcar en pantalla en esa posición. El resultado es muy bueno y no se necesita demasiada memoria extra. La cuestión es entonces: ¿es posible tener un juego que funcione con un scroll de toda la pantalla y con varios sprites a la vez? ¿Algo como el Uridium?. Ese era el reto. Dejar que la ULA trabaje por nosotros La respuesta inmediata a la pregunta anterior es no. Al menos no para un área grande de juego (como del 70-80% de la pantalla). O no por software. Es posible hacer un scroll lateral de la pantalla en alta resolución más o menos rápido (como hace Skool Daze), pero sólo si movemos todo el contenido en un momento dado y el juego luego sigue sobre un área fija (o sea, pasamos del doble búfer y movemos directamente el contenido de la pantalla). Eso no es lo que queremos (queremos algo continuo mientras se juega) y, además, no es visualmente suave ni permite parallax (hacer scroll a diferentes velocidades para diferentes planos). El truco aquí está en aprovecharse del modo TEXT del ORIC. Sí, el modo de texto. En ese modo la pantalla se organiza en 40 columnas y 27 filas. Cada celda contiene un byte que indica un carácter. La ULA lee el código y pinta el carácter asociado del juego de caracteres en pantalla. Es decir, hace el renderizado del bloque de 6x8 por hardware. Vale. Ese va a ser el truco. Sólo tendremos que borrar/pintar/volcar un área de 40x27 bytes como máximo, lo que puede hacerse muy rápido y la ULA hace el resto. Por supuesto tendremos que redefinir el juego de caracteres para que contenga los gráficos con los que componer todo el fondo (con la nave nodriza enemiga). Lo primero es ver de cuántos caracteres (al final serán nuestros “tiles”) disponemos. Lamentablemente no tenemos 256. Los códigos por debajo del 32 son considerados por la ULA como atributos (para cambiar colores, modo de pantalla, etc). Los que tienen el bit de más peso a 1 son considerados en vídeo inverso. Así que nos quedan sólo 96. Cualquier gráfico un poquito complicado va a necesitar más. Ya tenemos un problema. Eso sí, disponemos de un juego alternativo (el LORES 1), así que ahí tenemos otros 96, pero no podemos intercalarlos de cualquier manera con el estándar porque requieren de un atributo serie para cambiar de modo. Lo que podemos hacer es disponer nuestra área de juego de modo que se alternen ambos modos en cada fila. Esto es mejor que nada. Cada fila de la pantalla, pues, comenzará por un código serie que indique el color de la tinta a usar, y otro que indique si usamos el juego de caracteres estándar o el alternativo. Cada fila entonces tiene: Byte 0 COLOR Byte 1 8 (par)/9 (impar) Byte 2 ….. Byte 37 Byte 38 Byte 39 Códigos de los gráficos No usados Esta técnica va a tener una desventaja clara: todo el movimiento y el scroll va a ser carácter a carácter. Se podría intentar paliar esto con un esquema complejo que realice el scroll sub-carácter sobre los gráficos, pero requeriría más “tiles” y tiempo de CPU, cosas de las que, en principio, no disponemos. Esto, sin embargo, lo haremos con el fondo de estrellas, como veremos más adelante. Gráficos, “tiles” y organización en memoria El área de juego será de 36 columnas (las dos primeras las necesitamos para los atributos y las dos últimas no las usaremos para dejarlo todo centrado) por 20 filas. El mapa de un nivel es de 256 columnas y 20 filas y contiene el dibujo de la nave nodriza. El diseño de la misma se hace en base al siguiente juego de bloques gráficos (“tiles”): Los gráficos de la fila inferior van al juego estándar (56, del 32 al 87) y los de la superior al alternativo (54). Por supuesto ciertos gráficos habrá que alinearlos debidamente en el mapa para que coincidan bien y el resultado sea correcto. Los dos bloques que quedan libres del juego alternativo se va a usar para las estrellas y las bolas de energía que pueden recogerse sobre la superficie de la nave (y que están animadas). En ambos juegos, el primer código (que es el 32) se usa para indicar que está libre (un espacio transparente, a través del cual se ven las estrellas) y el 33 para un bloque en negro pero no transparente (para las sombras de los muros). El mapa se sitúa en los últimos 5K disponibles y cada fila de 256 bloques ocupa exactamente una página, así que acceder al tile correspondiente a una coordenada global (fila, columna) es muy sencillo y rápido. Por ejemplo, para obtener el código del tile basta con acceder a la página <(fila+base)+256> indexando con X = columna. Es decir la fila es el byte alto de la dirección, siendo el bajo 0; de este modo (por ejemplo): lda fila clc adc #>_start_rows ; Dirección de comienzo del mapa (fila cero) sta smc_address+2 ; Modificamos el código de abajo ldx columna smc_address ; Esto es código automodificable, el byte alto de $1200 se modifica ;antes para que el lda lo haga de la dirección correcta lda $1200,x Doble búfer Nuestro doble búfer (o back buffer) tiene que contener el área visible en pantalla. La idea es simple, en cada frame borramos el búfer, pintamos todo lo necesario sobre él y lo volcamos en la pantalla. Vamos a hacerlo un poco más grande de lo necesario (que sería 36x20) para poder olvidarnos de hacer clipping al pintar, es decir, de determinar qué partes de un objeto (sprite) se ven y cuales no). Al hacerlo más grande podemos pintar siempre que el objeto sea visible (aunque sea parcialmente) y luego volcaremos sólo lo necesario. Como los sprites son como mucho de 2x2 tiles, en este caso necesitamos dos columnas y dos filas extra. En mi caso he puesto un buffer de 40x22, porque tengo una tabla para multiplicar por 40 rápidamente, aunque bien podría haberse hecho de 38x22. Vamos a ver el proceso paso a paso: Para borrar el buffer, ponemos todo el contenido al carácter 32 (espacio). Tenemos que hacerlo rápido, así que desenrollamos el bucle parcialmente. Además tengo dos etiquetas definidas para indicar las columnas que no se usan tanto por la derecha como por la izquierda (PCOLSL y PCOLSR). Hay que tener en cuenta que los bucles son más eficientes si se usan decrementos en lugar de incrementos, porque podemos chequear simplemente la bandera de cero (Z) o de signo (N): _clear_backbuffer .( lda #32 ldy #39-PCOLSL-PCOLSR loop sta _backbuffer+40*0+PCOLSL,y sta _backbuffer+40*1+PCOLSL,y sta _backbuffer+40*2+PCOLSL,y … sta _backbuffer+40*18+PCOLSL,y sta _backbuffer+40*19+PCOLSL,y dey bpl loop rts .) Para dibujar el fondo la cosa es simple. Tenemos una variable _inicol con la columna inicial que se ve en pantalla (se modifica al hacer scroll): _render_background .( ; Start drawing the background ; From _inicol onwards. ; Unrolled to get speed. ; Takes the tile code from the map ; which is organized in memory ; so that every row lies on a page ; boundary, and an indexed addressing ; with x=column, gets the tile lda clc adc tax ldy loop lda sta lda sta … lda sta dex dey bpl #39-PCOLSL-PCOLSR _inicol #39-PCOLSL-PCOLSR _start_rows+256*0,x _backbuffer+40*0+PCOLSL,y _start_rows+256*1,x _backbuffer+40*1+PCOLSL,y _start_rows+256*19,x _backbuffer+40*19+PCOLSL,y loop rts .) Listo. Para volcar su contenido es similar. Por supuesto sólo volcamos la parte que se ve en pantalla, no sobre los atributos ni las dos columnas sin usar de la derecha. Es decir los bytes del 2 al 37. SCR_ADDR es una etiqueta con la dirección de pantalla donde queremos empezar a volcar. Sería $bb80 para empezar por la parte superior. En el juego es más abajo, porque reservamos sitio para el panel de puntuaciones. _dump_backbuffer .( ldy #39-PCOLSL-PCOLSR loop lda _backbuffer+40*0+PCOLSL,y sta SCR_ADDR+40*0+PCOLSL,y lda _backbuffer+40*1+PCOLSL,y sta SCR_ADDR+40*1+PCOLSL,y … lda _backbuffer+40*19+PCOLSL,y sta SCR_ADDR+40*19+PCOLSL,y dey bpl loop rts .) Esta parte es MUY eficiente, así que permite hacer un scroll de todo el mapa muy rápidamente (eso sí, carácter a carácter). Basta con actualizar el valor de _inicol y lanzar las rutinas anteriores. Es una buena base. El borrado del búfer lleva 3830 ciclos, el renderizado son 8060 (incluyendo el campo de estrellas que todavía no hemos visto) y el volcado 6731. Todo ello son 18.621 ciclos, menos de los 20.000 que producirían 50 frames por segundo con el 6502 del ORIC a 1Mhz. Pero faltan cosas. Sobre el doble búfer hay que pintar también los diferentes sprites y, además, queremos un efecto parallax con las estrellas (que no están ni pintadas aun). Comenzaremos por lo segundo. En la próxima entrega Ummm... igual esto es demasiado técnico o denso... De todas formas me parece una buena idea ir documentando estas cosas. Me sirve para repasar las rutinas y dejar por escrito qué voy haciendo. Oye, e igual alguien en un futuro lo encuentra útil. Así que seguimos con el. El campo de estrellas y el efecto parallax Es sencillo tener un campo de estrellas fijo. Basta con tener un array con las posiciones de las estrellas y pintarlo sobre el fondo. Digo sobre el fondo, porque lo hago después del bucle anterior, chequeando si hay algo o no, es decir si el backbuffer contiene un 32 (espacio) o no. De este modo me ahorro un montón de comprobaciones en el bucle anterior viendo si lo que se vuelca es un espacio o no y resulta más eficiente. Pero vamos a hacer que las estrellas hagan scroll píxel a píxel y así lograr un efecto más chulo. No nos hace falta generar estrellas para todo el mapa, sino sólo para un pequeño trozo. Cuando hagamos scroll de una columna en el mapa, la estrella lo hará un pixel y no se moverá de la posición que tiene en el mapa (basado en caracteres) hasta que no hayamos hecho esto 6 veces. Con un cálculo rápido vemos el área que hay que rellenar con estrellas: podemos hacer scroll como 256-40=216 veces (si suponemos que vemos 40 columnas, que ni eso). Esto dividido por 6 da 36. Así que tenemos que rellenar las 40 columnas que vemos, más 36; es decir 76. Como las dos últimas no se ven, en realidad son 74. Lo que tenemos entonces es un array con el número de estrellas (ahora mismo 30) que contiene su posición (x,y) en el mapa, donde x va de 0 a 73 e y de 0 a 19 y que se rellena de forma aleatoria. Como sólo existe el gráfico para el juego de caracteres alternativo, las estrellas tienen que estar en filas impares. De todo eso se encarga la rutina de generación. En realidad es más eficiente tener definidas tres tablas: dos para el puntero a la fila donde está la estrella y uno para la columna: starsx starshi starslo .dsb NSTARS .dsb NSTARS .dsb NSTARS El gráfico correspondiente a una estrella es un solo punto en la tercera fila. Lo que hacemos en cada scroll es rotar ese byte (lo que rota la estrella en todas partes donde aparece) y si hace falta actualizamos su posición X y reiniciamos. Como sólo tenemos 6 pixels por byte, no se puede usar el carry para ver si nos salimos y es algo más complicado. Por ejemplo este código hace scroll a la izquierda: ; Rotamos y miramos si nos ; hemos salido de los 6 bits menos ; significativos lda _star_tile+3 asl and #%00111111 beq update ; No nos hemos salido, nada más que hacer sta _star_tile+3 rts ; Nos hemos salido, movemos las estrellas y reiniciamos ; el gráfico update ; Bucle para todas las estrellas, decrementando ; su posición X ldx #NSTARS-1 loop dec starsx,x dex bpl loop ; Ponemos el pixel 0 a 1 lda #%00000001 sta _star_tile+3 rts En realidad no se hace así del todo porque, para evitar errores visuales (tearing), se marca que hay que reiniciar el gráfico y se hace al final del volcado de toda la pantalla. Ya aclararemos este punto más adelante. Para pintar las estrellas, la cosa es sencilla: _plot_stars .( ; Bucle para todas las estrellas (decrementando) ldx #NSTARS-1 loop ; Tomamos el puntero a la fila de la estrella y lo metemos en tmp (página cero) lda starslo,x sta tmp lda starshi,x sta tmp+1 ; Tomamos la columna de la estrella ; y comprobamos que sea visible ; si no lo es saltamos a la siguiente ldy starsx,x bmi skip cpy #39 bcs skip ; Es visible, miramos si en el backbuffer tenemos en ; la posición donde va la estrella un espacio libre o no lda (tmp),y cmp #32 bne skip ; Lo tenemos, así que ponemos el código del gráfico ; de la estrella lda #STAR_TILE sta (tmp),y skip dex bpl loop rts .) Hasta aquí vale, pero ¿y los marcianos? Naturalmente queremos poner naves enemigas, objetos y otras cosas. Y queremos que sean sprites con sus máscaras y poder darles animaciones, etc. Los sprites son de 2x2 caracteres, es decir: 12x16 pixeles. Los sprites por software se basan en la técnica clásica de tener por un lado el gráfico y por otro la máscara que indica la parte que ocluye el gráfico (como ceros y el resto unos). Se toma el fondo, se le hace un AND con la máscara y se aplica el gráfico con una operación OR. Por ejemplo la nave y su máscara son: Gráfico Máscara Los datos de los sprites se almacenan en arrays (vectores) que contienen cosas como la fila y columna en que están situados, su velocidad, hacia dónde miran, su estado, velocidad, etc. También, por supuesto, punteros a su gráfico y máscara actuales. Quizás es buen momento para recordar que para manejar arrays con el 6502, es más eficiente generar campos de 8 bit y ponerlos cada uno por separado. Así un puntero a un gráfico que es de 16 bits, es mejor tenerlo como dos vectores separados con la parte baja y la alta que un vector con entradas de 2 bytes. Tenemos, pues, cosas como: ; Position of sprites in the overall map sprite_cols .dsb MAX_SPRITES+1 sprite_rows .dsb MAX_SPRITES+1 ; Movement speed max_speed .dsb MAX_SPRITES+1 ; Pointers to graphic and mask data sprite_grapl .dsb MAX_SPRITES+1 sprite_graph .dsb MAX_SPRITES+1 sprite_maskl .dsb MAX_SPRITES+1 sprite_maskh .dsb MAX_SPRITES+1 Todo esto está muy bien y en alta resolución (HIRES) lo hemos hecho otras veces. La cosa es hacer esta operación lo más rápido posible, porque es el cuello de botella. Ahora bien, ¿cómo hacemos esto cuando usamos modo texto? Pues sobre caracteres, claro. Es como si manejásemos un modo de pantalla diferente, con otro direccionamiento (que es lo que es en realidad, nada de “como”). La rutina básica es la que se encarga de renderizar un tile de 6x8. Necesita el código del gráfico de fondo y un par de punteros al gráfico y a la máscara. Además, como usamos dos juegos de caracteres diferentes, hay que tener en cuenta de cuál se trata. Para optimizar los punteros se modifican directamente en el código (otra vez código auto-modificable) y se usan tablas para las multiplicaciones. El resultado del renderizado va a ir en alguno de los caracteres que aún no se han usado de los 96 y que están asignados a los sprites. Luego pondremos el código de ese carácter en el back buffer en la posición correcta antes del volcado. Está claro que no podemos hacerlo sobre el original, pues modificaríamos el gráfico en sí, que puede estarse usando en otros sitios. Vamos a intentar explicar la rutina que hace esto. El tile de fondo se pasa en el registro A, el tile de destino (donde va a ir el resultado final) en el registro Y. Los punteros al gráfico y máscara ya están actualizados y el juego de caracteres utilizado se pasa en una variable de página cero (tmp3+1) como el byte alto de donde empieza dicho juego, es decir $b4 o $b8. render_tile .( ; Obtener el puntero a los datos gráficos para ; el carácter en A: ; $b400+(regA*8) o $b800+(regA*8) ; Multiplicamos A*8 tax lda tab_mul8_lo,x ; Guardamos modificando el código más abajo sta smc_bkg_p+1 ; Vamos con la parte alta lda tmp3+1 ; contiene $b4 o $b8 clc adc tab_mul8_hi,x ; Guardamos modificando el código más abajo sta smc_bkg_p+2 ; Ahora lo mismo para el destino lda tab_mul8_lo,y sta smc_dest_p+1 lda tab_mul8_hi,y ;tmp+1 clc adc tmp3+1 sta smc_dest_p+2 Ya tenemos los punteros preparados, ahora hay que hacer el bucle que vaya recorriendo los 8 bytes tomando el fondo, haciendo el AND con la máscara y el ORA con el gráfico y almacenando en el carácter de destino. No confundirse porque algunas etiquetas tengan un + delante: es para que sean accesibles desde otras funciones externas. El código auto-modificable es engorroso pero ahorra muchos ciclos en este tipo de cosas. ldy #7 loop smc_bkg_p lda $1234,y +smc_mask_p and $1234,y +smc_sprite_p ora $1234,y smc_dest_p sta $1234,y dey bpl loop rts .) ; Esto lo acabamos de poner a la dirección correcta ; Esto se puso fuera, antes de llamar a la función ; Esto también ; Esto lo acabamos de poner a la dirección correcta Y listo. Ya tenemos el gráfico correcto en uno de los caracteres libres. En realidad estoy usando dos funciones idénticas, excepto que la segunda invierte el gráfico y la máscara antes de pintar. Esto es útil para no tener en memoria los gráficos de los objetos dos veces: una mirando a la derecha y otra a la izquierda. Para hacerlo rápido, se tiene una tabla de 64 entradas con los inversos de cada patrón de 6 bit y el bucle queda: ldy #7 loop smc_bkg_p lda $1234,y +smc_maski_p ldx $1234,y and inverse_table,x +smc_spritei_p ldx $1234,y ora inverse_table,x smc_dest_p sta $1234,y dey bpl loop Para pintar un sprite completo primero necesitamos 4 caracteres libres (dos en el juego estándar y dos en el alternativo, porque usa dos filas). Para ello se tienen unas tablas que indican el código de los caracteres a utilizar. Cada sprite tiene asociados cuatro códigos donde dibujarse sprite_tile11 (fila1, columna1), sprite_tile12 (fila1, columna2), sprite_tile21 y sprite_tile22 (ídem para la fila 2). Si un sprite puede tener asociados el 125 y el 126 para la fila 1 y también el 125 y el 126 para la fila 2, porque ambos van en juegos de caracteres diferentes. Tenemos libres del 88 al 127, lo que daría para unos 20 sprites. No está mal. Lástima que no vaya a ser así al final. Pero ya hablaremos de eso. Vamos a ver cómo se renderiza un sprite de 2x2 cuyo código (un identificador) se pasa en el registro X. La idea es: 1-Lo primero es ver si el sprite está en la zona visible (restar a su columna la columna inicial que estamos viendo y ver que está en rango). 2-Calcular el puntero a la fila del back buffer (fila_del_sprite*40+base_backbuffer). Luego pondremos la columna (menos la columna inicial) en el registro Y y accedemos al contenido por direccionamiento indirecto (Ej.: lda (puntero),y). 3-Poner los códigos de los caracteres asociados al sprite en el back buffer, guardando primero el contenido que tenían para pintar luego el fondo. render_sprite .( ; Lo primero colocar los caracteres donde vamos a pintar ; sobre el backbuffer ; Es el sprite visible? lda sprite_cols,x ; X es el id del sprite, para acceder a todos sus datos sec sbc _inicol clc adc #2 bpl okhere retme rts okhere cmp #39 bcs retme okthere ; Vale lo es, aprovechamos para guardar la ; posición de su columna en el backbuffer (restada la ; columna inicial _inicol) sta tmp4 ; Calcular row*40+base del backbuffer y guardar en tmp1 ; Como el backbuffer está alineado a una página, basta ; con sumar en el byte alto ldy sprite_rows,x lda tab_m40lo,y sta tmp1 lda #>_backbuffer clc adc tab_m40hi,y sta tmp1+1 ; Ponemos los caracteres donde vamos a pintar ; y vamos salvando los códigos que había ldy tmp4 ; Aquí teníamos la columna del sprite-columna inicial (por el scroll) lda (tmp1),y ; Contenido en esa posición sta t11+1 ; Lo guardamos (de nuevo código automodificable, ya veremos luego) lda sprite_tile11,x ; Cogemos el código asociado al sprite para ese tile sta (tmp1),y ; Lo ponemos en su sitio ; Siguiente columna de la fila 1 iny lda (tmp1),y sta t12+1 lda sprite_tile12,x sta (tmp1),y ; Vamos por la columna 1 de la fila 2 tya clc adc #39 tay lda (tmp1),y sta t21+1 lda sprite_tile21,x sta (tmp1),y ; Y iny lda sta lda sta la columna 2 de la fila 2 (tmp1),y t22+1 sprite_tile22,x (tmp1),y Vale, eso ya está. Ahora basta con renderizar los gráficos en cada uno de los cuatro caracteres. La idea es simple usando la rutina que vimos antes, pero hay algunos detalles que complican el código. Lo primero es que hay que tener en tmp3+1 $b4 o $b8 dependiendo del juego de caracteres a usar, es decir, de si la fila es par o impar. Se usa una tabla para eso y el código es: lda and tay lda sta sprite_rows,x #%1 base_charset_p,y tmp3+1 Bueno, ahora hay que obtener los punteros a los gráficos y máscara y ponerlos en el código auto-modificable de la rutina que renderiza. De nuevo dos versiones, dependiendo de si hay que invertir el gráfico, pero nada complicado. Los punteros se almacenan en tablas indexadas por el id del sprite (que sigue en el registro X): lda sprite_dir,x bmi inversed_draw ; Tomamos la dirección en la que mira el sprite ; Versión que no invierte lda sprite_maskl,x sta smc_mask_p+1 lda sprite_maskh,x sta smc_mask_p+2 lda sprite_grapl,x sta smc_sprite_p+1 lda sprite_graph,x sta smc_sprite_p+2 jmp endinvcheck ; Versión que invierte inversed_draw lda sprite_maskl,x sta smc_maski_p+1 lda sprite_maskh,x sta smc_maski_p+2 lda sprite_grapl,x sta smc_spritei_p+1 lda sprite_graph,x sta smc_spritei_p+2 endinvcheck Vale, ahora hay que cargar en A el código con el gráfico de fondo. Lo habíamos salvado antes en t11+1, t12+1, etc. ¿no? Pues se toma aquí. Es feo, pero más rápido que usar variables intermedias o la pila. Esto se repite para los cuatro caracteres, así que basta con poner uno: ; Tomamos el código del carácter de fondo t11 lda #0 ; Aquí lo hemos guardado antes ; ahora tomamos el carácter de destino para este sprite en la posición 1,1 ldy sprite_tile11,x ; Reservamos X stx tmp3 ; Y listos para llamar, por desgracia no es tan inmediato por culpa de ; tener en cuenta la versión que invierte… Esto está chapuceado, de momento. pha lda sprite_dir,x bmi inverse_draw1 pla jsr render_tile jsr add8pointers ; Suma 8 a los punteros necesarios jmp t12 inverse_draw1 jsr add8pointersi ; Suma 8 a los punteros necesarios, versión invertir pla jsr render_tilei jsr sub8pointersi ; Resta 8 a los punteros necesarios ; Siguiente t12 … .) Hala, ahora a tomar un café que esto ha sido mortal para nuestras neuronas. Eso sí, funciona y es rápido. Toma unos 1500 ciclos de CPU renderizar un sprite en pantalla. Pero si lo implementamos así nos encontramos con un montón de efectos espantosos en pantalla: “tearing,” “glitches” gráficos, etc. Inutilizable para nada decente. Por ejemplo la siguiente imagen muestra el "tearing". Se produce porque estamos actualizando la memoria de pantalla mientras la ULA está refrescando la imagen, de forma que una parte tiene el gráfico antiguo y la otra el nuevo. Aquí se ve bastante claro (y vemos que queda horrible), pero también ocurre si actualizamos un sprite, su sombra, etc. mientras ya está parcialmente dibujado. Existe otra versión más sutil, que ocurre cuando actualizamos el contenido gráfico de un carácter y su posición, pudiendo ocurrir que en la imagen tengamos el gráfico antiguo en la posición nueva o el nuevo en la antigua. Recordemos, además que los sprites llevan parte del fondo, que puede no ser el correcto en este caso. Lidiaremos con eso más adelante. O sea que hay que librarse de ello. Para el “tearing” normal es algo simple: sólo hay que sincronizar con el barrido vertical del monitor para pintar mientras no se está actualizando la imagen. Un momento… Eso no se puede hacer con un Oric. No hay manera de detectar esa señal sin un mod (simple, pero que poca gente tiene). Bueno, pues lo haremos por software. Menos mal que Fabrice Frances ya nos dijo cómo hacerlo en su día. Sólo hay que integrarlo. Para el resto de efectos raros… En fin, primero habrá que ver por qué se producen. Sincronización con el barrido vertical Para poder sincronizar con el barrido vertical necesitamos una interrupción que salte cada vez que se finalice un dibujado de la imagen (más o menos). En el ORIC no se dispone de dicha señal, pero se puede modificar el contador de la vía T1 que decide cada cuánto se envía una interrupción a la CPU. Fabrice Francés (el autor del emulador Euphoric y un monstruo de la comunidad) diseño una rutina de calibración simple para sincronizar ese contador con el barrido de pantalla. La rutina es simple: cambia el color de papel de la pantalla cuando se produce una interrupción, espera un tiempo y luego lo restaura. Si el cambio se produce en medio del dibujado de la imagen, sólo un trozo aparecerá con ese color de papel. Además la rutina lee el teclado y usa una tecla para incrementar el contador de T1 y otra para decrementarlo. Una tercera tecla sirve para salir. Comenzamos con el contador a un valor que genere interrupciones a 50Hz. Si decrementamos el contador, se producen las interrupciones en menos tiempo del que tarda el barrido y la barra se desplaza hacia arriba. Si lo incrementamos pasa lo contrario. Basta con que el usuario use las teclas para situar la barra (el trozo) de color de papel diferente al final de la pantalla (y que no se desplace) para tener las interrupciones sincronizadas con el barrido vertical. Vamos a usar esa rutina para nuestros propósitos, pero primero vamos a echar unos números. Tenemos que actualizar la memoria de pantalla (es decir, volcar nuestro back buffer) mientras la imagen no se está enviando a la TV. Creo que la tele usa modo entrelazado, así que en un barrido se actualizan 312 líneas (creo que es así, sino corregidme, por favor). En el ORIC sólo 224 de esas líneas tienen información, el resto es borde, así que tenemos: 20 ms por cuadro, 312 líneas, dan como 6,4 microsegundos por línea. El tiempo que el ORIC pinta borde es de 88 líneas (312224), o sea 5,6 milisegundos o 5600 ciclos. El volcado de nuestra área de juego duraba 6731 ciclos. Nos pasamos, pero es que no toda la pantalla es área de juego, sino solo 160 líneas (8x20). O sea que tenemos el tiempo de 152 líneas, más de 9700 ciclos. Eso está mejor. Podemos hacer que el usuario sincronice la interrupción cuando comienza el dibujado del radar y tenemos tiempo suficiente para volcar el back buffer. En el vídeo que ya os había puesto antes se ve este proceso: El código del bucle principal debe ahora hacer: … jsr _clear_backbuffer jsr _render_background ; Draw the sprites over the background jsr _put_sprites ; Now let's synchronize with the vbl jsr waitirq ; Done, now we are (hopefully) on the BLANK period, dump ; the double buffer quickly jsr _dump_backbuffer … La rutina waitirq simplemente espera a que se produzca una interrupción antes de volcar el back buffer, eliminando completamente el tearing. ¡Listo! ¿o no? Pues hay algunas cosas que decir. Lo primeo es que si hacemos un cálculo rápido con lo que nos llevaba borrar, renderizar el fondo y volcar, estábamos como en 18000 ciclos. Si pintamos, digamos, 5 ó 6 sprites nos vamos rápidamente a 7000 ciclos más. Acabamos de pasarnos de los 20000. Por muy poco que nos pasemos, perderemos una interrupción y la sincronización nos hará esperar al siguiente cuadro. Resultado: tenemos el juego a 25 cuadros por segundo. No está mal, pero sin sincronizar estaríamos fácilmente en más de 30 ó 35. Es el precio que se paga. Solo espero que no se dé el caso de emplear más de 40000 ciclos o bajamos a 16 cuadros por segundo inmediatamente. Eso sí es una pérdida. Vale, lo implementamos todo y nos ponemos a probar. Parece que todo va bien hasta que, de pronto nuestro sprite corrompe el fondo durante un cuadro y luego se arregla. Vemos que eso se da cada cierto tiempo, así como otros fallos esporádicos en los gráficos. ¿Qué ocurre? Pues ocurre que, cuando pintamos los sprites con su máscara estamos modificando el contenido de un carácter que está en pantalla y, como está fuera del sincronizado, puedes pillarlo en medio del volcado de la imagen. Imaginemos que no ha sido pintado y resulta que nos ponemos a pintarlo en otro sitio. Lo primero que hace la rutina es colocar el carácter en el nuevo lugar, pero en el back buffer, no en pantalla. Luego procedemos a dibujarlo enmascarado con el nuevo fondo. Si antes de volcar el back buffer toca un barrido, el gráfico pensado para la nueva posición, se va a pintar en la antigua. Ahí está el fallo. No estaría mal que nos diese tiempo a hacerlo todo en los 20000 ciclos, pero no se puede. Tampoco podemos dibujar los sprites tras detectar la interrupción, justo antes de volcar el back buffer, porque nos pasamos del tiempo que calculamos antes (9700 ciclos). Para solucionar esto hay que usar un pedazo de truco. Usar dos juegos de caracteres libres para dibujar los sprites de manera alternativa, así no corrompemos nunca el que pueda pintarse en pantalla. Lo malo es que eso nos reduce el número de sprites que podemos tener a la vez a la mitad. De 20 a 10. De todas formas pintar 10 sprites a la vez en pantalla lleva ya demasiado tiempo, así que no es tan terrible. Para los que quieren código, esto se hace justo después del jsr _reder_background, y cambia en la tabla que vimos los caracteres a usar. En la tabla aparecen inicialmente sólo pares y en cada bucle de juego se alterna con los impares: ; Update sprite tiles to use this frame ; Using two sets alternatively avoids having to draw them ; while the screen is not being refreshed, which was too ; tight when more than 5-6 sprites on screen. ; Drawback: we duplicate the tiles needed :/ lda frame_counter and #1 beq ones lda #$de ; Opcode for dec abs,x .byt $2c ones lda #$fe ; Opcode for inc abs,x sta loop ldx #(MAX_SPRITES+1)*4-1 loop inc sprite_tile11,x dex bpl loop Vale, el código es algo ofuscado, pero funciona. También se deja para justo después del volcado el actualizar el gráfico de las estrellas. Eso lleva poco tiempo y cabe antes de que comience la actualización de pantalla. Si no lo hacemos aquí, no queda sincronizado con el barrido y, de nuevo, aparecen errores gráficos. El motor ya está listo. Pero quedan mogollón de cosas.... Generación Procedural de la Nave Enemiga Ya tenemos el motor funcionando y el siguiente paso que vamos a ver es el diseño de las naves nodriza enemigas para cada nivel. Es algo así como el fondo donde se mueve el jugador y el resto de elementos del juego. El "mapa" del nivel ocupa 5K (256 celdas y 20 filas). Podríamos pensar en diseñar varias naves enemigas para el fondo y luego comprimir los datos. En cada nivel extraemos los datos correspondientes en la memoria y listo. Hay multitud de técnicas para guardar los datos comprimidos, y se pueden conseguir compresiones muy altas para guardar muchas pantallas o diseños. Normalmente para poder comprimir mucho, necesitamos elementos que se repitan, así que a mayor compresión, menos variedad. Supongamos que conseguimos comprimir cada nave nodriza en... yo qué se, pongamos 1K. ¿Cuántos niveles podemos poner? Depende de la memoria libre, pero ya vemos que 10 niveles serían 10K, lo que es bastante. ¿Y si queremos tener 50 niveles? o ¿70?... Nos quedamos rápidamente sin memoria. Pero hay una alternativa: la generación procedural (ver artículo en Wikipedia). La cosa es generar el contenido algorítmicamente en lugar de manualmente. La cosa es simple (y complicada a la vez). Necesitamos un generador de secuencias a partir de una semilla. Por ejemplo una implementación de la serie de Fibonacci o un generador de pseudoaleatorios. La cosa es que, para una semilla determinada se genere siempre la misma secuencia de números cada vez que llamemos a la función. Ahora podemos usar algoritmos que usen los datos generados por la función para ir construyendo el contenido. Cosas como llamar a la función y dependiendo de ciertos bits del resultado generar un bloque de un tamaño determinado, con esquinas cuadradas o en chaflán, con determinados elementos en su superficie, etc. Podemos ir llamando a la función cada vez que tengamos que decidir el tipo de contenido o su localización. Hacemos lo mismo para generar espacios entre bloques (o no), para elegir cómo los conectamos entre sí, etc. Son todo reglas que operan en base a los datos que se van generando. Como son siempre los mismos para una semilla dada, pues siempre sale la misma nave. La mayor dificultad está en ajustar el generador para que el contenido sea correcto y suficientemente variado. Las semillas de cada nivel se generan a partir de la inicial del nivel anterior. Así podemos ir a un nivel determinado sin más que llamar a una función las veces que haga falta y luego llamar al generador. Esta misma técnica la usaba Elite (y se importó para 1337) para generar todo el universo del juego. Ahí sí que era complicado, porque el contenido era muy rico y complejo. Aquí es algo más simple, pero suficiente para dar dolor de cabeza al programador. Aquí vemos un ejemplo de cómo el generador va construyendo la base de la nave. Fue un primer intento que implementé en Matlab para probar, pero está bien para ver cómo funciona la cosa (sólo una parte, porque el completo ocupaba mucho): El generador implementado en el juego es ligeramente diferente y ocupa ahora mismo 2559 bytes. Tiene algún fallo menor, pero está casi completo al 100%. No está mal. Con menos de 3K tenemos para generar, en principio, infinitos niveles, aunque no puedo asegurar que la secuencia no se acabe repitiendo. Probablemente el juego se quede en un número de niveles máximo del orden de 100. Además es bastante rápido, así que el usuario no nota ningún retardo entre niveles por su causa. Dejamos aquí este tema. Si alguien quiere más detalles técnicos o aclaraciones en algún punto, que lo pida. Yo encantado. En la próxima edición a ver si empezamos a hablar de los enemigos y la IA. La IA de los enemigos El esquema para implementar el movimiento de los enemigos es bastante sencillo. La idea es algo parecido a la "multitarea cooperativa" de antaño: en cada instante se recorren todos los enemigos activos y se invoca a una función que implementa su IA. Estas funciones deben realizar el trabajo en pasos de manera que cuando se las llame, hagan un par de cálculos, modifiquen lo que sea necesario y retornen lo antes posible para que el sistema pase a la siguiente. Cada enemigo debe tener entonces un área de datos donde almacenar su estado (su "contexto"). Como es costumbre en máquinas basadas en 6502, es mejor tener vectores de bytes (uno por dato, o dos por dato si los son de 16 bit), en lugar de todo en un bloque (como sería una estructura en C) para acceder más rápidamente. Creo que esto ya lo comenté antes. En este juego este área de datos está formado por lo siguiente: ; Posición en el mapa sprite_cols .dsb MAX_SPRITES+1 sprite_rows .dsb MAX_SPRITES+1 ; Comando principal de la IA pcommand_l .dsb MAX_SPRITES+1 pcommand_h .dsb MAX_SPRITES+1 ; Comando adicional de la IA ccommand_l .dsb MAX_SPRITES+1 ccommand_h .dsb MAX_SPRITES+1 ; Variable interna (su uso depende de la función ejecutada) sprite_var1 .dsb MAX_SPRITES+1 ; Estado de animación o paso en el algoritmo anim_state .dsb MAX_SPRITES+1 ; Velocidad de reacción del enemigo ; Una máscara para aplicar al número de frame actual (que es un byte que se incrementa en cada cuadro) ; se hace un AND entre ambos para ver si se ejecuta el comando (Z=0) o se retorna sin hacer nada (Z!=0) reac_speed .dsb MAX_SPRITES+1 ; Velocidad de movimiento máxima. max_speed .dsb MAX_SPRITES+1 ; Punteros al gráfico y máscara actuales sprite_grapl .dsb MAX_SPRITES+1 sprite_graph .dsb MAX_SPRITES+1 sprite_maskl .dsb MAX_SPRITES+1 sprite_maskh .dsb MAX_SPRITES+1 ; Dirección del movimiento: 0=parado, >0 a la derecha, <0 a la izquerda sprite_dir .dsb MAX_SPRITES+1 ; Estado del enemigo: 0=inactivo 1=normal 2=explotando sprite_status .dsb MAX_SPRITES+1 ; Velocidad de movimiento actual (es un índice a una tabla de velocidades, para implementar aceleraciones, deceleraciones speedp .dsb MAX_SPRITES+1 ; Movimiento arriba o abajo <0 arriba, or >0 abajo, 0 sin movimiento speedv .dsb MAX_SPRITES+1 ; Tipo de enemigo sprite_type .dsb MAX_SPRITES+1 Parece muy complicado, pero los datos van saliendo de manera natural al ir implementando. Lo más importante son los punteros a los comandos de la IA. Yo estoy usando dos (que llamo continuo y principal), porque me dan más flexibilidad (ahora mismo hablo de eso). Cuando queremos que un enemigo realice una función determinada (por ejemplo perseguir al jugador o simplemente explotar), se instala la dirección de la función adecuada en esos punteros. En cada cuadro, entonces, el motor recorre los enemigos mirando a ver lo que tiene que hacer de este modo (el código es un resumen de lo que hay implementado, para dejar solo lo importante): ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Move the sprites ;;;;;;;;;;;;;;;;;;;;;;;;;;; move_sprites .( ldx #MAX_SPRITES loop ; ¿Está el enemigo activo? lda sprite_status,x beq skip ; Miramos si tiene un comando continuo y saltamos a esa rutina si es así ; Se hace con código automodificable lda ccommand_h,x beq noccommand sta smc_jmpc+2 lda ccommand_l,x sta smc_jmpc+1 smc_jmpc jsr $1234 noccommand ; Miramos si tiene un comando primario y saltamos a esa rutina si es así ; Se hace con código automodificable lda pcommand_h,x beq noprimcommand sta smc_jmp+2 lda pcommand_l,x sta smc_jmp+1 smc_jmp jsr $1234 +noprimcommand Hasta aquí es fácil, porque sólo se mira si la parte alta del puntero es diferente de cero (no se tienen comandos en direcciones debajo de $00ff porque es la página cero) y, si no lo es, entonces hay un comando al que saltar con jsr. Generalmente yo pongo en los comandos primarios cosas como el seguir al jugador o moverse de izquierda a derecha, y en los secundarios o continuos cosas como la animación del enemigo, pero acabo mezclándolos más o menos según me interesa. El tenerlos separados me permite simplemente combinar cosas. Tras esto el motor sigue con el movimiento del enemigo. Para sincronizar el movimiento tengo una variable llamada frame_bit que parte de un valor 00000001 y va rotando el 1 en cada cuadro. Así un enemigo que se mueva cada cuadro (lo más rápido permitido) tiene una velocidad en binario de 11111111, uno que se mueva cada dos cuadros 10101010, etc. Esto lo tuve que hacer para evitar que dos naves moviéndose a la misma velocidad lo hicieran en cuadros alternos, porque el resultado era visualmente horroroso. ; Comprueba si debe moverse según su velocidad que, recordemos, es una tabla... ldy speedp,x lda tab_speedh,y ; ... que contiene máscaras para asociar al frame_bit and frame_bit beq nomoveh ; Vale,toca moverlo. Esto es simplemente sumarle el valor de la variable sprite_dir (dirección) a la variable sprite_cols (columna actual) lda sprite_cols,x clc adc sprite_dir,x sta sprite_cols,x nomoveh ; Ahora el movimiento vertical ; mucho más simple, sólo arriba o abajo ; pero cada dos frames, que sino es demasiado rápido lda frame_counter and #%1 bne skip ; Como antes, sumamos la velocidad, comprobando que no nos ; salimos de la pantalla. lda speedv,x beq skip clc adc sprite_rows,x bmi skipv cmp #20-1 bcs skipv sta sprite_rows,x skipv ; Hecho, a por el siguiente: hasta el X=0 (inclusive) dex bpl loop Bueno, en realidad se hacen algunos apaños con el movimiento para el jugador, porque hay algunos detalles que tener en cuenta, pero son cosas menores. Y aquí estamos hablando de la IA. Veamos un ejemplo sencillo: un movimiento lateral que se invierte al llegar al borde. El id del objeto a tratar viene siempre en el registro X. Podemos implementarlo con una función como ésta: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Primary command for the simplest ; back and forth movement. When near ; the edge, turn around ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; move_lateral_and_back .( ldy sprite_cols,x lda sprite_dir,x bmi checkleft cpy #250 bcs turn rts checkleft cpy #6 bcc turn rts .) Este es muy simple, ni acelera, ni varía la dirección ni nada, pero puede ser base para otros más complejos. Otro ejemplo: animación en cuatro cuadros. La idea es, dependiendo de la velocidad de animación que queramos, ir cambiando los punteros de gráfico y máscara entre cuatro posibles. Además mantenemos el estado de animación en la variable del objeto anim_state: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Generic 4 frame animation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; gen_fourframe .( ; Es el momento de animar? lda frame_counter and #%11 ; solo cada cuatro cuadros beq doit retme rts doit ; lo es, actualiza anim_state (solo importan los dos bits bajos) y los punteros a gráfico y máscara inc anim_state,x lda anim_state,x and #%11 beq restoreg ; Avanza un frame lda sprite_grapl,x clc adc #32 sta sprite_grapl,x bcc nocarry inc sprite_graph,x nocarry lda sprite_maskl,x clc adc #32 sta sprite_maskl,x bcc nocarry2 inc sprite_maskh,x nocarry2 rts restoreg ; Vuelve a poner el primer gráfico sec lda sprite_grapl,x sbc #(32*3) sta sprite_grapl,x bcs noborrow dec sprite_graph,x noborrow sec lda sprite_maskl,x sbc #(32*3) sta sprite_maskl,x bcs noborrow2 dec sprite_maskh,x noborrow2 rts .) Y así. Por supuesto en este juego hay comportamientos complejos: desde naves que intentan esquivar tus disparos, que se mueven en círculos a tu alrededor, que intentan chocar contigo... hasta cosas como el Eye of the Beholder que, si está cerrado se mueve lateralmente pero cuando estás cerca se abre y comienza a perseguirte para chocar contigo hasta que te alejas y vuelve a cerrarse. Y algunos otros que no desvelo, para que los descubráis jugando ;) El sistema es suficientemente flexible como para que la explosión de los enemigos sea un comando de este tipo, pero también el loop que hace la nave del jugador al cambiar de dirección e incluso hay una para los disparos (el normal y el super-shot). Cuando es necesario se instala y a correr. Vamos a ver con más detalle el que hace explotar una nave. Cuando queremos hacerlo, cargamos en el registro Y el id (índice en los arrays) del enemigo y llamamos a la siguiente función: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Instala el comando para ; hacer explotar la nave cuyo ; ID se pasa en el registro Y ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; explode_ship .( ; Prepara el comando lda #<do_explode sta pcommand_l,y lda #>do_explode sta pcommand_h,y ; Elimina cualquier comando continuo lda #0 sta ccommand_h,y ; Detener el movimiento sta speedv,y lda #8 sta speedp,y ; Preparar punteros a los gráficos ; de la explosión lda #<_sprite_explosion sta sprite_grapl,y lda #>_sprite_explosion sta sprite_graph,y lda #<_mask_explosion sta sprite_maskl,y lda #>_mask_explosion sta sprite_maskh,y ; Indicamos que está explotando lda #IS_EXPLODING ora sprite_status,y sta sprite_status,y ; Colocamos el estado de animación al inicial (cero) lda #0 sta anim_state,y ; Hacemos sonar la explosión lda #EXPLODE jmp _PlaySfx ; Esto salta y retorna de esta función Esta es la parte que lo instala e inicializa. Seguido está el comando en sí que se ejecuta por el sistema: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Punto de entrada para el comando ; que hace que un enemigo explote ; El objeto tratado viene en el registro X ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; do_explode ; Ajuste de velocidad (solo cada dos frames) lda frame_counter and #%1 beq doit rts doit lda anim_state,x cmp #7 bcc next_step ; Si el anim_state es 7, hemos ; terminado la secuencia. ; Si no se trata del jugador podemos ; crear un item. Sino borramos el objeto lda sprite_status beq delete_object ; No es el jugador, salta a crear item y retorna ; La función crear item puede no crearlo ; en cuyo caso salta a delete_object (más abajo) jmp create_item ; Borramos un objeto +delete_object ; Vaciamos los comandos y variables asociadas lda #0 sta pcommand_h,x sta sprite_status,x sta anim_state,x sta speedv,x sta speedp,x ; decrementamos los enemigos actuales y retornamos dec waveobjects rts next_step ; No hemos acabado, siguiente paso de la animación ; incrementar punteros y estado de animación inc anim_state,x lda sprite_grapl,x clc adc #32 sta sprite_grapl,x bcc nocarry inc sprite_graph,x nocarry lda sprite_maskl,x clc adc #32 sta sprite_maskl,x bcc nocarry2 inc sprite_maskh,x nocarry2 rts .) ¿A que es sencillo? Al menos conceptualmente. Si queréis más sobre la programación de IAs en estos juegos, algo más complejo y variado os recomiendo la de Skool Daze. Esa sí que es complicada, sólo hay que ver las cosas que hacen los personajes durante el juego. En este caso intenté reproducir la del juego original. Escribí los detalles en el foro de Defence-Force (aunque está en inglés, creo que esta IA de lo mejorcito que he visto): La IA en Skool Daze (en inglés) : http://forum.defence-force.org/viewtopic.php?f=20&t=706&start=30 Y esto es todo. Si hay alguna duda o queréis que aclare algún punto, decídmelo y edito o posteo más detalles. ¿Qué me queda por contar? No sé si alguien estará interesado en cómo se hace la música o los efectos de sonido (me tuve que implementar un motor simple para esto) o si me queda algún detalle del juego que os interese...