jueves, 5 de mayo de 2016

Cómo escribir juegos para el ZX Spectrum. Capítulo 15

Í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.

Matemáticas

Sumar y restar es bastante sencillo para la CPU del Spectrum, tenemos una gran cantidad de instrucciones para llevar a cabo estas tareas. Pero a diferencia de algunos procesadores posteriores en su misma serie, el programador del Z80A tiene que hacer por si mismo la multiplicación y la división. Aunque estos cálculos son poco frecuentes, tienen sus usos en ciertos tipos de juego y hasta que tengas rutinas para hacerlo, ciertas cosas son difíciles de hacer. Por ejemplo, sin el teorema de Pitágoras puede ser difícil programar para que un sprite enemigo dispare al jugador con cierto grado de precisión.

Supongamos que un sprite A necesita hacer un disparo al sprite B. Tenemos que encontrar el ángulo en el que el sprite A debe disparar y necesitamos algo de trigonometría para hacerlo. Sabemos las coordenadas de los sprites, Ax, Ay, Bx y By, y las distancias entre estos, Bx-Ax y By-Ay, eso nos darán la longitud de los catetos opuesto y contiguo. Por desgracia, la única manera de calcular el ángulo entre opuesto y contiguo es el uso del arco tangente, y las tangentes solo son adecuadas para ciertos ángulos, por lo que es mejor usar senos o cosenos en su lugar. Así que con el fin de encontrar el ángulo entre el sprite A y el sprite B, tenemos que encontrar la longitud de la hipotenusa.

La hipotenusa se calcula elevando al cuadrado la distancia en x, añadiéndola al cuadrado de la distancia en y, para a continuación encontrar la raíz cuadrada de la suma. Existen rutinas en la ROM de Sinclair para hacer todo esto, pero hay un serio inconveniente: como puede decirte cualquier persona que haya usado Sinclair BASIC alguna vez, las rutinas matemáticas son increíblemente lentas como para usarlas en la escritura de juegos. Así que tenemos que poner a trabajar los dedos y escribir nuestras propias rutinas.

Los cuadrados de nuestras distancias x e y se consiguen usando una rutina de multiplicación, multiplicando los números por sí mismos. Afortunadamente, esta parte es relativamente indolora. La multiplicación se logra de la misma manera como se haría la multiplicación larga en papel, aunque esta vez estamos trabajando en binario. Todo lo que se requiere es rotaciones, pruebas de bits y sumas. Cuando un bit exista en nuestro primer factor, añadimos el segundo factor al total. Luego desplazamos el segundo factor a la izquierda, y probamos el siguiente bit de nuestro primer factor. La rutina de abajo, tomada del juego Kuiper Persecution, demuestra esta técnica, multiplica h por d y devuelve el resultado en hl.

imul   ld e,d              ; HL = H * D
       ld a,h              ; poner en el acumulador el primer multiplicando.
       ld hl,0             ; poner a cero el resultado.
       ld d,h              ; poner a cero el byte alto ya que de=multiplicador.
       ld b,8              ; repetir 8 veces.
imul1  rra                 ; rotar el bit mas a la derecha en el acarreo.
       jr nc,imul2         ; no se estableció.
       add hl,de           ; se estableció el bit, por lo que añadir a de.
       and a               ; restablecer acarreo.
imul2  rl e                ; aumentar 1 bit izquierdo.
       rl d
       djnz imul1          ; repetir 8 veces.
       ret

Ahora necesitamos una raíz cuadrada, que es donde empiezan los problemas. Las raíces cuadradas son mucho más complicadas. Esto significa hacer una gran cantidad de divisiones, por lo que primero necesitamos una rutina de división. Esto se puede ver como trabajar de manera opuesta a la multiplicación, desplazando y restando. La siguiente rutina, también del juego Kuiper Persecución, divide hl por d y devuelve el resultado en hl.

idiv   ld b,8              ; bits a comprobar.
       ld a,d              ; número por el que dividir.
idiv3  rla                 ; comprobar bit de la izquierda.
       jr c,idiv2          ; no requiere más desplazamientos.
       inc b               ; necesita desplazamientos extra.
       cp h
       jr nc,idiv2
       jp idiv3            ; repetir.

idiv2  xor a
       ld e,a
       ld c,a              ; resultado.
idiv1  sbc hl,de           ; restarlo.
       jr nc,idiv0         ; no hay acarreo, mantener el resultado.
       add hl,de           ; restaurar el valor original de hl.
idiv0  ccf                 ; invertir bit de acarreo.
       rl c                ; rotar en ac.
       rla
       rr d                ; dividir por 2.
       rr e
       djnz idiv1          ; repetir.
       ld h,a              ; copiar el resultado a hl.
       ld l,c
       ret

De la misma manera que la multiplicación se compone de desplazamiento y suma, y la división se realiza mediante desplazamiento y resta, las raíces cuadradas se pueden calcular por desplazamiento y división. Simplemente estamos tratando de encontrar el número que "mejor se ajuste", de forma que multiplicado por sí mismo nos da el número con el que empezamos. No voy a entrar en la explicación detallada de cómo trabaja la siguiente rutina, si estás muy interesado sigue mis comentarios y haz una ejecución paso a paso con un depurador. Tomado del juego Blizzard's Rift, devuelve la raíz cuadrada de hl en el acumulador.

isqr   ld (sqbuf0),hl      ;  número del que queremos encontrar la raíz cuadrada.
       xor a               ; poner a cero el acumulador.
       ld (sqbuf2),a       ; buffer resultado.
       ld a,128            ; comenzar con la división con el bit alto.
       ld (sqbuf1),a       ; siguiente divisor.
       ld b,8              ; 8 bits a dividir.
