¡¡Nuestro compañero Chema no está quieto!!

Anuncio
¡¡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...
Documentos relacionados
Descargar