Índice de entradas
Esta serie de artículos han sido traducidos a partir del documento "How to
Write ZX Spectrum Games" con permiso de su autor, Jonathan Cauldwell, un gran
desarrollador de juegos para el Spectrum, os recomiendo
visitar su Web donde está
el texto original. El documento original, y por tanto esta traducción, tiene
© Jonathan Cauldwell y solo puede duplicarse con permiso expreso por escrito
de su autor.
Tablas
Los aliens no llegan de uno en uno
Digamos, por poner un ejemplo, que estábamos escribiendo un juego de space invaders
que muestra once columnas, cada una con cinco filas de invasores. No sería práctico
escribir el código para cada uno de los cincuenta y cinco aliens en cada turno,
lo que necesitamos es montar una tabla. En Sinclair BASIC podríamos hacerlo
mediante la definición de tres tablas de cincuenta y cinco elementos (una para
la coordenada x de los invasores, otra para la coordenada y, más una tercera
con el estado). Podríamos hacer algo similar en ensamblador mediante
la creación en memoria de tres tablas de cincuenta y cinco bytes cada una, y
a continuación añadir el número de cada alien al inicio de cada tabla para acceder
al elemento individual. Desafortunadamente, eso sería lento y engorroso.
Un método mucho mejor es agrupar los tres elementos de datos para cada invasor
en una estructura, y luego montar cincuenta y cinco de estas estructuras en una
tabla. Podemos entonces apuntar con hl a la dirección de cada invasor,
y sabemos que hl apunta al byte de estado, hl más uno apunta a
la coordenada x, hl mas dos apunta a la coordenada y. El código para
mostrar un alien podría ser algo como esto:
ld hl,aliens ; estructura de datos para los aliens. ld b,55 ; número de aliens. loop0 call show ; presentar este alien. djnz loop0 ; repetir para todos los aliens. ret show ld a,(hl) ; obtener el estado del alien. cp 255 ; ¿está el alien desactivado? jr z,next ; si, entonces no presentarlo. push hl ; guardar la dirección del alien en la pila. inc hl ; apuntar a la coordenada x. ld d,(hl) ; obtener la coordenada. inc hl ; apuntar a la coordenada y. ld e,(hl) ; obtener la coordenada. call disply ; mostrar el alien en (d,e). pop hl ; refrescar la dirección del alien desde la pila. next ld de,3 ; tamaño de cada alien en la tabla de entradas. add hl,de ; apuntar al siguiente alien. ret ; mantiene hl apuntando al siguiente.
Usando los registros índice
El inconveniente con esta rutina es que tenemos que tener mucho cuidado de a
donde está apuntando hl todo el tiempo (NdT: se refiere a que usando este método no podemos usar los registros HL para otra cosa) por lo que podría ser una buena
idea almacenar hl en una posición de la memoria temporal de dos bytes
antes de llamar a show, y al final del bucle restaurarla sumandole tres, para a continuación realizar la instrucción djnz.
Si estábamos escribiendo para la Nintendo Game Boy con su Z80 recortado, esta
sería probablemente nuestra mejor opción. En máquinas con procesadores más avanzados,
como el Spectrum y CPC 464 podemos utilizar los registros índice ix para simplificar
nuestro código un poco. Debido a que el par de registro ix nos permite desplazamos
usando direccionamiento indirecto, podemos apuntar ix al comienzo de
la estructura de datos de un alien y acceder a todos los elementos en ella sin la necesidad de cambiar de nuevo ix. Usando ix nuestra
rutina de visualización de aliens podría tener este aspecto:
ld ix,aliens ; estructura de datos para los aliens. ld b,55 ; número de aliens. loop0 call show ; presentar este alien. ld de,3 ; tamaño de cada alien en la tabla de entradas. add ix,de ; apuntar al siguiente alien. djnz loop0 ; repetir para todos los aliens. ret show ld a,(ix) ; obtener el estado del alien. cp 255 ; ¿está el alien desactivado? ret z ; si, entonces no presentarlo. ld d,(ix+1) ; obtener la coordenada. ld e,(ix+2) ; obtener la coordenada. jp disply ; mostrar el alien en (d,e).
Usar ix significa que sólo la primera vez tienes que señalar el comienzo
de la estructura de datos de un alien, por lo que siempre ix + 0 podrá devolver
el estado para el invasor actual, ix + 1 la coordenada x, y así sucesivamente.
Este método permite al programador utilizar estructuras de datos complejas para
sus aliens de hasta 128 bytes de largo, sin confundirse en cuanto de a que punto
de la estructura de nuestros registros se está apuntando en un momento dado,
como en el ejemplo anterior con hl. Desafortunadamente, el uso de
ix es un poco más lento que hl, por lo que no se debe utilizar para
las tareas de procesamiento más intensivas, como la manipulación de gráficos.
Vamos a aplicar este método a nuestro juego del Centipede. En primer lugar,
tenemos que decidir cuantos segmentos se necesitan, y qué datos almacenar sobre
cada segmento. En nuestro juego los segmentos tendrán que desplazarse hacia
la izquierda o hacia la derecha hasta que lleguen a una seta, y luego moverse
hacia abajo y volver a la inversa. Así que parece que necesitaremos una variable bandera para indicar la dirección en que un segmento está viajando, además
de una coordenada x o y. Nuestra variable también se puede utilizar para indicar
que un segmento particular ha sido destruido. Con esto en mente, podemos establecer
una estructura de datos de tres bytes:
centf defb 0 ; flag, 0=izquierda, 1=derecha, 255=muerto. centx defb 0 ; coordenada x del segmento. centy defb 0 ; coordenada y del segmento.
Si optamos por tener diez segmentos en nuestro Centipede, hay que reservar espacio
para una tabla de treinta bytes. Cada segmento tiene que ser inicializado al
principio del juego, para después moverlo, mostrarlo y eliminarlo durante el juego.
La inicialización de los segmentos es probablemente la tarea más simple, por
lo que podemos utilizar un simple bucle para incrementar el par de registros
HL para que apunte a cada byte antes de ajustarlo. Algo parecido a lo esto se
usa en este truco:
ld b,10 ; número de segmentos a inicializar. ld hl,segmnt ; tabla de segmentos. segint ld (hl),1 ; comienza moviéndose a la derecha. inc hl ld (hl),0 ; comienza arriba. inc hl ld (hl),b ; usa el registro B para la coordenada y. inc hl djnz segint ; repetir hasta que todos se inicialicen.
El procesamiento y la visualización de cada segmento va a ser un poco más complicado,
por lo que para eso vamos a utilizar los registros ix. Necesitamos escribir
un algoritmo simple que manipula un solo segmento hacia la izquierda o hacia
la derecha hasta que llega a una seta, y luego se mueve hacia abajo y cambia
de dirección. Llamaremos a esta rutina PROSEG (por "procesar segmento"), y establecer
un bucle que apunte a su vez a cada segmento y llame a PROSEG. Proporcionando
el correcto algoritmo de movimiento, podremos ver a continuación un ciempiés
que serpentea su camino a través de las setas. Aplicar esto a nuestro código
es sencillo: comprobamos el byte de bandera para cada segmento (ix) para ver
de qué manera el segmento se está moviendo, incremento o decremento según
la dirección de la coordenada horizontal (ix + 2), a continuación comprobar
el atributo en esa celda de carácter. Si es verde y negro incrementamos la coordenada
vertical (ix + 1) y cambiamos el indicador de dirección (ix).
De acuerdo, hay algunas cosas más a considerar, como cuando golpea contra
los lados o el fondo de la pantalla, pero eso es sólo un caso mas al comprobar
las coordenadas del segmento y cambiar su dirección, o su traslado a la parte
superior de la pantalla cuando sea necesario. Los segmentos también necesitan
ser borrados de sus antiguas posiciones antes de ser trasladados y mostrarlos
en sus nuevas posiciones, pero ya se han cubierto los pasos necesarios para
realizar esas tareas.
Nuestro nuevo código es el siguiente:
; Queremos una pantalla en negro. ld a,71 ; tinta blanca (7) en fondo negro (0), ; con brillo (64). ld (23693),a ; establecer nuestros colores de pantalla. xor a ; forma rápida de cargar el acumulador con cero. call 8859 ; establecer el colore del borde permanente. ; Configurar los gráficos. ld hl,blocks ; dirección de los datos para los UDG. ld (23675),hl ; apuntar los UDG hacia aquí. ; De acuerdo, vamos a empezar el juego. call 3503 ; rutina ROM - borra la pantalla, abre el canal 2. ; Inicializar coordenadas. ld hl,21+15*256 ; cargar el par hl con las coordenadas iniciales. ld (plx),hl ; fijar las coordenadas del jugador. ld b,10 ; número de segmentos a inicializar. ld hl,segmnt ; tabla de segmentos. segint ld (hl),1 ; comienza moviéndose a la derecha. inc hl ld (hl),0 ; comienza arriba. inc hl ld (hl),b ; usa el registro B para la coordenada y. inc hl djnz segint ; repetir hasta que todos se inicialicen. call basexy ; establecer las posiciones x e y del jugador. call splayr ; mostrar símbolo de la base del jugador. ; Ahora queremos llenar la zona de juegos con setas. ld a,68 ; tinta verde (4) en fondo negro (0), ; con brillo (64). ld (23695),a ; establecer nuestros colores temporales. ld b,50 ; comenzar con unas pocas. mushlp ld a,22 ; código del carácter de control para AT. rst 16 call random ; obtener un número 'aleatorio'. and 15 ; en vertical en rango de 0 a 15. rst 16 call random ; obtener otro número seudo-aleatorio. and 31 ; horizontal en el rango de 0 a 31. rst 16 ld a,145 ; el UDG 'B' es el gráfico de las setas. rst 16 ; poner la seta en la pantalla. djnz mushlp ; bucle hasta que todas las setas aparezcan. ; Este es el bucle principal. mloop equ $ ; Borrar el jugados. call basexy ; establecer las posiciones x e y del jugador. call wspace ; mostrar un espacio sobre el jugador. ; Ahora hemos eliminado el jugador y lo movemos antes de volverlo a mostrar ; en sus nuevas coordenadas. ld bc,63486 ; fila del teclado 1-5/joystick puerto 2. in a,(c) ; ver que teclas están pulsadas. rra ; bit mas externo = tecla 1. push af ; recordar el valor. call nc,mpl ; si está siendo pulsada, moverse a la izquierda. pop af ; restaurar el acumulador. rra ; siguiente bit (valor 2) = tecla 2. push af ; recordar el valor. call nc,mpr ; si está siendo pulsada, moverse a la derecha. pop af ; restaurar el acumulador. rra ; siguiente bit (valor 4) = tecla 3. push af ; recordar el valor. call nc,mpd ; si está siendo pulsada, moverse hacia abajo. pop af ; restaurar el acumulador. rra ; siguiente bit (valor 8) = tecla 4. call nc,mpu ; si está siendo pulsada, moverse hacia arriba. ; Ahora que se ha movido podemos volver a mostrar al jugador. call basexy ; establecer las posiciones x e y del jugador. call splayr ; mostrar al jugador. ; Ahora los segmentos del ciempiés. ld ix,segmnt ; tabla de datos del segmento. ld b,10 ; número de segmentos en la tabla. censeg push bc ld a,(ix) ; ¿está el segmento activado? inc a ; 255=desactivado, incrementar a cero. call nz,proseg ; si está activo, procesar el segmento. pop bc ld de,3 ; 3 bytes por segmento. add ix,de ; poner el nuevo segmento en el registro ix. djnz censeg ; repetir para todos los segmentos. halt ; retardo. ; Saltar de nuevo al principio del bucle principal. jp mloop ; Mover al jugador a la izquierda. mpl ld hl,ply ; recordar, ¡y es la coordenada horizontal! ld a,(hl) ; ¿cuál es el valor actual? and a ; ¿es cero? ret z ; sí - no podemos seguir hacia la izquierda. ; antes comprobar que no hay una seta en el camino. ld bc,(plx) ; coordenadas actuales. dec b ; mirar una posición a la izquierda. call atadd ; obtener la dirección del atributo en esta posición. cp 68 ; las setas son brillo (64) + verde (4). ret z ; hay una seta, no podemos movernos aquí. dec (hl) ; restar 1 a la coordenada y. ret ; Mover al jugador a la derecha. mpr ld hl,ply ; recordar, ¡y es la coordenada horizontal! ld a,(hl) ; ¿cuál es el valor actual? cp 31 ; ¿está en el borde derecho (31)? ret z ; sí - no podemos seguir hacia la derecha. ; antes comprobar que no hay una seta en el camino. ld bc,(plx) ; coordenadas actuales. inc b ; mirar una posición a la derecha. call atadd ; obtener la dirección del atributo en esta posición. cp 68 ; las setas son brillo (64) + verde (4). ret z ; hay una seta, no podemos movernos aquí. inc (hl) ; sumar 1 a la coordenada y. ret ; Mover al jugador hacia arriba. mpu ld hl,plx ; recordar, ¡x es la coordenada vertical! ld a,(hl) ; ¿cuál es el valor actual? cp 4 ; ¿está en el límite superior (4)? ret z ; sí - no podemos seguir hacia arriba. ; antes comprobar que no hay una seta en el camino. ld bc,(plx) ; coordenadas actuales. dec c ; mirar una posición hacia arriba. call atadd ; obtener la dirección del atributo en esta posición. cp 68 ; las setas son brillo (64) + verde (4). ret z ; hay una seta, no podemos movernos aquí. dec (hl) ; restar 1 a la coordenada x. ret ; Mover al jugador hacia abajo. mpd ld hl,plx ; recordar, ¡x es la coordenada vertical! ld a,(hl) ; ¿cuál es el valor actual? cp 21 ; ¿está en el límite inferior (21)? ret z ; sí - no podemos seguir hacia abajo. ; antes comprobar que no hay una seta en el camino. ld bc,(plx) ; coordenadas actuales. inc c ; mirar una posición hacia abajo. call atadd ; obtener la dirección del atributo en esta posición. cp 68 ; las setas son brillo (64) + verde (4). ret z ; hay una seta, no podemos movernos aquí. inc (hl) ; sumar 1 a la coordenada x. ret ; Configurar las coordenadas X e Y de la posición de la base del jugador, ; se le llama antes de la visualización y supresión de la base. basexy ld a,22 ; código para AT. rst 16 ld a,(plx) ; coordenada vertical del jugador. rst 16 ; fijar la posición vertical del jugador. ld a,(ply) ; coordenada horizontal del jugador. rst 16 ; fijar la posición horizontal del jugador. ret ; Mostrar al jugador en la posición de impresión actual. splayr ld a,69 ; tinta cían (5) en fondo negro (0), ; brillante (64). ld (23695),a ; establecer nuestros colores temporales de pantalla. ld a,144 ; código ASCII para el UDG 'A'. rst 16 ; dibujar jugador. ret wspace ld a,71 ; tinta blanca (7) en fondo negro (0), ; brillante (64). ld (23695),a ; establecer nuestros colores temporales de pantalla. ld a,32 ; carácter de ESPACIO. rst 16 ; dibujar espacio. ret segxy ld a,22 ; código ASCII para el carácter de AT. rst 16 ; presentar código AT. ld a,(ix+1) ; obtener la coordenada x del segmento. rst 16 ; posicionarse en esa coordenada. ld a,(ix+2) ; obtener la coordenada y del segmento. rst 16 ; posicionarse en esa coordenada. ret proseg ld a,(ix) ; verificar si el segmento está activo. inc a ; para la rutina de detección de colisiones. ret z ; está activo, por tanto pasa a muerto. call segxy ; actualizar las coordenadas del segmento. call wspace ; presentar un espacio, blanco sobre fondo negro. call segmov ; mover el segmento. ld a,(ix) ; verificar si el segmento está activado. inc a ; para la rutina de detección de colisiones. ret z ; está activo, por tanto pasa a muerto. call segxy ; actualizar las coordenadas del segmento. ld a,2 ; código de atributo = 2, segmento rojo. ld (23695),a ; actualizar los atributos temporales. ld a,146 ; UDG 'C' para presentar el segmento. rst 16 ret segmov ld a,(ix+1) ; coordenada x. ld c,a ; área x GP. ld a,(ix+2) ; coordenada y. ld b,a ; área y GP. ld a,(ix) ; indicador de estado. and a ; ¿está el segmento en el borde izquierdo? jr z,segml ; ir a la izquierda, saltar según ese bit de código. ; ¡ahora el segmento se mueve a la derecha! segmr ld a,(ix+2) ; coordenada y. cp 31 ; ¿está en el borde derecho de la pantalla? jr z,segmd ; si, mover el segmento hacia abajo. inc a ; ver la izquierda. ld b,a ; actualizar la coordenada y GP. call atadd ; mirar el atributo de esa dirección. cp 68 ; setas son brillo (64) + verde (4). jr z,segmd ; seta a la derecha, moverse hacia abajo. inc (ix+2) ; sin obstáculos, seguir hacia la derecha. ret ; ¡ahora el segmento se mueve a la izquierda! segml ld a,(ix+2) ; coordenada y. and a ; ¿está en el borde izquierdo de la pantalla? jr z,segmd ; si, mover el segmento hacia abajo. dec a ; ver la derecha. ld b,a ; actualizar la coordenada y GP. call atadd ; mirar el atributo en la dirección (dispx,dispy). cp 68 ; setas son brillo (64) + verde (4). jr z,segmd ; seta a la izquierda, moverse abajo. dec (ix+2) ; sin obstáculos, seguir a la izquierda. ret ; ¡ahora el segmento se mueve hacia abajo! segmd ld a,(ix) ; dirección del segmento. xor 1 ; cambiarla. ld (ix),a ; guardar la nueva dirección. ld a,(ix+1) ; coordenada y. cp 21 ; ¿llegamos al final de la pantalla? jr z,segmt ; si, mover el segmento al inicio. ; En este momento nos estamos moviendo hacia abajo independientemente de las ; setas que pueden bloquear la trayectoria del segmento. Cualquier cosa en el ; camino del segmento será borrada. inc (ix+1) ; no ha llegado a la parte inferior, bajar. ret ; mover segmento a la parte superior de la pantalla. segmt xor a ; igual que ld a,0 pero ahorra 1 byte. ld (ix+1),a ; nueva coordenada x = inicio de la pantalla. ret ; Sencillo generador de números seudo-aleatorio. ; Seguir un puntero a través de la ROM (a partir de una semilla), ; retornando el contenido del byte en esa posición. random ld hl,(seed) ; puntero ld a,h and 31 ; mantenerlo en los primeros 8Kb de ROM. ld h,a ld a,(hl) ; Obtener el número "aleatorio" de esa ubicación. inc hl ; Incrementar el puntero. ld (seed),hl ret seed defw 0 ; Calcular la dirección del atributo de carácter en (dispx, dispy). atadd ld a,c ; coordenada vertical. rrca ; multiplicar por 32. rrca ; desplazar a la derecha con acarreo 3 veces rrca ; mas rápido que desplazar izquierda 5 veces. ld e,a and 3 add a,88 ; 88x256=dirección de los atributos. ld d,a ld a,e and 224 ld e,a ld a,b ; posición horizontal. add a,e ld e,a ; de=dirección de los atributos. ld a,(de) ; devolver atributo en el acumulador. ret plx defb 0 ; coordenada x del jugador. ply defb 0 ; coordenada y del jugador. ; gráficos UDG. blocks defb 16,16,56,56,124,124,254,254 ; base del jugador. defb 24,126,255,255,60,60,60,60 ; seta. defb 24,126,126,255,255,126,126,24 ; segmento. ; Tabla de segmentos. ; Formato: 3 bytes por entrada, 10 segmentos. ; byte 1: 255=segmento apagado, 0=izquierda, 1=derecha. ; byte 2: coordenada x (vertical). ; byte 3: coordenada y (horizontal). segmnt defb 0,0,0 ; segmento 1. defb 0,0,0 ; segmento 2. defb 0,0,0 ; segmento 3. defb 0,0,0 ; segmento 4. defb 0,0,0 ; segmento 5. defb 0,0,0 ; segmento 6. defb 0,0,0 ; segmento 7. defb 0,0,0 ; segmento 8. defb 0,0,0 ; segmento 9. defb 0,0,0 ; segmento 10.
NdT: El direccionamiento indexado necesita sumar siempre una cantidad al puntero, por lo que si no se indica será cero, pero algunos ensambladores no soportan la instrucción (ix) a secas, debes usar en su lugar (ix+0), sería poner por ejemplo lo siguiente:
segmd ld a,(ix+0) ; dirección del segmento.
xor 1 ; cambiarla.
ld (ix+0),a ; guardar la nueva dirección.
ld a,(ix+1) ; coordenada y.
cp 21 ; ¿llegamos al final de la pantalla?
jr z,segmt ; si, mover el segmento al inicio.
NdT: Recomiendo no usar valores fijos en el código, en su lugar es mejor definir contantes, de esta manera es muy sencillo cambiar la tabla sin alterar los resultados. Por ejemplo en Pasmo podemos usar EQU para las constantes, y reservar el espacio de la tabla con DEFS (o su equivalente DS)ld b,NTabla ; número de segmentos a inicializar. Mejor usar una constante .......... ld de,LTabla ; 3 bytes por segmento, pero mejor usar una constante .......... ; Tabla de segmentos. ; Formato: 3 bytes por entrada, 10 segmentos. ; byte 1: 255=segmento apagado, 0=izquierda, 1=derecha. ; byte 2: coordenada x (vertical). ; byte 3: coordenada y (horizontal). LTabla equ 3 ; cada entrada de la tabla tiene 3 elementos NTabla equ 10 ; tenemos 10 entradas en la tabla segmnt defs NTabla*LTabla,0 ; Reservamos espacio para la tabla, rellena a ceros ............
No hay comentarios:
Publicar un comentario