isqr1  push bc             ; guardar contador de bucles.
       ld a,(sqbuf2)       ; resultado actual.
       ld d,a
       ld a,(sqbuf1)       ; siguiente bit a comprobar.
       or d                ; combinar con divisor.
       ld d,a              ; almacenar byte bajo.
       xor a               ; HL = HL / D
       ld c,a              ; poner a cero c.
       ld e,a              ; poner a cero e.
       push de             ; recordar divisor.
       ld hl,(sqbuf0)      ; número original.
       call idiv4          ; dividir el número por d.
       pop de              ; restaurar divisor.
       cp d                ; ¿el divisor es mayor que el resultado?
       jr c,isqr0          ; sí, no guardar este bit entonces.
       ld a,d
       ld (sqbuf2),a       ; guardar nuevo divisor.
isqr0  ld hl,sqbuf1        ; bit que comprobamos.
       and a               ; limpiar bandera de acarreo.
       rr (hl)             ; siguiente bit a la derecha.
       pop bc              ; restaurar el contador del bucle.
       djnz isqr1          ; repetir.
       ld a,(sqbuf2)       ; retornar resultado en hl.
       ret
sqbuf0 defw 0
sqbuf1 defb 0
sqbuf2 defb 0

Con la longitud de la hipotenusa calculada, podemos dividir simplemente el cateto opuesto por la hipotenusa para encontrar el coseno del ángulo. Una búsqueda rápida en nuestra tabla de senos nos dará el ángulo. ¡Uf!

Este es todo el cálculo tomado del juego Blizzard's Rift. Debes notar que usa la longitud del cateto contiguo en lugar del opuesto, por lo que se encuentra el arco coseno en lugar del arco seno. Solo se utiliza cuando la nave está por encima del cañón de la torreta, dando al jugador la oportunidad de acercarse sigilosamente y atacar desde abajo. Sin embargo demuestra cómo un sprite puede disparar a otros con precisión mortal. Si has jugado alguna vez al Blizzard's Rift, sabrás exactamente lo letales que pueden ser los cañones de las torretas.

; La nave debe estar por encima del cañón para que podamos emplear trigonometría 
; básica para apuntar.
; Tenemos que encontrar el ángulo, para ello dividimos el cateto contiguo por
; la hipotenusa y encontramos el arco coseno.

; En primer lugar ponemos la distancia al oponente en la pila:

mgunx  ld a,(nshipy)       ; coordenada y de la nave.
       ld hl,guny          ; coordenada y del cañón.
       sub (hl)            ; encontrar la diferencia.
       jr nc,mgun0         ; el resultado fue positivo.
       neg                 ; negativo, hacerlo positivo.
mgun0  cp 5                ; ¿diferencia y menor de 5?
       jr c,mgunu          ; sí, apuntar hacia arriba.
       push af             ; colocar longitud del oponente en la pila.

; A continuación se necesita la longitud de la hipotenusa para lo que
; podemos usar el viejo teorema de Pitágoras.

       ld h,a              ; copiar a en h.
       ld d,h              ; copiar h en d.
       call imul           ; multiplicar parte entera obtener resultado 16 bits.
       push hl             ; guardar el valor al cuadrado.

       ld hl,nshipx        ; cañón coordenada x.
       ld a,(gunx)         ; nave coordenada x.
       sub (hl)            ; encontrar la diferencia, siempre será positiva.
       ld h,a              ; poner la diferencia de x en h.
       ld d,h              ; copiar h en d.
       call imul           ; multiplicar h por d para obtener el cuadrado.

       pop de              ; obtener el último resultado al cuadrado.
       add hl,de           ; necesitamos la suma de los dos.
       call isqr           ; encontrar la raíz cuadrada, hipotenusa en a.
       pop de              ; cateto opuesto ahora en el registro d..

       ld h,a              ; longitud de la hipotenusa.
       ld l,0              ; ninguna fracción o signo.
       ex de,hl            ; intercambiarlos.

; Cateto opuesto e hipotenusa se encuentran ahora en de y hl.
; Ahora dividimos la primera por la segunda y encontramos el arco seno.
; Recuerda: seno = cateto opuesto sobre la hipotenusa.

       call div            ; la división nos dará el seno.
       ex de,hl            ; queremos el resultado en de.
       call asn            ; obtener arco seno para encontrar el ángulo.
       push af

; Bien, tenemos el ángulo, pero está sólo entre 0 y PI/2 radianes (64 ángulos)
; por lo que tenemos que hacer un ajuste en base al cuadrante del círculo.
; Podemos establecer en qué cuadrante del círculo está nuestro ángulo
; examinando la diferencia entre las coordenadas de la nave y el arma.

       ld a,(guny)         ; cañón posición y.
       ld hl,shipy         ; nave y.
       cp (hl)             ; ¿está la nave a la derecha?
       jr nc,mgun2         ; jugador a la izquierda, ángulo en segundo cuadrante.

; Ángulo del jugador en el primer cuadrante, por lo que debemos restar de 64.

       ld a,64             ; pi/2 radianes = 64 ángulos.
       pop bc              ; ángulo en b.
       sub b               ; restarlos.
       ld (ix+1),a         ; nuevo ángulo.
       ret                 ; tenemos nuestro ángulo.

; Segundo cuadrante, añadir el literal 64 a nuestro ángulo.

mgun2  pop af              ; ángulo original.
       add a,192           ; sumar pi/2 radianes.
       ld (ix+1),a         ; nuevo ángulo.
       ret                 ; ¡buen trabajo!

No hay comentarios:

Publicar un comentario