¡Hola a todos!, hay problemas con la web que espero poder arreglar lo antes posible, ¡mil perdones!

Espamatica

Espamatica
Visitar página web/blogCastellanoEspaña58 entradas

Espamatica es una página web/blog de España en castellano que ha publicado 58 entradas, siendo la última del día 13/05/2022.

Programación en 8 bits y algo más.

Ver índice de webs/blogs

0x02 Ensamblador ZX Spectrum Marciano – Pintando UDG [Espamatica] [Leer]


En este capítulo de Ensamblador ZX Spectrum Marciano, vamos empezar a dibujar usando UDG. El mapa de caracteres del ZX Spectrum está compuesto por doscientos cincuenta y seis valores, de los cuales podemos redefinir veintiuno, en concreto los que se encuentran entre el $90 (144) y el $A4 (164), ambos inclusive.

Antes de empezar, creamos una carpeta que se llame Paso02 y copiamos el arhivo Var.asm (dónde definimos los gráficos que vamos a usar) desde la carpeta Paso01.

¿Dónde están los UDG?

El valor de la dirección de memoria $5C7B contiene la dirección de memoria donde están los gráficos definidos por el usuario, por lo que lo único que tenemos que hacer es cargar en esa dirección de memoria, la dirección dónde están definidos nuestros gráficos. Una vez que lo tengamos, al pintar con RST $10 cualquier carácter comprendido entre $90 y $A4, pintara los gráficos que hemos definido.

Vamos a crear un nuevo fichero llamado Const.asm y vamos a añadir la línea siguiente:

; Dirección de memoria donde se cargan los gráficos definidos por el usuario. UDG: EQU $5c7b

En esta constante guardamos la dirección de memoria dónde cargaremos la dirección dónde están nuestros gráficos.

Pintamos nuestros UDG

Es el momento de hacer nuestra primera prueba, vamos a pintar los UDG. Creamos un fichero llamado Main.asm y vamos a añadir las líneas siguientes:

org $5dad Main: ld a, $90 ld b, $15 Loop: push af rst $10 pop af inc a djnz Loop ret end Main

Lo primero que hacemos es indicar dónde se va a cargar el programa, ORG $5DAD. El programa lo cargamos en la posición $5DAD (23981), ya que Batalla espacial va a ser un programa compatible con modelos 16K.

La siguiente línea es una etiqueta, Main, el punto de entrada del programa.

Lo siguiente que hacemos es cargar 144 en A, LD A, $90, y 21 en B, LD B, $15, para pintar desde el carácter 144 al 164, haciendo un total de veintiún caracteres.

El siguiente paso es hacer el bucle de veintiuna iteraciones, empezando con la etiqueta del mismo, Loop, y a continuación preservamos en la pila el valor del registro A, PUSH AF, lo cual es muy importante ya que la siguiente instrucción, RST $10, imprime en pantalla el carácter cuyo código esté cargado en el registro A, y luego modifica el valor de dicho registro. Acto seguido recuperamos de la pila el valor del registro A, POP AF.

A continuación, incrementamos A, INC A, para que apunte al siguiente carácter y decrementamos B y saltamos a Loop si no ha llegado a cero, DJNZ Loop. Por último volvemos al Basic, RET.

Con la última línea le indicamos a PASMO que debe incluir en el cargador Basic una llamada a la dirección de memoria donde se encuentra la etiqueta Main.

Es el momento de compilar y ver los resultados en el emulador.

pasmo --name Marciano --tapbas Main.asm Maciano.tap --public

Pero, ¿hemos pintado nuestros gráficos?

Ensamblador ZX Spectrum, pitando UDGEnsamblador ZX Spectrum, pitando UDG

Como podemos ver, hemos pintado las letras mayúsculas de la A a la U, esto es debido a que en ningún momento hemos indicado dónde están nuestros gráficos.

Seguimos en el archivo Main.asm, justo debajo de la etiqueta Main añadimos las líneas siguientes:

ld hl, udgsCommon ld (UDG), hl

Cargamos en HL la dirección de memoria dónde están nuestros gráficos, LD HL, udgsCommon, y luego cargamos ese valor en la dirección de memoria en la que hay que indicar dónde están nuestros gráficos, LD (UDG), HL.

Dado que tanto udgsCommon, como UDG no están definidas en el fichero Main.asm, justo detrás de la instrucción RET hay que añadir los includes para los ficheros Const.asm y Var.asm.

include "Const.asm" include "Var.asm"

Ahora sí, podemos volver a compilar el programa, cargarlo en el emulador y ver nuestros gráficos en pantalla.

Ensamblador ZX Spectrum, pitando UDGEnsamblador ZX Spectrum, pitando UDG

Mucho mejor, ¿verdad? Pero, hemos pintado veintiún gráficos: la nave, el disparo, la explosión, el marco, el carácter vacío, los cuatro gráficos del enemigo uno, y dos gráficos del enemigo dos. ¿Cómo vamos a pintar los otros dos gráficos del enemigo dos y los gráficos de los veintiocho enemigos restantes?

Cargamos los UDG de los enemigos

Si observáis la definición de los gráficos, la primera etiqueta se llama udgsCommon, y esto debería darnos una pista de como lo vamos a hacer. Como UDG comunes tenemos definidos quince gráficos (nave, disparo, explosión, marco y blanco), por lo que vamos a definir treinta y dos bytes para poder ir volcando en ellos los gráficos de los enemigos; lo vamos a hacer así porque los enemigos son uno por nivel, y la operación de volcado solo la tenemos que hacer una vez, justo con el cambio de nivel.

En el archivo Var.asm, justo por encima de la etiqueta udgsEnemiesLeve1 añadimos las siguientes líneas:

udgsExtension: db $00, $00, $00, $00, $00, $00, $00, $00 ; $9f Left/Up db $00, $00, $00, $00, $00, $00, $00, $00 ; $a0 Rigth/Up db $00, $00, $00, $00, $00, $00, $00, $00 ; $a1 Left/Down db $00, $00, $00, $00, $00, $00, $00, $00 ; $a2 Rigth/Down

En este bloque de memoria es dónde vamos a ir volcando los gráficos de los enemigos, dependiendo del nivel en el que nos encontremos.

Probad a compilar ahora y observad que pinta. ¿Faltan los gráficos del enemigo uno verdad? Está pintando udgsExtension.

Creamos un nuevo archivo, Graph.asm, y vamos a implementar en él la rutina que carga en udgsExtension los gráficos de los enemigos de cada nivel, dato que recibe en A.

Para calcular la dirección de memoria donde se encuentran los gráficos, vamos a multiplicar el nivel por treinta y dos (bytes que ocupan los gráficos) y el resultado se lo vamos a sumar a la dirección de memoria donde se encuentran los gráficos del primer enemigo.

LoadUdgsEnemies: dec a ld h, $00 ld l, a

Dado que los niveles van de uno a treinta, decrementamos A, DEC A, para que no sume un nivel de más (si el nivel es cero, tiene que sumar cero veces a udgsEnemies, si es dos una vez, etc.).

Lo siguiente es cargar el nivel en HL, para lo cual cargamos cero en H, LD H, $00, y el nivel en L, LD L, A.

add hl, hl add hl, hl add hl, hl add hl, hl add hl, hl

Multiplicamos el nivel por treinta y dos, sumando HL a si mismo cinco veces, ADD HL, HL. La primera suma es igual a multiplicar por dos, la segunda por cuatro y las siguientes por ocho, por dieciséis, y por treinta y dos.

ld de, udgsEnemiesLevel1 add hl, de ld de, udgsExtension ld bc, $20 ldir ret

Por último, cargamos la dirección de los gráficos del enemigo uno en DE, LD DE, udgsEnemiesLevel1, y se lo sumamos a HL, ADD HL, DE, cargamos la dirección de la extensión de udgs en DE, LD DE, udgsExtension, cargamos en BC en número de bytes que vamos a cargar en udgsExtension, LD BC, $20, y cargamos los treinta y dos bytes de los gráficos del enemigo del nivel a udgsExtension, LDIR. Finalmente salimos, RET.

El aspecto final de la rutina es el siguiente:

; ---------------------------------------------------------------------------- ; Carga los gráficos definidos por el usuario relativos a los enemigos ; ; Entrada: A -> Nivel de 1 a 30 ; ; Altera el valor de los registros A, BC, DE y HL ; ---------------------------------------------------------------------------- LoadUdgsEnemies: dec a ; Decrementa A para que no sume un nivel de más ld h, $00 ld l, a ; Carga en HL el nivel add hl, hl ; Multiplica por 2 add hl, hl ; por 4 add hl, hl ; por 8 add hl, hl ; por 16 add hl, hl ; por 32 ld de, udgsEnemiesLevel1 ; Carga la dirección de los gráficos del ; enemigo 1 en DE add hl, de ; Lo suma a HL ld de, udgsExtension ; Carga en DE la dirección de la extensión ld bc, $20 ; Carga en BC el número de bytes a copiar, 32 ldir ; Copia los bytes del enemigo en los de extensión ret

Y ahora vamos a probar la nueva rutina, para lo cual vamos a editar el archivo Main.asm, empezando por cambiar la instrucción LD B, $15, justo encima de la etiqueta Loop, y la dejamos como sigue, para imprimir los quince primeros UDG, los comunes:

ld b, $0f

El resto lo vamos a implementar entre la instrucción DJNZ Loop y la instrucción RET.

ld a, $01 ld b, $1e

Cargamos en A el nivel uno, LD A, $01, y en B el número de niveles totales (treinta), LD B, $1E. Implementamos un bucle para pintar los enemigos de los treinta niveles.

Loop2: push af push bc call LoadUdgsEnemies

Preservamos los valores de AF, PUSH AF, y de BC, PUSH BC, ya que usamos A y B para controlar que enemigos pintamos y las iteraciones del bucle. A continuación, llamamos a la rutina que carga los gráficos del enemigo del nivel en udgsExtension, CALL LoadUdgsEnemies.

ld a, $9f rst $10 ld a, $a0 rst $10 ld a, $a1 rst $10 ld a, $a2 rst $10

Los caracteres correspondientes a los gráficos de los enemigos son $9F, $A0, $A1 y $A2; los vamos cargando en A, LD A, $9F, y pintando, RST $10. Repetimos la operación con $A0, $A1 y $A2.

pop bc pop af inc a djnz Loop2

Recuperamos el valor de BC, POP BC, de AF, POP AF, incrementamos A para pasar al siguiente nivel, INC A, y repetimos hasta que B sea 0, DJNZ Loop2.

Por último, a final del fichero y antes de END Main, incluimos el fichero Graph.asm.

include "Graph.asm"

El código final de Main.asm es el siguiente:

org $5dad Main: ld hl, udgsCommon ld (UDG), hl ld a, $90 ld b, $0f Loop: push af rst $10 pop af inc a djnz Loop ld a, $01 ld b, $1e Loop2: push af push bc call LoadUdgsEnemies ld a, $9f rst $10 ld a, $a0 rst $10 ld a, $a1 rst $10 ld a, $a2 rst $10 pop bc pop af inc a djnz Loop2 ret include "Const.asm" include "Var.asm" include "Graph.asm" end Main

Compilamos y cargamos en el emulador; ya pintamos todos nuestros gráficos.

Ensamblador ZX Spectrum, pintando UDGEnsamblador ZX Spectrum, pintando UDG
Ensamblador ZX Spectrum, conclusión

Llegados a este punto, ya tenemos definidos todos los gráficos y hemos aprendido como pintarlos.

En el próximo capítulo pintaremos el área de juego.

El código generado lo podéis descargar desde aquí.

Enlaces de interés

Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x01 Ensamblador ZX Spectrum Marciano – Gráficos [Espamatica] [Leer]


En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a definir todos los gráficos que vamos a usar en Batalla espacial, aprovechando para practicar la conversión hexadecimal/binario, por lo que vamos a hacer nuestra primera práctica.

UDG

Ya comenté en el capítulo anterior que estoy usando una Raspberry Pi 400, y eso me impone alguna limitación, como que no tengo disponible el programa que suelo usar para diseñar los gráficos para ZX Spectrum, así que he hecho dos plantillas para usar con GIMP, y que simula el área de dibujo de ZX Paintbrush, que es el programa que uso en Windows.

Las plantillas que he preparado son las siguientes:

  • 8x8Template: para diseñar gráficos de 8×8 píxeles.
  • 256x192Template: tapiz con el tamaño de la pantalla del ZX Spectrum.

Para batalla espacial vamos a usar 8x8Template, os pondré la imagen de los gráficos, los códigos hexadecimales de los mismos, y vuestra labor consistirá en convertir, de cabeza, esos códigos hexadecimales para dibujar los gráficos en las plantillas.

Ensamblador ZX Spectrum, sección plantilla 8x8Sección plantilla 8×8
Ensamblador ZX Spectrum, sección plantilla 256x192Sección plantilla 256×192
Conversión hexadecimal/binario

Aunque en un primer momento pueda resultar complicado hacer la conversión de un número hexadecimal a binario, y viceversa, la realidad es que es muy sencillo y prácticamente directa, necesitamos saber el valor de cada bit (pixel) en bloques de cuatro, lo que nos da un valor comprendido entre 0 y F, que es el valor que se puede representar con cada dígito hexadecimal.

En un byte, cada bit a uno tiene un valor específico, siendo los siguientes:

Bit76543210
Valor1286432168421
Peso de cada bit de un byte

Cuando hacemos la conversión hexadecimal/binario, dividimos el byte en dos bloques de cuatro bits (nibble), lo que resulta en un rango de valores entre 0 y F (8 + 4 + 2 + 1 = 15 = F). De esta manera, para convertir de binario a hexadecimal, tan solo hay que sumar el valor de los bits a 1 de cada nibble, lo cual nos da el valor en hexadecimal.

Suponed que tenemos el siguiente valor en binario:

01011001

Si sumamos los valores de los nibbles, el resultado sería:

0 + 4 + 0 + 1 = 58 + 0 + 0 + 1 = 9

Resultando que 01011001 en hexadecimal es 59.

En hexadecimal, un byte se representa con dos dígitos. Pero, ¿qué pasa si el valor de algunos de los nibbles es mayor de 9? Veamos un ejemplo:

11011011 = 8 + 4 + 0 + 1 = 13 y 8 + 0 + 2 + 1 = 11

¿Cómo representamos 13 y 11 con solo dos dígitos? En hexadecimal los valores de 10 a 15 se representan con letras, usando la siguiente nomenclatura:

Decimal123456789101112131415
Hexadecimal123456789ABCDEF
Valores decimales/hexadecimales

Por lo que, en el ejemplo anterior, el valor hexadecimal de 11011011 es DB.

Practicado la conversión hexadecimal/binario

Una buena forma de aprender es con la práctica, y eso es lo que propongo a continuación; vamos a ver la definición de cada uno de los UDG que vamos a usar (los valores hexadecimales) y vamos a dibujarlos haciendo la conversión de hexadecimal a binario.

Al trabajar con nibble (4 bits), la tabla de conversión para un byte sería la siguiente:

Byte76543210
Valor84218421
Peso de los bits por nibble

Vamos a crear la carpeta Paso01, y dentro de ella el archivo Var.asm. Teniendo esta tabla a mano, vamos a dibujar la nave, cuya definición en hexadecimal (que vamos a copiar en el archivo creado) es la siguiente:

udgsCommon: db $24, $42, $99, $bd, $ff, $18, $24, $5a ; $90 Nave

Vamos a hacer la conversión a binario.

Byte76543210
Valor84218421
$24

X

X

$42
X



X
$99X

XX

X
$bdX
XXXX
X
$ffXXXXXXXX
$18


XX


$24

X

X

$5a
X
XX
X
Gráfico de la nave

Si trasladáis esta conversión a ZX Paintbrush, o a las plantillas que os he dejado, el resultado debe ser el siguiente, aquí está nuestra nave:

Ensamblador ZX Spectrum, naveNave

Vamos a seguir practicando, haciendo las conversiones para el disparo y la animación de la explosión de la nave. Es importante que intentéis trasladar vosotros de hexadecimal a binario, y de ahí al gráfico.

db $00, $18, $24, $5a, $5a, $24, $18, $00 ; $91 Disparo
Byte76543210
Valor84218421
$00







$18


XX


$24

X

X

$5a
X
XX
X
$5a
X
XX
X
$24

X

X

$18


XX


$00







Gráfico del disparo
Ensamblador ZX Spectrum, disparoDisparo
db $00, $00, $00, $00, $24, $5a, $24, $18 ; $92 Explosión 1
Byte76543210
Valor84218421
$00







$00







$00







$00







$24

X

X

$5a
X
XX
X
$24

X

X

$18


XX


Gráfico uno de la explosión
db $00, $00, $00, $14, $2a, $34, $24, $18 ; $93 Explosión 2
Byte76543210
Valor84218421
$00







$00







$00







$14


X
X

$2a

X
X
X
$34

XX
X

$24

X

X

$18


XX


Gráfico dos de la explosión
db $00, $00, $0c, $12, $2a, $56, $64, $18 ; $94 Explosión 3
Byte76543210
Valor84218421
$00







$00







$0c



XX

$12


X

X
$2a

X
X
X
$56
X
X
XX
$64
XX

X

$18


XX


Gráfico tres de la explosión
db $20, $51, $92, $d5, $a9, $72, $2c, $18 ; $95 Explosión 4
Byte76543210
Valor84218421
$20

X




$51
X
X


X
$92X

X

X
$d5XX
X
X
X
$a9X
X
X

X
$72
XXX

X
$2c

X
XX

$18


XX


Gráfico cuatro de la explosión
Ensamblador ZX Spectrum, explosiónExplosión

A partir de aquí solo voy a poner la definición hexadecimal y la imagen del aspecto final de cada UDG.

Quizá os preguntéis que significa el número que hay en cada comentario de cada definición; es el código del carácter que estamos redefiniendo, ese el código de carácter que mandaremos a imprimir para pintar el gráfico. No os preocupéis si ahora no lo entendéis, más adelante lo veréis mucho más claro.

db $3f, $6a, $ff, $b8, $f3, $a7, $ef, $ae ; $96 Esquina superior izquierda db $ff, $aa, $ff, $00, $ff, $ff, $00, $00 ; $97 Horizontal superior db $fc, $ae, $fb, $1f, $cd, $e7, $f5, $77 ; $98 Esquina superior derecha db $ec, $ac, $ec, $ac, $ec, $ac, $ec, $ac ; $99 Lateral izquierda db $35, $37, $35, $37, $35, $37, $35, $37 ; $9a Lateral derecha db $ee, $af, $e7, $b3, $f8, $df, $75, $3f ; $9b Esquina inferior izquierda db $00, $00, $ff, $ff, $00, $ff, $55, $ff ; $9c Horizontal inferior db $75, $f7, $e5, $cf, $1d, $ff, $56, $fc ; $9d Esquina inferior derecha db $00, $00, $00, $00, $00, $00, $00, $00 ; $9e Blanco
Ensamblador ZX Spectrum, marcoMarco
udgsEnemiesLevel1: db $8c, $42, $2d, $1d, $b4, $be, $46, $30 ; $9f Left/Up db $31, $42, $b4, $b8, $2d, $7d, $62, $0c ; $a0 Rigth/Up db $30, $46, $be, $b4, $1d, $2d, $42, $8c ; $a1 Left/Down db $0c, $62, $7d, $2d, $b8, $b4, $42, $31 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel unoEnemigos nivel uno
udgsEnemiesLevel2: db $c0, $fb, $69, $5d, $7b, $14, $4a, $79 ; $9f Left/Up db $03, $df, $96, $ba, $de, $28, $52, $9e ; $a0 Rigth/Up db $79, $4a, $14, $7b, $5d, $69, $fb, $c0 ; $a1 Left/Down db $9e, $52, $28, $de, $ba, $96, $df, $03 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel dosEnemigos nivel dos
udgsEnemiesLevel3: db $fc, $84, $b4, $af, $99, $f7, $14, $1c ; $9f Left/Up db $3f, $21, $2d, $f5, $99, $ef, $28, $38 ; $a0 Rigth/Up db $1c, $14, $f7, $99, $af, $b4, $84, $fc ; $a1 Left/Down db $38, $28, $ef, $99, $f5, $2d, $21, $3f ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel tresEnemigos nivel tres
udgsEnemiesLevel4: db $f2, $95, $98, $fe, $39, $55, $92, $4d ; $9f Left/Up db $4f, $a9, $19, $7f, $9c, $aa, $49, $b2 ; $a0 Rigth/Up db $4d, $92, $55, $39, $fe, $98, $95, $f2 ; $a1 Left/Down db $b2, $49, $aa, $9c, $7f, $19, $a9, $4f ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel cuatroEnemigos nivel cuatro
udgsEnemiesLevel5: db $76, $99, $a4, $d4, $47, $bd, $8a, $4c ; $9f Left/Up db $6e, $99, $25, $2b, $e2, $bd, $51, $32 ; $a0 Rigth/Up db $4c, $8a, $bd, $47, $d4, $a4, $99, $76 ; $a1 Left/Down db $32, $51, $bd, $e2, $2b, $25, $99, $6e ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel cincoEnemigos nivel cinco
udgsEnemiesLevel6: db $98, $66, $59, $aa, $b6, $49, $5a, $24 ; $9f Left/Up db $19, $66, $9a, $55, $6d, $92, $5a, $24 ; $a0 Rigth/Up db $24, $5a, $49, $b6, $aa, $59, $66, $98 ; $a1 Left/Down db $24, $5a, $92, $6d, $55, $9a, $66, $19 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel seisEnemigos nivel seis
udgsEnemiesLevel7: db $04, $72, $5d, $74, $2e, $be, $4c, $20 ; $9f Left/Up db $20, $4e, $ba, $2e, $74, $7d, $32, $04 ; $a0 Rigth/Up db $20, $4c, $be, $2e, $74, $5d, $72, $04 ; $a1 Left/Down db $04, $32, $7d, $74, $2e, $ba, $4e, $20 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel sieteEnemigos nivel siete
udgsEnemiesLevel8: db $00, $7c, $5a, $68, $7c, $4f, $26, $04 ; $9f Left/Up db $00, $3e, $5a, $16, $3e, $f2, $64, $20 ; $a0 Rigth/Up db $04, $26, $4f, $7c, $68, $5a, $7c, $00 ; $a1 Left/Down db $20, $64, $f2, $3e, $16, $5a, $3e, $00 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel ochoEnemigos nivel ocho
udgsEnemiesLevel9: db $e0, $d8, $b6, $6e, $5b, $36, $3c, $08 ; $9f Left/Up db $07, $1b, $6d, $76, $da, $6c, $3c, $10 ; $a0 Rigth/Up db $08, $3c, $36, $5b, $6e, $b6, $d8, $e0 ; $a1 Left/Down db $10, $3c, $6c, $da, $76, $6d, $1b, $07 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel nueveEnemigos nivel nueve
udgsEnemiesLevel10: db $e0, $ce, $bf, $3c, $73, $75, $6a, $2c ; $9f Left/Up db $07, $73, $fd, $3c, $ce, $ae, $56, $34 ; $a0 Rigth/Up db $2c, $6a, $75, $73, $3c, $bf, $ce, $e0 ; $a1 Left/Down db $34, $56, $ae, $ce, $3c, $fd, $73, $07 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel diezEnemigos nivel diez
udgsEnemiesLevel11: db $e0, $de, $bf, $7c, $7b, $75, $6a, $2c ; $9f Left/Up db $07, $7b, $fd, $3e, $de, $ae, $56, $34 ; $a0 Rigth/Up db $2c, $6a, $75, $7b, $7c, $bf, $de, $e0 ; $a1 Left/Down db $34, $56, $ae, $de, $3e, $fd, $7b, $07 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel onceEnemigos nivel once
udgsEnemiesLevel12: db $e0, $fe, $f7, $6c, $5f, $7e, $6c, $28 ; $9f Left/Up db $07, $7f, $ef, $36, $fa, $7e, $36, $14 ; $a0 Rigth/Up db $28, $6c, $7e, $5f, $6c, $f7, $fe, $e0 ; $a1 Left/Down db $14, $36, $7e, $fa, $36, $ef, $7f, $07 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel doceEnemigos nivel doce
udgsEnemiesLevel13: db $07, $6c, $7e, $34, $6f, $fb, $ae, $8c ; $9f Left/Up db $e0, $36, $7e, $2c, $f6, $df, $75, $31 ; $a0 Rigth/Up db $8c, $ae, $fb, $6f, $34, $7e, $6c, $07 ; $a1 Left/Down db $31, $75, $df, $f6, $2c, $7e, $36, $e0 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel treceEnemigos nivel trece
udgsEnemiesLevel14: db $21, $1a, $96, $75, $4c, $3c, $62, $90 ; $9f Left/Up db $84, $58, $69, $ae, $32, $3c, $46, $09 ; $a0 Rigth/Up db $90, $62, $3c, $4c, $75, $96, $1a, $21 ; $a1 Left/Down db $09, $46, $3c, $32, $ae, $69, $58, $84 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel catorceEnemigos nivel catorce
udgsEnemiesLevel15: db $04, $02, $0d, $14, $28, $b0, $40, $20 ; $9f Left/Up db $20, $40, $b0, $28, $14, $0d, $02, $04 ; $a0 Rigth/Up db $20, $40, $b0, $28, $14, $0d, $02, $04 ; $a1 Left/Down db $04, $02, $0d, $14, $28, $b0, $40, $20 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel quinceEnemigos nivel quince
udgsEnemiesLevel16: db $30, $48, $be, $b9, $7c, $2e, $27, $13 ; $9f Left/Up db $0c, $12, $7d, $9d, $3e, $74, $e4, $c8 ; $a0 Rigth/Up db $13, $27, $2e, $7c, $b9, $be, $48, $30 ; $a1 Left/Down db $c8, $e4, $74, $3e, $9d, $7d, $12, $0c ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel dieciséisEnemigos nivel dieciséis
udgsEnemiesLevel17: db $c0, $df, $36, $7c, $58, $77, $66, $44 ; $9f Left/Up db $03, $fb, $6c, $3e, $1a, $ee, $66, $22 ; $a0 Rigth/Up db $44, $66, $77, $58, $7c, $36, $df, $c0 ; $a1 Left/Down db $22, $66, $ee, $1a, $3e, $6c, $fb, $03 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel diecisieteEnemigos nivel diecisiete
udgsEnemiesLevel18: db $02, $71, $69, $57, $2f, $1e, $9e, $78 ; $9f Left/Up db $40, $8e, $96, $ea, $f4, $78, $79, $1e ; $a0 Rigth/Up db $78, $9e, $1e, $2f, $57, $69, $71, $02 ; $a1 Left/Down db $1e, $79, $78, $f4, $ea, $96, $8e, $40 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel dieciochoEnemigos nivel dieciocho
udgsEnemiesLevel19: db $20, $7f, $e6, $4e, $5e, $79, $78, $44 ; $9f Left/Up db $04, $fe, $67, $72, $7a, $9e, $1e, $22 ; $a0 Rigth/Up db $44, $78, $79, $5e, $4e, $e6, $7f, $20 ; $a1 Left/Down db $22, $1e, $9e, $7a, $72, $67, $fe, $04 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel diecinueveEnemigos nivel diecinueve
udgsEnemiesLevel20: db $36, $2f, $db, $be, $7c, $db, $f6, $64 ; $9f Left/Up db $6c, $f4, $db, $7d, $3e, $db, $6f, $26 ; $a0 Rigth/Up db $64, $f6, $db, $7c, $be, $db, $2f, $36 ; $a1 Left/Down db $26, $6f, $db, $3e, $7d, $db, $f4, $6c ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veinteEnemigos nivel veinte
udgsEnemiesLevel21: db $00, $70, $6e, $54, $2b, $34, $28, $08 ; $9f Left/Up db $00, $0e, $76, $2a, $d4, $2c, $14, $10 ; $a0 Rigth/Up db $08, $28, $34, $2b, $54, $6e, $70, $00 ; $a1 Left/Down db $10, $14, $2c, $d4, $2a, $76, $0e, $00 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veintiunoEnemigos nivel veintiuno
udgsEnemiesLevel22: db $00, $78, $6e, $56, $6d, $3b, $34, $0c ; $9f Left/Up db $00, $1e, $76, $6a, $b6, $dc, $2c, $30 ; $a0 Rigth/Up db $0c, $34, $3b, $6d, $56, $6e, $78, $00 ; $a1 Left/Down db $30, $2c, $dc, $b6, $6a, $76, $1e, $00 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veintidósEnemigos nivel veintidós
udgsEnemiesLevel23: db $0c, $02, $3d, $35, $ac, $b8, $40, $30 ; $9f Left/Up db $30, $40, $bc, $ac, $35, $1d, $02, $0c ; $a0 Rigth/Up db $30, $40, $b8, $ac, $35, $3d, $02, $0c ; $a1 Left/Down db $0c, $02, $1d, $35, $ac, $bc, $40, $30 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veintitrésEnemigos nivel veintitrés
udgsEnemiesLevel24: db $00, $77, $6e, $56, $2a, $74, $7b, $42 ; $9f Left/Up db $00, $ee, $76, $6a, $54, $2e, $de, $42 ; $a0 Rigth/Up db $42, $7b, $74, $2a, $56, $6e, $77, $00 ; $a1 Left/Down db $42, $de, $2e, $54, $6a, $76, $ee, $00 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veinticuatroEnemigos nivel veinticuatro
udgsEnemiesLevel25: db $c0, $ff, $76, $6c, $5f, $7e, $6c, $48 ; $9f Left/Up db $03, $ff, $6e, $36, $fa, $7e, $36, $12 ; $a0 Rigth/Up db $48, $6c, $7e, $5f, $6c, $76, $ff, $c0 ; $a1 Left/Down db $12, $36, $7e, $fa, $36, $6e, $ff, $03 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veinticincoEnemigos nivel veinticinco
udgsEnemiesLevel26: db $3c, $7e, $f7, $e8, $da, $e1, $68, $24 ; $9f Left/Up db $3c, $7e, $ef, $17, $5b, $87, $16, $24 ; $a0 Rigth/Up db $24, $68, $e1, $da, $e8, $f7, $7e, $3c ; $a1 Left/Down db $24, $16, $87, $5b, $17, $ef, $7e, $3c ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veintiséisEnemigos nivel veintiséis
udgsEnemiesLevel27: db $04, $02, $39, $2d, $3f, $9e, $4c, $38 ; $9f Left/Up db $20, $40, $9c, $b4, $fc, $79, $32, $1c ; $a0 Rigth/Up db $38, $4c, $9e, $3f, $2d, $39, $02, $04 ; $a1 Left/Down db $1c, $32, $79, $fc, $b4, $9c, $40, $20 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veintisieteEnemigos nivel veintisiete
udgsEnemiesLevel28: db $00, $37, $69, $5c, $34, $5f, $46, $64 ; $9f Left/Up db $00, $ec, $96, $3a, $2c, $fa, $62, $26 ; $a0 Rigth/Up db $64, $46, $5f, $34, $5c, $69, $37, $00 ; $a1 Left/Down db $26, $62, $fa, $2c, $3a, $96, $ec, $00 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel veintiochoEnemigos nivel veintiocho
udgsEnemiesLevel29: db $00, $37, $6d, $5e, $34, $7f, $56, $64 ; $9f Left/Up db $00, $ec, $b6, $7a, $2c, $fe, $6a, $26 ; $a0 Rigth/Up db $64, $56, $7f, $34, $5e, $6d, $37, $00 ; $a1 Left/Down db $26, $6a, $fe, $2c, $7a, $b6, $ec, $00 ; $a2 Rigth/Down
Ensambladdor ZX Spectrum, enemigos nivel veintinueveEnemigos nivel veintinueve
udgsEnemiesLevel30: db $e0, $ff, $ed, $5b, $7e, $6e, $5f, $72 ; $9f Left/Up db $07, $ff, $b7, $da, $7e, $76, $fa, $4e ; $a0 Rigth/Up db $72, $5f, $6e, $7e, $5b, $ed, $ff, $e0 ; $a1 Left/Down db $4e, $fa, $76, $7e, $da, $b7, $ff, $07 ; $a2 Rigth/Down
Ensamblador ZX Spectrum, enemigos nivel treintaEnemigos nivel treinta
Ensamblador ZX Spectrum, conclusión

Con esto ya tenemos definidos los gráficos que vamos a usar: tenemos la nave, el disparo, la explosión de la nave, el marco de la pantalla y los enemigos.

Podéis observar que todos los enemigos tienen el mismo código de carácter, lo cual es debido a que hay un número limitado de caracteres para usar como UDG. No os preocupéis, más adelante veremos una forma de salvar esta limitación.

Vuelvo a insistir en la importancia de practicar la conversión hexadecimal / binario, así que no lo dejéis para otro día, es un ejercicio sencillo.

En el próximo capítulo veremos como usar los UDG y pintaremos todos nuestros gráficos en pantalla.

El código generado, las imágenes y las plantillas las podéis descargar desde aquí.

Enlaces de interés

Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x00 Ensamblador ZX Spectrum Marciano – Introducción [Espamatica] [Leer]


Iniciamos un nuevo tutorial de programación en ensamblador para ZX Spectrum. Si eres nuevo, te recomiendo que primero visites el tutorial en el que vemos como programar un clon de Pong para ZX Spectrum.

Mata marcianos en ensamblador

En este caso vamos a desarrollar un mata marcianos, Batalla espacial, siendo este el primer programa que desarrollé en ensamblador para ZX Spectrum, quitándome de esta manera la espina que tenía clavada desde pequeño. Batalla espacial no es más que una prueba de los conocimientos adquiridos siguiendo el curso Curso de Ensamblador Z80 de Compiler Software.

Ya publiqué Batalla espacial anteriormente, aunque durante el desarrollo de este tutorial han cambiado varias cosas.

En Batalla espacial los gráficos son de un carácter, se usan UDG (User-Defined Graphic) y el movimiento es carácter a carácter, por lo que las diferencias con respecto a PorompomPong son patentes.

Batalla espacial hace uso de las interrupciones, cosa que en PorompomPong no hace, también hace un mayor uso de las rutinas de la ROM y al usar UDG y RST $10 para pintar, es necesario cambiar de canal para pintar en la parte superior de la pantalla, o en la línea de comandos.

Batalla Espacial, ensamblador para ZX SpectrumMata marcianos en ensamblador para ZX Spectrum Herramientas necesarias
  • Editor de texto, ya sea Notepad, Notepad++, Visual Studio Code o cualquier otro con el que os sintáis cómodos.
  • Compilador de ensamblador PASMO: está disponible para Windows, Linux y Mac, y es compatible con Raspberry Pi OS, que es el sistema desde el que estoy redactando el presente tutorial.
  • Emulador de ZX Spectrum: aquí tenéis varios para elegir como ZEsarUX, Fuse, Retro Virtual Machine, etc., dependiendo del sistema operativo que uséis. En mi caso vuelvo a optar por ZEsarUX.

Con respecto a PASMO, recomiendo a los usuarios de Windows que incluyan la ruta del ejecutable en la variable de entorno Path. Aquí tenéis un vídeo que muestra como hacerlo en Windows 10.

Estructura del tutorial

A diferencia de PorompomPong, Batalla espacial no se desarrolló con idea de realizar un tutorial, por lo que la estructura es distinta, no voy a ir desarrollando, haciendo y deshaciendo como hice en PorompomPong, en esta ocasión ya lo tengo terminado y ahora toca escribir “Cómo se hizo”, aunque si hay cambios en el código con respecto de lo que hay publicado.

En lo primeros pasos vamos a definir la nave, la vamos a mover, luego vamos a abordar los enemigos y el disparo. Una vez realizado lo anterior, vamos a implementar la mecánica del juego, las colisiones, las puntuaciones, las vidas disponibles, etc.

En los últimos pasos vamos a implementar el menú de inicio, la selección de controles, inicio y fin de partida, marcadores, los efectos de sonido y, en general, a decorar un poco el resultado final.

Lo último será añadir una pantalla de carga, a ver que tal nos sale esta vez.

Entorno de trabajo

Esta vez no voy a trabajar bajo Windows, voy a usar el reciente regalo que me han hecho, lo que algunos llaman el Spectrum del siglo XXI, una Raspberry Pi 400.

El emulador que voy a usar es ZEsarUX, el compilador PASMO y el editor Visual Studio Code, por lo que en este aspecto no cambio nada con respecto a PorompomPong.

En esta ocasión voy a mostrar algo que no mostré en el tutorial anterior, la forma de depurar con ZEsarUX, cosa que espero que os sea de utilidad, aunque lo dejaremos para el final.

El resultado final

Batalla espacial es un sencillo juego mata marcianos, compatible con los modelos de ZX Spectrum 16K, 48K, +2 y +3, manejable con teclado, joystick Sinclair y Kempstom, y que consta de treinta niveles.

Decir que consta de treinta niveles quizá sea algo pretencioso, ya que la mecánica del juego no cambia, aunque lo que sí cambia son los enemigos, habiendo un total de treinta distintos, uno por cada nivel.

El movimiento de nuestra nave es horizontal, por lo que solo es necesario un gráfico, mientras que el movimiento de los enemigos es diagonal, siendo necesarios cuatro gráficos (arriba-derecha, arriba-izquierda, abajo-derecha, abajo-izquierda). El disparo de nuestra nave consta también de un solo gráfico, mientras que la explosión de la nave, cuando nos matan, consta de cuatro gráficos, con lo que haremos una pequeña animación cuando nos maten.

Vamos a definir otros ocho gráficos para el marco de la pantalla, y un gráfico más en blanco para borrar gráficos impresos en pantalla.

Conclusión

Poco más se puede añadir, espero que este tutorial os sirva para aprender y sobre todo que os divierta.

Gran parte de lo que vamos a ver en Batalla espacial lo expliqué en PorompomPong, motivo por el cual en ocasiones no pararé a explicar muchas instrucciones.

En la próxima entrega empezamos definiendo los gráficos y practicando la conversión hexadecimal/binario.

Enlaces de interés

Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x0C Ensamblador ZX Spectrum Pong – Pantalla de carga [Espamatica] [Leer]


Esta entrega de Ensamblador ZX Spectrum Pong también es nueva con respecto a la publicación original, y el objetivo es dotar a nuestro programa de una pantalla de carga; quizá no sea la mejor manera de hacerlo, pero es relativamente sencillo y cumple con su función.

Ensamblador ZX Spectrum Pong – Pantalla de carga

En esta ocasión vamos a cambiar de emulador, vamos a usar Retro Virtual Machine. ¿Por qué este cambio? Vamos a realizar un cargador BASIC personalizado para añadir nuestra pantalla de carga, y Retro Virtual Machine me parece que se acerca visualmente más a lo que usábamos de pequeños, Computone incluido.

Como hasta ahora, creamos una carpeta que se llame Paso12 y copiamos todos los ficheros .asm desde la carpeta Paso11. En esta ocasión, apenas vamos a modificar el código de nuestro programa, pero si que lo vamos a compilar de distinta manera a como lo hemos hecho hasta ahora.

Antes de nada, debéis descargar y descomprimir la pantalla de carga que he preparado o hacer una vosotros. No soy grafista, así que no esperéis gran cosa, aunque cumple con el cometido de esta entrega.

Implementamos nuestro cargador

Para crear el cargador no vamos a partir desde cero, vamos a modificar el cargador que crea PASMO con la opción –tapbas.

Cargamos nuestro programa en Retro Virtual Machine, pero en lugar de cargarlo con LOAD»», lo vamos a cargar con MERGE»». Para poder escribir MERGE, si estáis trabajando en un PC, tenéis que pulsar las teclas Control y Mayúsculas para pasar a modo extendido, y luego Control + T para escribir MERGE. Después de MERGE hay que poner «», esto lo logramos pulsando la tecla P con la tecla Control pulsada. Un vez que tenemos escrito MERGE»», pulsamos Enter y ya podemos cargar el cargador que nos crea PASMO.

Cuando carga el primer bloque, paramos el reproductor y pulsamos Enter para quitar el mensaje 0 OK, 0:1 y ver el código del cargador.

Ensamblador ZX Spectrum - Cargador PASMOEnsamblador ZX Spectrum, cargador generado por PASMO

Lo primero que vamos a hacer el editar la primera línea, para lo cual pulsamos el cursor arriba y una vez seleccionada, Mayúsculas + 1 para editar.

Ensamblador ZX Spectrum Pong - Editamos la primera lnea del cargadorEnsamblador ZX Spectrum Pong, editamos la primera línea del cargador

Vamos a cambiar la dirección donde se inicia el programa ya que, al meter más BASIC, es necesario cargar el programa en una posición de memoria más alta. Vamos a cambiar el CLEAR 23980, por CLEAR 24059. Seguidamente vamos a cambiar la línea 40 para poner la dirección de memoria donde cargar el programa, de manera que RANDOMIZE USR 23981 la dejamos como RANDOMIZE USR 24060.

Ahora vamos a ampliar la línea 20. Editamos la línea, nos vamos al final y agregamos dos puntos, pulsando Control + Z. Seguido a los dos puntos, vamos a añadir POKE 23624, 0, que se obtiene con Mayúsculas + O y la coma con la coma. Con este POKE ponemos tinta y fondo negro en la línea de comandos del ZX Spectrum, también ponemos en negro el borde; la dirección 23624 es donde esta la variable de sistema que en la entrega 0x0A llamamos BORDCR.

Seguimos en la línea 20 y vamos a poner otro POKE (no olvidéis los dos puntos) para poner un valor en la variable de sistema donde se ponen los atributos permanentes de la pantalla, POKE 23693, 0, para poner la tinta en negro y el fondo en negro en toda la pantalla.

Por último, ponemos otros dos puntos y CLS, que obtenemos pulsando la V.

Ensamblador ZX Spectrum, lneas 10, 20 y 40 modificadasEnsamblador ZX Spectrum, líneas 10, 20 y 40 modificadas

Vamos a modificar la línea 30, y antes de LOAD «»CODE, vamos a añadir LOAD «»SCREEN$: (LOAD se obtiene pulsando la J y SCREEN$ pulsando Control + K en modo extendido).

Después de SCREEN$, vamos a añadir otro POKE para que no se muestren en pantalla el resto de bloques que se van a cargar, y de esta forma que no borren parte de nuestra pantalla de carga. Este POKE en concreto lo he tomado prestado de uno de los vídeos de AsteroideZX; POKE 23739, 111.

Es el momento de grabar los cambios en una cinta, en este caso concreto la vamos a llamar PongCargador.tap, ya que vamos a crear otras dos cintas: una para la pantalla de carga y otra para el programa.

Para grabar ponemos SAVE «PoromPong» LINE 10. SAVE se obtiene en la S y LINE se obtiene pulsando Control + 3 en modo extendido. Con esto grabamos el programa en cinta y después, al cargarlo con LOAD «», se ejecuta desde la línea 10.

Ensamblador ZX Spectrum, cargador finalizadoEnsamblador ZX Spectrum, cargador finalizado

Reiniciamos el emulador y probamos lo grabado, veréis que se va a auto ejecutar y se queda esperando a que se siga cargando el resto del programa. Pulsamos Escape para parar la ejecución y pulsamos B, luego 7 y Enter, para poner el borde en blanco. Luego en modo extendido Control + X, luego 7 y Enter, y ya tenemos la tinta de la pantalla principal en blanco.

Añadimos la pantalla de carga

La pantalla de carga la he diseñado con ZX PaintBrush y la he exportado como PongPantalla.tap; me ha pedido que ponga un nombre al bloque y he puesto PongScr.

Ensamblador ZX Spectrum - Exportación de pantalla a .tapEnsamblador ZX Spectrum, exportación de pantalla a .tap

Es muy importante exportar como tap y no grabar como tap. Si grabamos como tap no nos pide el nombre de la cabecera y luego no cargaría.

Ahora vamos a concatenar el cargador y la pantalla a través de Copy, con el símbolo de sistema (en mi caso que es Windows); no olvidéis situaros en el directorio de trabajo.

copy /b PongCargador.tap+PongPantalla.tap Pong.tap

Hemos concatenado los archivos PongCargador.tap y PongPantalla.tap y el resultado lo hemos escrito en Pong.tap.

El siguiente paso es cargar en el emulador el archivo Pong.tap para ver si carga nuestra pantalla.

Ensamblador ZX Spectrum - Pantalla de cargaEnsamblador ZX Spectrum, pantalla de carga
Incluimos nuestro PorompomPong

Ahora solo queda compilar nuestro PorompomPong con PASMO, sin que nos cree el cargador BASIC.

Antes de nada abrimos el archivo Main.asm y cambiamos en la primera línea el ORG $5dad por ORG $5dfc (24060), que es la dirección donde se debe cargar nuestro programa según hemos indicado en el cargador.

Es el momento de compilar con PASMO pero cambiando el –tapbas que veníamos usando hasta ahora por –tap.

pasmo --name PoromPong --tap Main.asm PongBytes.tap

Con esto compilamos pero PASMO no nos crea un cargador BASIC.

Por último vamos a concatenar los tres archivos en uno solo y comprobar si nuestro PorompomPong sigue funcionado.

copy /b PongCargador.tap+PongPantalla.tap+PongBytes.tap Pong.tap

Si todo ha ido bien, cargamos en el emulador y después de la pantalla de carga, se cargará nuestro programa y todo listo para jugar.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, pantalla de carga

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x0C Ensamblador ZX Spectrum Pong – Pantalla de carga [Espamatica] [Leer]


Esta entrega de Ensamblador ZX Spectrum Pong también es nueva con respecto a la publicación original, y el objetivo es dotar a nuestro programa de una pantalla de carga; quizá no sea la mejor manera de hacerlo, pero es relativamente sencillo y cumple con su función.

Ensamblador ZX Spectrum Pong – Pantalla de carga

En esta ocasión vamos a cambiar de emulador, vamos a usar Retro Virtual Machine. ¿Por qué este cambio? Vamos a realizar un cargador BASIC personalizado para añadir nuestra pantalla de carga, y Retro Virtual Machine me parece que se acerca visualmente más a lo que usábamos de pequeños, Computone incluido.

Como hasta ahora, creamos una carpeta que se llame Paso12 y copiamos todos los ficheros .asm desde la carpeta Paso11. En esta ocasión, apenas vamos a modificar el código de nuestro programa, pero si que lo vamos a compilar de distinta manera a como lo hemos hecho hasta ahora.

Antes de nada, debéis descargar y descomprimir la pantalla de carga que he preparado o hacer una vosotros. No soy grafista, así que no esperéis gran cosa, aunque cumple con el cometido de esta entrega.

Implementamos nuestro cargador

Para crear el cargador no vamos a partir desde cero, vamos a modificar el cargador que crea PASMO con la opción –tapbas.

Cargamos nuestro programa en Retro Virtual Machine, pero en lugar de cargarlo con LOAD»», lo vamos a cargar con MERGE»». Para poder escribir MERGE, si estáis trabajando en un PC, tenéis que pulsar las teclas Control y Mayúsculas para pasar a modo extendido, y luego Control + T para escribir MERGE. Después de MERGE hay que poner «», esto lo logramos pulsando la tecla P con la tecla Control pulsada. Un vez que tenemos escrito MERGE»», pulsamos Enter y ya podemos cargar el cargador que nos crea PASMO.

Cuando carga el primer bloque, paramos el reproductor y pulsamos Enter para quitar el mensaje 0 OK, 0:1 y ver el código del cargador.

Ensamblador ZX Spectrum - Cargador PASMOEnsamblador ZX Spectrum, cargador generado por PASMO

Lo primero que vamos a hacer el editar la primera línea, para lo cual pulsamos el cursor arriba y una vez seleccionada, Mayúsculas + 1 para editar.

Ensamblador ZX Spectrum Pong - Editamos la primera lnea del cargadorEnsamblador ZX Spectrum Pong, editamos la primera línea del cargador

Vamos a cambiar la dirección donde se inicia el programa ya que, al meter más BASIC, es necesario cargar el programa en una posición de memoria más alta. Vamos a cambiar el CLEAR 23980, por CLEAR 24059. Seguidamente vamos a cambiar la línea 40 para poner la dirección de memoria donde cargar el programa, de manera que RANDOMIZE USR 23981 la dejamos como RANDOMIZE USR 24060.

Ahora vamos a ampliar la línea 20. Editamos la línea, nos vamos al final y agregamos dos puntos, pulsando Control + Z. Seguido a los dos puntos, vamos a añadir POKE 23624, 0, que se obtiene con Mayúsculas + O y la coma con la coma. Con este POKE ponemos tinta y fondo negro en la línea de comandos del ZX Spectrum, también ponemos en negro el borde; la dirección 23624 es donde esta la variable de sistema que en la entrega 0x0A llamamos BORDCR.

Seguimos en la línea 20 y vamos a poner otro POKE (no olvidéis los dos puntos) para poner un valor en la variable de sistema donde se ponen los atributos permanentes de la pantalla, POKE 23693, 0, para poner la tinta en negro y el fondo en negro en toda la pantalla.

Por último, ponemos otros dos puntos y CLS, que obtenemos pulsando la V.

Ensamblador ZX Spectrum, lneas 10, 20 y 40 modificadasEnsamblador ZX Spectrum, líneas 10, 20 y 40 modificadas

Vamos a modificar la línea 30, y antes de LOAD «»CODE, vamos a añadir LOAD «»SCREEN$: (LOAD se obtiene pulsando la J y SCREEN$ pulsando Control + K en modo extendido).

Después de SCREEN$, vamos a añadir otro POKE para que no se muestren en pantalla el resto de bloques que se van a cargar, y de esta forma que no borren parte de nuestra pantalla de carga. Este POKE en concreto lo he tomado prestado de uno de los vídeos de AsteroideZX; POKE 23739, 111.

Es el momento de grabar los cambios en una cinta, en este caso concreto la vamos a llamar PongCargador.tap, ya que vamos a crear otras dos cintas: una para la pantalla de carga y otra para el programa.

Para grabar ponemos SAVE «PoromPong» LINE 10. SAVE se obtiene en la S y LINE se obtiene pulsando Control + 3 en modo extendido. Con esto grabamos el programa en cinta y después, al cargarlo con LOAD «», se ejecuta desde la línea 10.

Ensamblador ZX Spectrum, cargador finalizadoEnsamblador ZX Spectrum, cargador finalizado

Reiniciamos el emulador y probamos lo grabado, veréis que se va a auto ejecutar y se queda esperando a que se siga cargando el resto del programa. Pulsamos Escape para parar la ejecución y pulsamos B, luego 7 y Enter, para poner el borde en blanco. Luego en modo extendido Control + X, luego 7 y Enter, y ya tenemos la tinta de la pantalla principal en blanco.

Añadimos la pantalla de carga

La pantalla de carga la he diseñado con ZX PaintBrush y la he exportado como PongPantalla.tap; me ha pedido que ponga un nombre al bloque y he puesto PongScr.

Ensamblador ZX Spectrum - Exportación de pantalla a .tapEnsamblador ZX Spectrum, exportación de pantalla a .tap

Es muy importante exportar como tap y no grabar como tap. Si grabamos como tap no nos pide el nombre de la cabecera y luego no cargaría.

Ahora vamos a concatenar el cargador y la pantalla a través de Copy, con el símbolo de sistema (en mi caso que es Windows); no olvidéis situaros en el directorio de trabajo.

copy /b PongCargador.tap+PongPantalla.tap Pong.tap

Hemos concatenado los archivos PongCargador.tap y PongPantalla.tap y el resultado lo hemos escrito en Pong.tap.

El siguiente paso es cargar en el emulador el archivo Pong.tap para ver si carga nuestra pantalla.

Ensamblador ZX Spectrum - Pantalla de cargaEnsamblador ZX Spectrum, pantalla de carga
Incluimos nuestro PorompomPong

Ahora solo queda compilar nuestro PorompomPong con PASMO, sin que nos cree el cargador BASIC.

Antes de nada abrimos el archivo Main.asm y cambiamos en la primera línea el ORG $5dad por ORG $5dfc (24060), que es la dirección donde se debe cargar nuestro programa según hemos indicado en el cargador.

Es el momento de compilar con PASMO pero cambiando el –tapbas que veníamos usando hasta ahora por –tap.

pasmo --name PoromPong --tap Main.asm PongBytes.tap

Con esto compilamos pero PASMO no nos crea un cargador BASIC.

Por último vamos a concatenar los tres archivos en uno solo y comprobar si nuestro PorompomPong sigue funcionado.

copy /b PongCargador.tap+PongPantalla.tap+PongBytes.tap Pong.tap

Si todo ha ido bien, cargamos en el emulador y después de la pantalla de carga, se cargará nuestro programa y todo listo para jugar.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, pantalla de carga

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x0B Ensamblador ZX Spectrum Pong – Optimización parte 2 [Espamatica] [Leer]


Esta entrega de Ensamblador ZX Spectrum Pong surge a raíz de una serie de comentarios realizados por Spirax en el grupo de Ensamblador Z80 de Telegram, una vez terminadas las sesiones que se realizaron en colaboración con Retro Parla, y cuyos vídeos he ido añadiendo al final de los artículos. Estamos ante una entrega completamente nueva con respecto de la publicación original.

Ensamblador ZX Spectrum Pong – Optimización parte 2

Vamos a realizar dos nuevas optimizaciones, que van a permitir reducir el número de bytes y ciclos de reloj de nuestro Pong.

Como viene siendo costumbre, creamos una carpeta que se llame Paso11 y copiamos todos los ficheros .asm desde la carpeta Paso10, y nos ponemos manos a la obra.

Optimización de PlaySound

La primera optimización la vamos a realizar en la rutina de sonido, siguiendo los comentarios de Spirax, en concreto en la manera en la que evaluamos el sonido que tenemos que emitir.

Abrimos el archivo Sound.asm y localizamos la etiqueta PlaySound, cuyas primeras líneas son.

PlaySound: ; Preserva el valor de los registros push de push hl cp $01 ; Evalúa si se emite el sonido de Punto jr z, playSound_point ; Si el resultado es 0, el valor de A era 1 y emite el ; sonido del punto cp $02 ; Evalúa si se emite el sonido de Pala jr z, playSound_paddle ; Si el resultado es 0, el valor de A era 2 y emite el ; sonido de choque con la pala

En esta rutina utilizamos CP $01 y CP $02 para comprobar que sonido hay que emitir. Cada instrucción CP ocupa 2 bytes y tarda 7 ciclos de reloj; vamos a sustituir estas instrucciones por DEC A, que ocupa 1 byte y tarda 4 ciclos de reloj, por lo que nos vamos a ahorrar 2 bytes y 6 ciclos de reloj. Al contrario de CP, DEC si altera el valor del registro A, pero dado que lo que tenemos en A es el tipo de sonido a emitir, no nos afecta.

Veamos como queda el inicio de la rutina.

PlaySound: ; Preserva el valor de los registros push de push hl ; Spirax dec a ; Evalúa si se emite el sonido de Punto jr z, playSound_point ; Si el resultado es 0, el valor de A era 1 y emite el ; sonido del punto ; Spirax dec a ; Evalúa si se emite el sonido de Pala jr z, playSound_paddle ; Si el resultado es 0, el valor de A era 2 y emite el ; sonido de choque con la pala

Primero preserva el valor de DE, PUSH DE, luego el de HL, PUSH HL, y a continuación decrementa A, DEC A. Si A era 1, el resultado de la operación es 0 y salta a emitir el sonido, JR Z, PlaySound_point.

Si A no era uno, seguimos con las comprobaciones; decrementa A, DEC A, y si el resultado de la operación es 0 salta a emitir el sonido, JR Z, PlaySound_paddle. Si salta a reproducir el sonido es porque inicialmente A valía 2, con el primer decremento vale 1 y con este segundo decremento vale 0.

Si no ha saltado, la rutina sigue tal y como estaba y emite el sonido del punto.

Este es el momento de compilar, cargar en el emulador y comprobar que todo sigue funcionando.

Optimización de ReprintPoints

Esta rutina la implementamos de nuevo en la entrega anterior, la optimizamos y ahora volvemos a optimizarla.

Con esta optimización vamos a ahorrar 20 bytes y 107 ciclos de reloj. Para lograr este ahorro vamos a aplicar el mismo método que aplicamos, en la entrega anterior, siguiendo los comentarios de Spirax; vamos a seguir por el camino que nos marcó.

Como recordaréis, pusimos una etiqueta para que el pintado del marcador del jugador 2 se pudiera llamar de manera independiente; vamos a hacer los mismo con el marcador del jugador 1. Con esta modificación, los marcadores van a tardar algo más en pintarse (solo se pintan al inicio de la partida y al marcar un punto), pero vamos a simplificar la rutina ReprintPoints, ahorrando bytes y ciclos de reloj, y eliminando código redundante.

Vamos a empezar modificando la rutina PrintPoints para que se pueda llamar de manera independiente al pintado de los marcadores de ambos jugadores.

Abrimos el archivo Video.asm y localizamos la etiqueta PrintPoints; justo debajo de ella agregamos otra etiqueta; es la que vamos a llamar para pintar el marcador del jugador 1.

printPoint_1_print:

Entre las etiquetas PrintPoints y printPoint_1_print vamos a añadir las llamadas a pintar el marcador de cada jugador.

call printPoint_1_print ; Pinta el marcador del jugador 1 jr printPoint_2_print ; Pinta el marcador del jugador 2

Lo primero que hacemos es llamar a pintar el marcador del jugador 1, CALL printPoint_1_print, y luego saltar a pintar el marcador del jugador 2, JR printPoint_1_print.

Ya solo queda un cambio en PrintPoints, hay que añadir RET justo antes de la etiqueta printPoint_2_print, para que CALL printPoint_1_print salga correctamente; recordad que el resto de saltos salen por el RET de PrintPoint.

Añadimos RET antes de printPoint_2_print.

ret printPoint_2_print:

Con esto hemos acabado con las modificaciones necesarias en PrintPoints, y como vemos no hemos ahorrado nada, al contrario, hemos añadido código, añadiendo bytes y ciclos de reloj.

Vamos ahora con el ahorro, para ello localizamos la etiqueta reprintPoint_1_print y la borramos. También borramos las líneas que la siguen hasta llegar a la etiqueta reprintPoint_2, está ultima etiqueta no la borramos.

Localizamos la etiqueta ReprintPoints y nueve líneas más abajo encontramos la instrucción JR Z, reprintPoint_1_print. Dado que esta etiqueta ya no existe, hay que cambiar esta línea y dejarla como sigue.

jr z, printPoint_1_print

Ahora Localizamos la etiqueta reprintPoint_1 y vamos a terminar con las modificaciones. El código actual de esta etiqueta, una vez borrada toda la parte de reprintPoint_1_print es el siguiente.

reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr c, reprintPoint_1_print ; Si hay acarreo, pasa por el marcador 1 ; y salta para pintar jr nz, reprintPoint_2 ; Si no es cero, pasa por la derecha ; y salta para comprobar paso por marcador 2

Tenemos que cambiar la doble comprobación; dado que la etiqueta reprintPoint_2 esta ahora justo debajo de la línea JR NZ, reprintPoint_2, ese salto ya no es necesario, pero si que es necesario comprobar si es cero, en cuyo caso hay que pintar el marcador del jugador 1, JR Z, printPoint_1_print, y cambiar el salto de JR C, reprintPoint_1_print por JR C, printPoint_1_print, por lo tanto, el código quedaría así.

reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr z, printPoint_1_print jr c, printPoint_1_print ; Si es 0 o hay acarreo, pasa por el marcador 1 ; y salta para pintar

El aspecto final de las rutinas PrintPoints y ReprintPoints es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- PrintPoints: call printPoint_1_print ; Pinta el marcador del jugador 1 jr printPoint_2_print ; Pinta el marcador del jugador 2 printPoint_1_print: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call PrintPoint ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P1 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 1 call PrintPoint ; Pinta el segundo dígito del marcador del jugador 1 ret printPoint_2_print: ; 1er dígito del jugador 2 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call PrintPoint ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P2 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 2 ; Pinta el segundo dígito del marcador del jugador 2 PrintPoint: ld b, $10 ; Cada dígito son 1 byte por 16 (scanlines) printPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz printPoint_printLoop ; Hasta que B = 0 ret ; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene tercio, línea y scanline de la posición ; de la bola cp POINTS_Y_B ; Lo compara con el límite inferior del marcador ret nc ; Si no hay acarreo, pasa por debajo y sale ld a, l ; Carga en A la línea y columna de la posición de la bola and $1f ; Se queda con la columna cp POINTS_X1_L ; Lo compara con el límite izquierdo del marcador 1 ret c ; Si hay acarreo, pasa por la izquierda y sale jr z, printPoint_1_print ; Si es 0, está justo en el margen izquierdo ; y salta para pintar cp POINTS_X2_R ; Lo compara con el límite derecho de marcador 2 jr z, printPoint_2_print ; Si es 0, está justo en el margen derecho ; y salta para pintar ret nc ; Si no hay acarreo, pasa por la derecha y sale reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr z, printPoint_1_print jr c, printPoint_1_print ; Si es 0 o hay acarreo, pasa por el marcador 1 ; y salta para pintar reprintPoint_2: cp POINTS_X2_L ; Lo compara con el límite derecho de marcador 2 ret c ; Si hay acarreo, pasa por la izquierda y sale ; Spirax jr printPoint_2_print ; Pinta el marcador del jugador 2

Si comparamos la implementación de ReprintPoints con la implementación que hicimos de esta rutina en el capítulo anterior, podemos observar que la rutina se ha simplificado significativamente, quedando prácticamente reducida a las comprobaciones que incluimos para que el marcador solo se repintase cuando es necesario.

Ahora ya solo queda compilar, cargar en el emulador y comprobar que todo sigue funcionando.

Hemos terminado. ¿O queréis añadir una pantalla de carga?

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, optimización parte 2

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x0B Ensamblador ZX Spectrum Pong – Optimización parte 2 [Espamatica] [Leer]


Esta entrega de Ensamblador ZX Spectrum Pong surge a raíz de una serie de comentarios realizados por Spirax en el grupo de Ensamblador Z80 de Telegram, una vez terminadas las sesiones que se realizaron en colaboración con Retro Parla, y cuyos vídeos he ido añadiendo al final de los artículos. Estamos ante una entrega completamente nueva con respecto de la publicación original.

Ensamblador ZX Spectrum Pong – Optimización parte 2

Vamos a realizar dos nuevas optimizaciones, que van a permitir reducir el número de bytes y ciclos de reloj de nuestro Pong.

Como viene siendo costumbre, creamos una carpeta que se llame Paso11 y copiamos todos los ficheros .asm desde la carpeta Paso10, y nos ponemos manos a la obra.

Optimización de PlaySound

La primera optimización la vamos a realizar en la rutina de sonido, siguiendo los comentarios de Spirax, en concreto en la manera en la que evaluamos el sonido que tenemos que emitir.

Abrimos el archivo Sound.asm y localizamos la etiqueta PlaySound, cuyas primeras líneas son.

PlaySound: ; Preserva el valor de los registros push de push hl cp $01 ; Evalúa si se emite el sonido de Punto jr z, playSound_point ; Si el resultado es 0, el valor de A era 1 y emite el ; sonido del punto cp $02 ; Evalúa si se emite el sonido de Pala jr z, playSound_paddle ; Si el resultado es 0, el valor de A era 2 y emite el ; sonido de choque con la pala

En esta rutina utilizamos CP $01 y CP $02 para comprobar que sonido hay que emitir. Cada instrucción CP ocupa 2 bytes y tarda 7 ciclos de reloj; vamos a sustituir estas instrucciones por DEC A, que ocupa 1 byte y tarda 4 ciclos de reloj, por lo que nos vamos a ahorrar 2 bytes y 6 ciclos de reloj. Al contrario de CP, DEC si altera el valor del registro A, pero dado que lo que tenemos en A es el tipo de sonido a emitir, no nos afecta.

Veamos como queda el inicio de la rutina.

PlaySound: ; Preserva el valor de los registros push de push hl ; Spirax dec a ; Evalúa si se emite el sonido de Punto jr z, playSound_point ; Si el resultado es 0, el valor de A era 1 y emite el ; sonido del punto ; Spirax dec a ; Evalúa si se emite el sonido de Pala jr z, playSound_paddle ; Si el resultado es 0, el valor de A era 2 y emite el ; sonido de choque con la pala

Primero preserva el valor de DE, PUSH DE, luego el de HL, PUSH HL, y a continuación decrementa A, DEC A. Si A era 1, el resultado de la operación es 0 y salta a emitir el sonido, JR Z, PlaySound_point.

Si A no era uno, seguimos con las comprobaciones; decrementa A, DEC A, y si el resultado de la operación es 0 salta a emitir el sonido, JR Z, PlaySound_paddle. Si salta a reproducir el sonido es porque inicialmente A valía 2, con el primer decremento vale 1 y con este segundo decremento vale 0.

Si no ha saltado, la rutina sigue tal y como estaba y emite el sonido del punto.

Este es el momento de compilar, cargar en el emulador y comprobar que todo sigue funcionando.

Optimización de ReprintPoints

Esta rutina la implementamos de nuevo en la entrega anterior, la optimizamos y ahora volvemos a optimizarla.

Con esta optimización vamos a ahorrar 20 bytes y 107 ciclos de reloj. Para lograr este ahorro vamos a aplicar el mismo método que aplicamos, en la entrega anterior, siguiendo los comentarios de Spirax; vamos a seguir por el camino que nos marcó.

Como recordaréis, pusimos una etiqueta para que el pintado del marcador del jugador 2 se pudiera llamar de manera independiente; vamos a hacer los mismo con el marcador del jugador 1. Con esta modificación, los marcadores van a tardar algo más en pintarse (solo se pintan al inicio de la partida y al marcar un punto), pero vamos a simplificar la rutina ReprintPoints, ahorrando bytes y ciclos de reloj, y eliminando código redundante.

Vamos a empezar modificando la rutina PrintPoints para que se pueda llamar de manera independiente al pintado de los marcadores de ambos jugadores.

Abrimos el archivo Video.asm y localizamos la etiqueta PrintPoints; justo debajo de ella agregamos otra etiqueta; es la que vamos a llamar para pintar el marcador del jugador 1.

printPoint_1_print:

Entre las etiquetas PrintPoints y printPoint_1_print vamos a añadir las llamadas a pintar el marcador de cada jugador.

call printPoint_1_print ; Pinta el marcador del jugador 1 jr printPoint_2_print ; Pinta el marcador del jugador 2

Lo primero que hacemos es llamar a pintar el marcador del jugador 1, CALL printPoint_1_print, y luego saltar a pintar el marcador del jugador 2, JR printPoint_1_print.

Ya solo queda un cambio en PrintPoints, hay que añadir RET justo antes de la etiqueta printPoint_2_print, para que CALL printPoint_1_print salga correctamente; recordad que el resto de saltos salen por el RET de PrintPoint.

Añadimos RET antes de printPoint_2_print.

ret printPoint_2_print:

Con esto hemos acabado con las modificaciones necesarias en PrintPoints, y como vemos no hemos ahorrado nada, al contrario, hemos añadido código, añadiendo bytes y ciclos de reloj.

Vamos ahora con el ahorro, para ello localizamos la etiqueta reprintPoint_1_print y la borramos. También borramos las líneas que la siguen hasta llegar a la etiqueta reprintPoint_2, está ultima etiqueta no la borramos.

Localizamos la etiqueta ReprintPoints y nueve líneas más abajo encontramos la instrucción JR Z, reprintPoint_1_print. Dado que esta etiqueta ya no existe, hay que cambiar esta línea y dejarla como sigue.

jr z, printPoint_1_print

Ahora Localizamos la etiqueta reprintPoint_1 y vamos a terminar con las modificaciones. El código actual de esta etiqueta, una vez borrada toda la parte de reprintPoint_1_print es el siguiente.

reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr c, reprintPoint_1_print ; Si hay acarreo, pasa por el marcador 1 ; y salta para pintar jr nz, reprintPoint_2 ; Si no es cero, pasa por la derecha ; y salta para comprobar paso por marcador 2

Tenemos que cambiar la doble comprobación; dado que la etiqueta reprintPoint_2 esta ahora justo debajo de la línea JR NZ, reprintPoint_2, ese salto ya no es necesario, pero si que es necesario comprobar si es cero, en cuyo caso hay que pintar el marcador del jugador 1, JR Z, printPoint_1_print, y cambiar el salto de JR C, reprintPoint_1_print por JR C, printPoint_1_print, por lo tanto, el código quedaría así.

reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr z, printPoint_1_print jr c, printPoint_1_print ; Si es 0 o hay acarreo, pasa por el marcador 1 ; y salta para pintar

El aspecto final de las rutinas PrintPoints y ReprintPoints es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- PrintPoints: call printPoint_1_print ; Pinta el marcador del jugador 1 jr printPoint_2_print ; Pinta el marcador del jugador 2 printPoint_1_print: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call PrintPoint ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P1 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 1 call PrintPoint ; Pinta el segundo dígito del marcador del jugador 1 ret printPoint_2_print: ; 1er dígito del jugador 2 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call PrintPoint ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P2 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 2 ; Pinta el segundo dígito del marcador del jugador 2 PrintPoint: ld b, $10 ; Cada dígito son 1 byte por 16 (scanlines) printPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz printPoint_printLoop ; Hasta que B = 0 ret ; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene tercio, línea y scanline de la posición ; de la bola cp POINTS_Y_B ; Lo compara con el límite inferior del marcador ret nc ; Si no hay acarreo, pasa por debajo y sale ld a, l ; Carga en A la línea y columna de la posición de la bola and $1f ; Se queda con la columna cp POINTS_X1_L ; Lo compara con el límite izquierdo del marcador 1 ret c ; Si hay acarreo, pasa por la izquierda y sale jr z, printPoint_1_print ; Si es 0, está justo en el margen izquierdo ; y salta para pintar cp POINTS_X2_R ; Lo compara con el límite derecho de marcador 2 jr z, printPoint_2_print ; Si es 0, está justo en el margen derecho ; y salta para pintar ret nc ; Si no hay acarreo, pasa por la derecha y sale reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr z, printPoint_1_print jr c, printPoint_1_print ; Si es 0 o hay acarreo, pasa por el marcador 1 ; y salta para pintar reprintPoint_2: cp POINTS_X2_L ; Lo compara con el límite derecho de marcador 2 ret c ; Si hay acarreo, pasa por la izquierda y sale ; Spirax jr printPoint_2_print ; Pinta el marcador del jugador 2

Si comparamos la implementación de ReprintPoints con la implementación que hicimos de esta rutina en el capítulo anterior, podemos observar que la rutina se ha simplificado significativamente, quedando prácticamente reducida a las comprobaciones que incluimos para que el marcador solo se repintase cuando es necesario.

Ahora ya solo queda compilar, cargar en el emulador y comprobar que todo sigue funcionando.

Hemos terminado. ¿O queréis añadir una pantalla de carga?

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, optimización parte 2

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x0A Ensamblador ZX Spectrum Pong – Sonido y optimización [Espamatica] [Leer]


Con esta entrega de Ensamblador ZX Spectrum Pong, vamos a dar por concluida la segunda parte del tutorial dedicándola al sonido y la optimización, y vamos a continuar justo donde lo dejamos en la entrega anterior.

Como es costumbre, creamos la carpeta Paso10 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso09.

Ensamblador ZX Spectrum – Sonido y optimización

Al final de la entrega anterior comentábamos que había algo que no funcionaba del todo bien cuando la bola golpeaba en el último scanline de la pala. También nos preguntábamos al final de la entrega anterior si la bola no iba lenta; sí, la bola va algo lenta. Esto es debido, en gran parte, a que el marcador se repinta en cada iteración del bucle principal, lo cual no es necesario. Vamos a empezar cambiando el repintado del marcador.

Optimización de repintado de marcador

El marcador solo se debería repintar cuando es borrado por la bola. Modificando este aspecto, vamos a ganar velocidad en la bola, ya que el tiempo de proceso en cada iteración del bucle principal se va a reducir.

Lo primero es localizar el área de la pantalla dónde la bola borra el marcador, definimos una serie de constantes en el archivo Sprite.asm, justo debajo de la constante POINTS_P2.

POINTS_X1_L: EQU $0c POINTS_X1_R: EQU $0f POINTS_X2_L: EQU $10 POINTS_X2_R: EQU $13 POINTS_Y_B: EQU $14

El significado de estas constantes, en orden de aparición, es:

  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la derecha.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la derecha.
  • Tercio, línea y scanline en la que la bola empieza a borrar el marcador por la parte de abajo.

Una vez que hemos definido estas constantes, vamos a modificar las rutinas PrintPoints y ReprintPoints del archivo Video.asm, empezando por localizar la etiqueta printPoint_print, que vamos a sustituir por PrintPoint. Dentro de la rutina PrintPoints, hay tres llamadas a printPoint_print, que vamos a sustituir por PrintPoint.

Compilamos, cargamos en el emulador y comprobamos que no hemos roto nada.

El siguiente paso es modificar la rutina ReprintPoints. En realidad, no la vamos a modificar, la vamos a borrar y a volver a implementar.

ReprintPoints: ld hl, (ballPos) call GetPtrY cp POINTS_Y_B ret nc

Cargamos la posición de la bola en HL, LD HL, (ballPos), obtenemos tercio, línea y scanline de la posición de la bola, CALL GetPtrY, y lo comparamos con la posición donde la bola empieza a borrar el marcador desde abajo, CP POINTS_Y_B. Si no hay acarreo, la bola pasa por debajo del marcador y sale, RET NC.

Si hay acarreo, según la coordenada Y de la bola, ésta podría borrar el marcador.

ld a, l and $1f cp POINTS_X1_L ret c jr z, reprintPoint_1_print

Cargamos la línea y columna de la posición de la bola en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con la coordenada X en la que se empieza a borrar el marcador del jugador 1 por la izquierda, CP POINTS_X1_L. Si hay acarreo, la bola pasa por la izquierda del marcador y sale, RET C. Si las dos coordenadas coinciden, la bola va a borrar el marcador del jugador 1, y salta para repintarlo, JR Z, reprintPoint_1_print.

Si no hemos salido, ni saltado, seguimos con las comprobaciones.

cp POINTS_X2_R jr z, reprintPoint_2_print ret nc

Comparamos la coordenada X donde está la bola con la coordenada donde se empieza a borrar el marcador del jugador 2 por la derecha, CP POINT_X2_R. Si son iguales, salta a repintar el marcador del jugador 2, JR Z, reprintPoint_2_print. Si no salta y no hay acarreo, la bola pasa por la derecha y sale, RET NC.

Si no hemos saltado, ni hemos salido, seguimos con las comprobaciones.

reprintPoint_1: cp POINTS_X1_R jr c, reprintPoint_1_print jr nz, reprintPoint_2

Comparamos la coordenada X de la bola con la coordenada donde la bola empieza a borrar el marcador del jugador 1 por la derecha, CP POINTS_X1_R. Si hay acarreo, está borrando el marcador del jugador 1 y salta para repintarlo, JR C, reprintPoint_1_print. Si no son la misma coordenada, pasa por la derecha del marcador del jugador 1 y salta para comprobar si borra el marcador del jugador 2, JR NZ, reprintPoint_2.

Si está borrando el marcador del jugador 1, lo repinta.

reprintPoint_1_print: ld a, (p1points) call GetPointSprite push hl

Cargamos los puntos del jugador 1 en A, LD A, (p1points), obtenemos la dirección del sprite a pintar, CALL GetPointSprite, y preservamos el valor, PUSH HL.

Empezamos pintando el primer dígito, las decenas.

ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P1 call PrintPoint pop hl

Cargamos en E la parte baja de la dirección de memoria del sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, cargamos la parte alta de la dirección en D, LD D, (HL), cargamos en HL la dirección de memoria donde se pinta el marcador del jugador 1, LD HL, POINTS_P1, pintamos el primer dígito, CALL PrintPoint, y recuperamos el valor de HL, POP HL.

Terminamos pintando el segundo dígito.

inc hl inc hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P1 inc l jr PrintPoint

Apuntamos HL a la dirección de memoria del sprite del segundo dígito, INC HL INC HL, cargamos la parte baja de la dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, la cargamos en D, LD D, (HL), cargamos en HL la dirección de memoria dónde se pinta el marcador del jugador 1, LD HL, POINTS_P1, apuntamos HL a la dirección donde se pinta el segundo dígito, INC L, y pintamos el dígito y salimos, JR PrintPoint.

Posiblemente os estaréis preguntando, ¿cómo salimos? ¡Si no hay ningún RET! Estaréis pensando que en lugar de JR PrintPoint, tendríamos que haber puesto CALL PrintPoint. Y efectivamente esto funciona, pero no es necesario. Además, de la forma que lo hemos implementado, ahorramos tiempo de proceso y bytes.

La última instrucción de PrintPoint es un RET, y este es el RET que utilizamos para salir, por eso podemos poner JR en lugar de CALL y RET. Por eso, y porque no tenemos nada que tengamos que recuperar de la pila. Si hubiéramos dejado algo en la pila, los resultados serían impredecibles.

A continuación, podemos ver la diferencia de ciclos de reloj y bytes entre hacerlo de una manera o de otra.

InstrucciónCiclos de relojBytes
CALL PrintPoint173
RET101
JR PrintPoint122
Ciclos de reloj y bytes de las opciones estudiadas

Nos hemos ahorrado 15 ciclos de reloj y 2 bytes.

También hemos cambiado la forma de repintar. Antes repintábamos los marcadores haciendo OR con lo que hubiera pintado en esa zona, y ahora directamente pintamos el marcador. El resultado es que al pintar el marcador borramos la bola, lo que puede producir algún parpadeo. Como estos parpadeos también existen en el arcade original, lo dejamos así… o podéis cambiarlo.

Vamos ahora a ver cómo repintamos el marcador del jugador 2.

reprintPoint_2: cp POINTS_X2_L ret c

En este punto, solo hay que comprobar que la bola no esté pasando entre los marcadores sin borrarlos. Comparamos con el límite izquierdo del marcador del jugador 2, CP POINTS_X2_L, y si hay acarreo sale pues pasa por la izquierda, RET C.

Si no ha salido, hay que repintar el marcador del jugador 2, lo cual es casi idéntico a lo que hacemos con el marcador del jugador 1, por lo que se marcan las diferencias sin entrar en el detalle.

reprintPoint_2_print: ; ¡CAMBIO! ld a, (p2points) ; ¡CAMBIO! call GetPointSprite push hl ; 1er dígito ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; ¡CAMBIO! call PrintPoint pop hl ; 2º dígito inc hl inc hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; ¡CAMBIO! inc l jr PrintPoint

Siendo el aspecto final de la rutina el siguiente.

; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld hl, (ballPos) ; Carga la posición de la bola en HL call GetPtrY ; Obtiene tercio, línea y scanline de esta posición cp POINTS_Y_B ; Compara con la posición Y donde ; empieza a borrar el marcador ret nc ; Si no hay acarreo, paso por debajo y sale ; Si llega aquí la bola podría borrar el marcador, según su posición Y ld a, l ; Carga línea y columna de la posición ; de la bola en A and $1f ; Se queda con la columna cp POINTS_X1_L ; Compara con la posición donde la bola borrar el ; marcador del jugador 1 por la izquierda ret c ; Si hay acarreo pasa por la izquierda y sale jr z, reprintPoint_1_print ; Si coinciden, la bola va a borrar el marcador ; y repinta ; Sigue con las comprobaciones cp POINTS_X2_R ; Compara la coordenada X de la bola con la ; posición donde borra el marcador 2 por la derecha jr z, reprintPoint_2_print ; Si son iguales, repinta el marcador ret nc ; Si no hay acarreo, pasa por la derecha y sale ; Resto de comprobaciones para averiguar si borra el marcador 1 reprintPoint_1: cp POINTS_X1_R ; Compara la coordenada X de la bola con la ; posición donde borra el marcador 1 por la derecha jr c, reprintPoint_1_print ; Si hay acarreo, borra el marcador y repinta jr nz, reprintPoint_2 ; Si no es 0 para por la derecha del marcador 1 ; y salta ; Repinta el marcador del jugador 1 reprintPoint_1_print: ld a, (p1points) ; Carga en A la puntuación del jugador 1 call GetPointSprite ; Obtiene la dirección del sprite a pintar push hl ; Preserva el valor de HL ld e, (hl) ; Carga en E la parte baja de la dirección ; del sprite inc hl ; Apunta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P1 ; Carga en HL la dirección dónde se pinta el ; marcador 1 call PrintPoint ; Pinta el primer dígito pop hl ; Recupera el valor de HL inc hl inc hl ; Apunta HL al sprite del segundo dígito ld e, (hl) ; Carga la parte baja de la dirección en E inc hl ; A punta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P1 ; Carga en HL la dirección dónde se pinta el ; marcador 1 inc l ; Apunta a la dirección dónde se pinta el segundo ; dígito jr PrintPoint ; Pinta el dígito y sale ; Resto de comprobaciones para averiguar si borra el marcador 2 reprintPoint_2: cp POINTS_X2_L ; Compara la coordenada X de la bola con la ; posición donde borra el marcador 2 por la ; izquierda ret c ; Si hay acarreo, pasa por la izquierda y sale ; Repinta el marcador del jugador 2 reprintPoint_2_print: ld a, (p2points) ; Carga en A la puntuación del jugador 2 call GetPointSprite ; Obtiene la dirección del sprite a pintar push hl ; Preserva el valor de HL ld e, (hl) ; Carga en E la parte baja de la dirección del ; sprite inc hl ; Apunta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P2 ; Carga en HL la dirección dónde se pinta el ; marcador 2 call PrintPoint ; Pinta el primer dígito pop hl ; Recupera el valor de HL inc hl inc hl ; Apunta HL al sprite del segundo dígito ld e, (hl) ; Carga la parte baja de la dirección en E inc hl ; A punta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P2 ; Carga en HL la dirección dónde se pinta el ; marcador 2 inc l ; Apunta a la dirección dónde se pinta el segundo ; dígito jr PrintPoint ; Pinta el dígito y sale

Compilamos, cargamos en el emulador, y vemos el resultado.

Podemos ver que la bola ahora va más rápida, incluso cuándo tiene que ir lento. También, si nos fijamos cuando es el jugador 2 el que marca el tanto y la bola debe salir por la derecha, parte de la misma se ve durante un corto espacio de tiempo en la izquierda.

Si hacemos memoria, cuando marcamos un punto la pelota sale desde el campo del jugador que ha ganado el punto. Eso nos lleva a la conclusión de que el problema está en la rutina SetBallRight, y más concretamente, en la primera línea.

ld hl, $4d7f

Según esta línea, posicionamos la pelota en tercio el 1, scanline 5, línea 3, columna 31. Además, dos líneas más abajo, cambiamos la rotación de la bola, poniéndola a -1.

ld a, $ff
ld (ballRotation), a

Ahora, si buscamos el sprite correspondiente a esta rotación, vemos que es el siguiente.

db    $00, $78    ; +7/$07    00000000 01111000       -1/$ff

Por lo que en la columna 31 la pintamos en blanco, y en la 32 pintamos $78. Pero es que la columna 32 no existe; las columnas en total son 32, pero van de la 0 a la 31. Al pintar en la 32, estamos pintando en la columna 0.

Una vez visto esto, la solución es sencilla. Editamos la primera línea de la rutina SetBallRight, para posicionar la bola en la columna 30.

ld hl, $4d7e

Y ahora vamos a cambiar la velocidad de la bola para que no vaya tan rápida. La configuración de la bola la tenemos guardada en ballSetting, en el archivo Sprite.asm.

; Velocidad y dirección de la bola. ; bits 0 a 3: Movimientos de la bola para que cambie la posición Y. ; Valores f = semiplano, 2 = semi diagonal, 1 = diagonal ; bits 4 y 5: velocidad de la bola: 1 muy rápido, 2 rápido, 3 lento ; bit 6: dirección X: 0 derecha / 1 izquierda ; bit 7: dirección Y: 0 arriba / 1 abajo ballSetting: db $31 ; 0011 0001

Según vemos en los comentarios, la velocidad de la bola se configura en los bits 4 y 5. Sería tan sencillo como que la velocidad 2 sea muy rápido, la 3 rápido, y la… ¡Cáspita! En 2 bits solo podemos especificar valores del 0 a 3, y el resto de bits lo tenemos ocupados.

Vamos a “robar” un bit a la inclinación de la bola. Como resultado, podremos reducir la velocidad de la bola, y como contraprestación, cuando la bola vaya plana, va a ir un poco más inclinada.

; Velocidad y dirección de la bola. ; bits 0 a 2: Movimientos de la bola para que cambie la posición Y. ; Valores 7 = semiplano, 2 = semi diagonal, 1 = diagonal ; bits 3 y 5: velocidad de la bola: 2 muy rápido, 3 rápido, 4 lento ; bit 6: dirección X: 0 derecha / 1 izquierda ; bit 7: dirección Y: 0 arriba / 1 abajo ballSetting: db $21 ; 0010 0001

Y ahora hay tres rutinas que tenemos que cambiar:

  • CheckCrossY en Game.asm: en esta rutina asignamos inclinación y velocidad de la bola, dependiendo de en qué parte de la pala golpea.
  • MoveBallY en Game.asm: en esta rutina evaluamos si los movimientos acumulados de la bola han alcanzado los necesarios para cambiar la coordenada Y de la misma.
  • SetBallLeft y SetBallRight en Game.asm: en estas rutinas reiniciamos la configuración de la bola.
  • Loop en Main.asm: al inicio de esta rutina, verificamos si se ha llegado al número de iteraciones del bucle necesarias para mover la bola.

Empezamos por CheckCrossY en Game.asm. Localizamos la etiqueta checkCrossY_1_5, y después la línea OR $31.

or    $31   ; Hacia arriba, velocidad 3 e inclinación diagonal

Según la nueva definición, vamos a poner velocidad 4 e inclinación diagonal.

0010 0001

Los bits marcados en rojo especifican la velocidad, y los marcados en amarillo la inclinación. La línea OR $31 debe quedar de la siguiente manera.

or $21

Localizamos la etiqueta checkCrossY_2_5 y ponemos velocidad 3, inclinación semi diagonal.

0001 1010

Modificamos la línea.

or    $22   ; Hacia arriba, velocidad 2 e inclinación semi diagonal

Y la dejamos como.

or $1a

Localizamos la etiqueta checkCrossY_3_5 y ponemos velocidad 2, inclinación semi plana.

0001 0111

Modificamos la línea.

or           $1f         ; Hacia arriba/abajo, velocidad 1 e inclinación semi plana

Y la dejamos como.

or $17

Localizamos la etiqueta checkCrossY_4_5 y ponemos velocidad 3, inclinación semi diagonal.

1001 1010

Modificamos la línea.

or    $a2   ; Hacia abajo, velocidad 2 e inclinación semi diagonal

Y la dejamos como.

or $9a

Localizamos la etiqueta checkCrossY_5_5 y ponemos velocidad 4, inclinación diagonal.

1010 0001

Modificamos la línea.

or    $b1   ; Hacia abajo, velocidad 3 e inclinación diagonal

Y la dejamos como.

or $a1

Con esto hemos acabado con la parte más laboriosa de la modificación.

Localizamos la etiqueta MoveBallY, y modificamos la segunda línea.

and   $0f

Y la dejamos como.

and $07

Con $0f ahora obtendríamos la inclinación y el primer bit de la velocidad. Con $07 sólo obtenemos la inclinación.

Modificamos el reinicio de la configuración de la bola en las rutinas SetBallLeft y SetBallRight.

En SetBallLeft modificamos la línea.

or $31      ; Pone dirección X a derecha, velocidad 3, inclinación diagonal

Y la dejamos como.

or $21

En SetBallRight modificamos la línea.

or    $71   ; Pone dirección X a izquierda, velocidad 3, inclinación diagonal

Y la dejamos como.

or $61

Vamos a terminar modificando el código de la etiqueta Loop de Main.asm. A partir de la segunda línea, nos encontramos 4 instrucciones RRCA. Quitamos una, para rotar sólo 3 veces y dejar en los bits 0, 1 y 2, la velocidad de la bola.

; rrca ; ¡ELIMINAR! rrca rrca rrca

Como ahora tenemos 3 bits para la velocidad, en lugar de dos, modificamos la línea siguiente, que es.

and   $03

Y la dejamos como.

and $07

Compilamos, cargamos en el emulador, y comprobamos que la velocidad de la bola es ahora más llevadera, en detrimento de la inclinación.

Optimización de ScanKeys

Ahora es el momento de optimizar la rutina ScanKeys, tal y como anunciamos en la entrega 0x02.

En la rutina ScanKeys hay cuatro instrucciones BIT, dos BIT $00, A, y otras dos BIT $01, A. Con las instrucciones BIT comprobamos el estado de un BIT en concreto de un registro, sin alterar el valor de dicho registro; cada instrucción BIT ocupa 2 bytes y tarda 8 ciclos de reloj.

Vamos a sustituir las instrucciones BIT por AND, ahorrándonos un ciclo de reloj en cada una. Sustituimos las instrucciones BIT $00, A por AND $01, y las instrucciones BIT $01, A por AND $02. Con esta modificación vamos a ahorrar 4 ciclos de reloj, aunque vamos a alterar el valor del registro A, que en este caso no importa.

Optimización de Cls

En la entrega 0x03, comentamos que la rutina Cls se podía optimizar ahorrándonos 8 ciclos de reloj y 4 bytes.

Vamos a recordar cómo es la rutina actualmente.

; ----------------------------------------------------------------------------- ; Limpia la pantalla, tinta 7, fondo 0. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- Cls: ; Limpia los píxeles de la pantalla ld hl, $4000 ; Carga en HL el inicio de la VideoRAM ld (hl), $00 ; Limpia los píxeles de esa dirección ld de, $4001 ; Carga en DE la siguiente posición de la VideoRAM ld bc, $17ff ; 6143 repeticiones ldir ; Limpia todos los píxeles de la VideoRAM ; Pone la tinta en blanco y el fondo en negro ld hl, $5800 ; Carga en HL el inicio del área de atributos ld (hl), $07 ; Lo pone con la tinta en blanco y el fondo en negro ld de, $5801 ; Carga en DE la siguiente posición del área de atributos ld bc, $2ff ; 767 repeticiones ldir ; Asigna el valor a toda el área de atributos ret

La primera parte de la rutina limpia los píxeles, y la segunda asigna los colores a la pantalla. Es en esta segunda parte donde vamos a realizar la optimización.

Una vez ejecutado el primer LDIR, HL vale $57FF y DE vale $5800. Cargar un valor de 16 bits en un registro de 16 bits consume 10 ciclos de reloj y 3 bytes, por lo que haciendo LD HL, $5800 y LD DE, $5801, consumimos 20 ciclos de reloj y 6 bytes.

Como podemos ver, HL y DE valen uno menos de lo que necesitamos para asignar los atributos a la pantalla, por lo que lo único que necesitamos es incrementar su valor en uno, y es ahí donde vamos a conseguir la optimización; vamos a sustituir LD HL, $5800 y LD DE, $5801 por INC HL e INC DE. Incrementar un registro de 16 bits consume 6 ciclos de reloj y ocupa un byte, por lo que el coste total será de 12 ciclos de reloj y 2 bytes, frente a los 20 ciclos de reloj y 6 bytes actuales, logrando un ahorro de 8 ciclos de reloj y 4 bytes.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Limpia la pantalla, tinta 7, fondo 0. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- Cls: ; Limpia los píxeles de la pantalla ld hl, $4000 ; Carga en HL el inicio de la VideoRAM ld (hl), $00 ; Limpia los píxeles de esa dirección ld de, $4001 ; Carga en DE la siguiente posición de la VideoRAM ld bc, $17ff ; 6143 repeticiones ldir ; Limpia todos los píxeles de la VideoRAM ; Pone la tinta en blanco y el fondo en negro inc hl ; Apunta HL al inicio del área de atributos ld (hl), $07 ; Lo pone con la tinta en blanco y el fondo en negro inc de ; Apunta DE a la siguiente posición del área de atributos ld bc, $2ff ; 767 repeticiones ldir ; Asigna el valor a toda el área de atributos ret
Optimización de MoveBall

En la entrega 0x05 comentamos que se podían ahorrar 5 bytes y 2 ciclos de reloj, lo cual vamos a conseguir modificando cinco líneas del conjunto de rutinas MoveBall, que se encuentran en el archivo Game.asm. En concreto vamos a sustituir las cinco líneas JR moveBall_end por RET; JR ocupa 2 bytes y tarda 12 ciclos de reloj, mientras que RET ocupa 1 byte y tarda 10 ciclos de reloj.

Como podemos observar, en la etiqueta MoveBall_end sólo hay una instrucción, RET, de ahí que podamos sustituir todos los JR moveBall_end por RET.

Hemos dicho que sólo ahorramos 2 ciclos de reloj, lo cual debido a que cada vez que se llama a MoveBall, sólo se ejecuta uno de los JR, por eso solo se ahorran 2 ciclos y no 10, aunque sí se ahorran 5 bytes.

Los JR que vamos a sustituir, los encontramos como última línea de las etiquetas:

  • moveBall_right.
  • moveBall_rightLast.
  • moveBall_rightChg.
  • moveBall_left.
  • moveBall_leftLast.

La etiqueta movelBall_end se puede eliminar, pero no el RET que la sigue, aunque la etiqueta no ocupa nada.

Optimización de ReprintLine

En la entrega 0x06 comentamos que se podían ahorrar 5 bytes y 22 ciclos de reloj, lo cual vamos a conseguir modificando ocho líneas de la rutina ReprintLine del archivo Video.asm.

Lo primero que vamos a hacer es localizar la etiqueta reprintLine_loopCont y la vamos a mover tres líneas más abajo, justo encima de Call NextScan.

El siguiente paso es localizar la línea LD C, LINE y borrar las tres líneas siguientes.

;jr ReprintLine_loopCont ; ¡ELIMINAR! ;ReprintLine_00: ; ¡ELIMINAR! ;ld c, ZERO ; ¡ELIMINAR!

El siguiente paso es localizar las líneas JR C, reprintLine_00 y JR Z, reprintLine_00 y sustituimos reprintLine_00 por reprintLine_loopCont.

El último paso nos lleva al primero. Localizamos la nueva ubicación de la etiqueta reprintLine_loopCont, y cuatro líneas más arriba eliminamos LD C, LINE. Dos líneas más abajo de la línea eliminada, sustituimos OR C por OR LINE.

¿Qué hemos hecho?

El objetivo final de la rutina es repintar la parte de la línea central que se ha borrado, sin borrar la parte de la bola que hay donde se tiene que repintar, para lo cual obtenemos los píxeles que hay en pantalla y los mezclamos con la parte de la línea que hay que pintar, y ahí está la cuestión; si lo que hay que repintar de la línea es la parte que va a ZERO (blanco), no es necesario repintarla.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Repinta la línea central. ; Altera el valor de los registros AF, B y HL. ; ----------------------------------------------------------------------------- ReprintLine: ld hl, (ballPos) ; Carga en HL la posición de la bola ld a, l ; Carga la línea y columna en A and $e0 ; Se queda con la línea or $10 ; Pone la columna a 16 ($10) ld l, a ; Carga el valor en L. HL = Posición inicial ld b, $06 ; Se repintan 6 scanlines reprintLine_loop: ld a, h ; Carga tercio y scanline en A and $07 ; Se queda con el scanline ; Si está en los scanlines 0 o 7 pinta ZERO ; Si está en los scanlines 1, 2, 3, 4, 5 o 6 pinta LINE cp $01 ; Comprueba si está en scanline 1 o superior jr c, reprintLine_loopCont ; Si está por debajo, salta cp $07 ; Comprueba si está en scanline 7 jr z, reprintLine_loopCont ; Si es así, salta ld a, (hl) ; Obtiene los pixeles de la posición actual or LINE ; Los mezcla con C ld (hl), a ; Pinta el resultado en la posición actual reprintLine_loopCont: call NextScan ; Obtiene el scanline siguiente djnz reprintLine_loop ; Hasta que B = 0 ret
Optimización de GetPointSprite

En la entrega 0x08 comentamos que podríamos ahorrar 2 bytes y unos cuantos ciclos de reloj implementando la rutina GetPointSprite de otra manera, lo que vamos a hacer es no usar un bucle.

Actualmente, esta rutina tarda más cuanto mayor sea la puntuación de los jugadores. Mientras el máximo de puntos sea 15 no se aprecia el problema, pero si son 99 o 255, entonces ahí sí que tenemos un problema, tal y como pudimos apreciar cuando hicimos la pruebas y el partido no se paraba al llegar a 15 puntos.

Según la definición de los sprites, cada uno está a 4 bytes del otro, es por eso que lo que hacemos es un bucle partiendo de la dirección de Cero y sumando 4 bytes por cada punto que tiene el jugador del que vamos a pintar el marcador. En realidad, hacer esto sería lo mismo que multiplicar los puntos del jugador por 4, y sumarle el resultado a la dirección del sprite Cero. De esta manera siempre va a tardar lo mismo, sean 0 o 99 puntos, nos ahorramos 2 bytes y unos cuantos ciclos de reloj.

Recordemos que en GetPointSprite, recibimos en A la puntuación, y devolvemos en HL la dirección del sprite a pintar.

¿Cómo multiplicamos por 4 si el Z80 no tiene una instrucción para multiplicar?

Multiplicar no es más que sumar un número tantas veces como dice el multiplicador, o lo que es lo mismo, multiplicar un número por cuatro, sería igual a.

2*4 = 2+2+2+2 = 8

Esto lo podríamos hacer con un bucle, pero lo vamos a simplificar aún más, ya que para multiplicar un número por 4, solo nos hace falta hacer dos sumas.

3*4 = 3+3 = 6     6+6 = 12

Es decir, sumamos el número a sí mismo, y el resultado lo sumamos a si mismo, y ya tenemos hecha la multiplicación por 4. Si ese resultado lo sumamos a si mismo, ya tendríamos la multiplicación por 8, y si seguimos así por 16, 32, 64… o lo que es lo mismo n*2n.

Tenemos dos maneras de implementar GetPointSprite sin necesidad de modificar nada más: con un marcador de hasta 61 puntos o un marcador de hasta 99 puntos.

Vamos con la primera implementación, con un marcador de hasta 61 puntos (61 * 4 = 244 = 1 byte).

; ----------------------------------------------------------------------------- ; Obtiene el sprite correspondiente a pintar en el marcador. ; Entrada: A = puntuación. ; Salida: HL = Dirección del sprite a pintar. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- GetPointSprite: ; HASTA 61 PUNTOS ld hl, Cero ; Carga en HL la dirección del sprite del 0 ; Cada sprite está del anterior a 4 bytes add a, a ; Multiplica A * 2 add a, a ; Multiplica A * 2 = A original por 4 ld b, ZERO ld c, a ; Carga el valor de A en BC add hl, bc ; Se lo suma a HL ret

En este caso, la puntuación máxima sería 61 que al multiplicarlo por 4 da 244, resultado que nos cabe en un byte y por tanto podemos usar el registro A para realizar la multiplicación por 4. Esta rutina ocupa 10 bytes y tarda 50 ciclos de reloj.

Si una partida de Pong a 61 puntos se nos hace corta la podemos hacer a 99, la rutina ocuparía lo mismo que la anterior, pero tardaría 64 ciclos de reloj (en este caso las sumas hay que hacerlas con un registro de 16bits ya que 99*4 = 396 = 2 bytes).

; ----------------------------------------------------------------------------- ; Obtiene el sprite correspondiente a pintar en el marcador. ; Entrada: A = puntuación. ; Salida: HL = Dirección del sprite a pintar. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- GetPointSprite: ; HASTA 255 PUNTOS, 99 SI NO SE CAMBIA LA RUTINA DE IMPRESIÓN DEL MARCADOR ld h, ZERO ld l, a ; Carga en HL los puntos ; Cada sprite está del anterior a 4 bytes add hl, hl ; Multiplica HL * 2 add hl, hl ; Multiplica HL * 2 = HL original por 4 ld bc, Cero ; Carga en BC la dirección del sprite del 0 add hl, bc ; Lo suma a HL para calcular donde está el sprite ; que corresponde a la puntuación ret

Si queremos una puntuación mayor de 99 hay que modificar la rutina de impresión de los marcadores pues ahora solo imprime dos dígitos, y tener en cuenta que estas implementaciones de GetPointSprite tampoco serían válidas (posiblemente habría que repensar todo, empezando por la forma de declarar los sprites).

Optimización de PrintPoint y ReprintPoints.

¡Pero oye! Si ReprintPoints la acabamos de implementar de nuevo, al inicio de esta estrega.

Bueno, en realidad hemos añadido una parte para que repinte el marcador solo cuando sea necesario, pero hemos heredado alguna cosilla de la implementación original.

En la entrega 0x08 comentamos que podríamos ahorrar 2 bytes y 12 ciclos de reloj haciendo una pequeña modificación en la rutina PrintPoints. Pues bien, estamos de enhorabuena ya que en realidad nos vamos a ahorrar 33 bytes y 138 ciclos de reloj; los cambios que vamos realizar en PrintPoints, los vamos a realizar también en ReprintPoints.

En la tercera línea de PrintPoints encontramos PUSH HL, y esta es la primera línea que vamos a cambiar de lugar, ya que preservamos el valor del registro HL antes de tiempo. Cortamos esta línea y la pegamos tres líneas más abajo, justo antes de cargar la dirección de memoria donde se pintan los puntos del jugador 1 en HL, LD HL, POINTS_P1. El motivo de preservar el valor del registro HL es justamente esta instrucción.

Una vez que llamamos a pintar el punto, recuperamos el valor de HL, POP HL, e incrementamos HL dos veces para apuntarlo a la parte baja de la dirección donde está el segundo dígito. Pues bien, como hemos preservado HL después de posicionarnos en la parte alta de la dirección del primer dígito, ahora vamos a quitar uno de estos dos INC HL; nos acabamos de ahorrar 1 byte y 6 ciclos de reloj.

Esta misma modificación tenemos que hacerla al pintar el marcador del jugador 2 y en la rutina ReprintPoints. En total ahorramos 4 bytes y 24 ciclos de reloj.

Spirax comentó otra optimización que podríamos hacer, con la cual podremos quitar cuatro instrucciones INC L, ahorrando otros 4 bytes y 16 ciclos de reloj.

Tanto en PrintPoints como en ReprintPoints, al dibujar el segundo dígito de los marcadores hacemos los siguiente.

ld hl, POINTS_P1
inc l

ld hl, POINTS_P2
inc l

Como esto lo hacemos tanto en PrintPoints como en ReprintPoints, en realidad hacemos cuatro veces INC L, y lo podemos evitar de la siguiente manera.

ld hl, POINTS_P1 + 1

ld hl, POINTS_P2 + 1

De esta forma apuntamos directamente HL a la posición donde se dibuja el segundo dígito, y nos ahorramos los INC L.

Y ahora nos vamos a ahorrar 25 bytes y 138 ciclos de reloj más, gracias otra vez a Spirax.

En la parte final de la rutina ReprintPoints encontramos la etiqueta reprintPoint2_print, y justo por encima de esta etiqueta la instrucción RET C. Bien, vamos a borrar la etiqueta reprintPoint2_print y todo lo que tiene por debajo hasta el final de la rutina. Después de RET C vamos a incluir JR printPoint2_print.

En una implementación anterior, PrintPoints y ReprintPoints pintaban de distinta manera, pues ReprintPoints hacía OR con los píxeles de la pantalla, pero éste ya no es el caso, por lo que vamos a utilizar el código que pinta el marcador del jugador 2 para repintarlo, y nos vamos a ahorrar 25 bytes y 138 ciclos de reloj.

La etiqueta printPoint2_print no existe, por lo que vamos a incluirla. Buscamos la etiqueta PrintPoints, como vemos primero pinta el marcador del jugador 1, y una vez que ha finaliza pinta el marcador del jugador 2, que empieza justo debajo de la segunda llamada a PrintPoint. Pues es ahí, justo debajo del segundo CALL PrintPoint, donde vamos a añadir la etiqueta printPoint_2_print.

¡Muchas gracias Spirax!

El aspecto final de las rutinas es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- PrintPoints: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call PrintPoint ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P1 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 1 call PrintPoint ; Pinta el segundo dígito del marcador del jugador 1 printPoint_2_print: ; 1er dígito del jugador 2 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call PrintPoint ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P2 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 2 PrintPoint: ld b, $10 ; Cada dígito son 1 byte por 16 (scanlines) printPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz printPoint_printLoop ; Hasta que B = 0 ret ; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene tercio, línea y scanline de la posición de la bola cp POINTS_Y_B ; Lo compara con el límite inferior del marcador ret nc ; Si no hay acarreo, pasa por debajo y sale ld a, l ; Carga en A la línea y columna de la posición de la bola and $1f ; Se queda con la columna cp POINTS_X1_L ; Lo compara con el límite izquierdo del marcador 1 ret c ; Si hay acarreo, pasa por la izquierda y sale jr z, reprintPoint_1_print ; Si es 0, está justo en el margen izquierdo ; y salta para pintar cp POINTS_X2_R ; Lo compara con el límite derecho de marcador 2 jr z, printPoint_2_print ; Si es 0, está justo en el margen derecho ; y salta para pintar ret nc ; Si no hay acarreo, pasa por la derecha y sale reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr c, reprintPoint_1_print ; Si hay acarreo, pasa por el marcador 1 ; y salta para pintar jr nz, reprintPoint_2 ; Si no es cero, pasa por la derecha ; y salta para comprobar paso por marcador 2 reprintPoint_1_print: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ; 1er dígito ld e, (hl) ; Carga en E la parte baja de la dirección donde ; está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección donde ; está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call PrintPoint ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito inc hl ; Apunta HL a la parte baja de la dirección donde ; está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección donde ; está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 1 jr PrintPoint ; Pinta el segundo dígito del marcado del jugador 2 reprintPoint_2: cp POINTS_X2_L ; Lo compara con el límite derecho de marcador 2 ret c ; Si hay acarreo, pasa por la izquierda y sale ; Spirax jr printPoint_2_print ; Pinta el marcador del jugador

Compilamos, cargamos en el emulador y comprobamos que todo sigue funcionando.

Bug del golpeo de la bola en la parte baja de la pala

Es el momento de arreglar un bug que arrastramos desde que implementamos el cambio de velocidad e inclinación de la bola en base a en que parte de la pala golpea. Cuando la bola golpea en el último scanline de la pala, no cambia inclinación, ni velocidad, ni dirección vertical. ¿A qué se debe?

El motivo está en la forma en la que implementamos la detección de colisiones. Antes de evaluar en que parte de la pala golpea, evaluamos si golpea en la pala, y aquí está el error; cuando golpea en el último scanline de la pala sale de la rutina, con el flag Z activado indicando que hay colisión, pero sin evaluar en qué parte de la pala golpea.

Abrimos el archivo Video.asm y localizamos la etiqueta CheckCrossY. Quince líneas más abajo nos encontramos con esto.

ret nc ; Si no hay acarreo la bola pasa por debajo ; de la pala o colisiona en el último scanline. ; En este último caso ya se ha activado el flag Z

Si leemos atentamente los comentarios, salimos de la rutina si no hay acarreo (flag Z desactivado = no hay colisión). El problema es qué si no hay acarreo, el resultado puede ser mayor o igual a 0. Es decir, si el resultado es 0, salimos de la rutina con el flag Z activado (hay colisión) sin evaluar en que parte de la pala ha golpeado.

Para solucionar este aspecto vamos a hacer una doble comprobación y añadir una nueva etiqueta a la que saltar.

El código actual de la parte que vamos a tocar es el siguiente.

ret nc ; Si no hay acarreo la bola pasa por debajo ; de la pala o colisiona en el último scanline. ; En este último caso ya se ha activado el flag Z ; Dependiendo de donde sea la colisión, se asigna grado de inclinación ; y velocidad a la bola ld a, c ; Carga la posición del penúltimo scanline de la pala en A

Vamos a añadir una línea antes de RET NC y una etiqueta antes de LD A, C, dejando el código de la siguiente manera.

jr z, checkCrossY_eval ; Si es cero, choca en el último scanline ret nc ; Si no hay acarreo la bola pasa por debajo y sale. ; Dependiendo de donde sea la colisión, se asigna grado de inclinación ; y velocidad a la bola checkCrossY_eval: ld a, c ; Carga la posición del penúltimo scanline de la pala en A

Incluso ese JR Z, checkCrossY_eval lo podríamos cambiar por JR Z, checkCrossY_5_5, pues sabemos que ha golpeado en la parte inferior de la pala (probad de las dos maneras).

Compilamos, cargamos en el emulador y comprobamos que hemos arreglado el bug.

Sonido

Y abordamos el penúltimo paso; vamos a implementar efectos de sonido cuando la bola golpea con los laterales, las palas, o cuando se marque algún punto.

Añadimos el archivo Sound.asm, y añadimos las contantes y rutinas necesarias para nuestros efectos de sonido, que van a ser los sonidos que se van a reproducir cuando la bola rebota contra los distintos elementos.

Vamos a definir tres sonidos distintos:

  • Cuando se marca un punto.
  • Cuando la bola choca con una pala.
  • Cuando la bola choca con el borde.

Para cada sonido tenemos que definir la nota y la frecuencia. La frecuencia es el tiempo que va a durar la nota, y la vamos a identificar con el sufijo FQ.

; Punto C_3: EQU $0D07 C_3_FQ: EQU $0082 / $10 ; Pala C_4: EQU $066E C_4_FQ: EQU $0105 / $10 ; Rebote C_5: EQU $0326 C_5_FQ: EQU $020B / $10

Todos los sonidos que vamos a usar son DO, aunque en distintas escalas; a mayor escala, el sonido es más agudo. Las frecuencias especificadas son las que hacen que la nota dure un segundo, es por eso que las dividimos por 16. Si las multiplicáramos por 2, la nota duraría 2 segundos. A cada nota, en cada escala, le corresponde una frecuencia propia.

La siguiente constante que vamos a ver, es la dirección de memoria donde está alojada la rutina BEEPER de la ROM.

BEEPER: EQU $03B5

Esta rutina recibe en HL la nota y en DE la duración, y altera el valor de los registros AF, BC, DE, HL e IX, además de otro aspecto que veremos más adelante. Debido a que la rutina BEEPER de la ROM altera tantos registros, es recomendable no llamarla directamente; vamos a implementar una rutina que lo haga.

La rutina que vamos a implementar, recibe en A el tipo de sonido a emitir, 1 = punto, 2 = pala, 3 = borde, y no altera el valor de ningún registro.

PlaySound: push de push hl

Preservamos el valor de los registros DE, PUSH DE, y HL, PUSH HL.

cp $01 jr z, playSound_point

Comprobamos si el sonido a reproducir es de tipo 1 (punto), CP $01, y de ser así saltamos, JR Z, playSound_point.

cp $02 jr z, playSound_paddle

Si el sonido no es de tipo 1, comprobamos si es de tipo 2 (pala), CP $02, y de ser así saltamos, JR Z, playSound_paddle.

Si el sonido no es de tipo 1, ni de tipo 2, es de tipo 3 (borde).

ld hl, C_5 ld de, C_5_FQ jr beep

Cargamos en HL la nota, LD HL, C_5, cargamos en DE la frecuencia (duración), LD DE, C_5_FQ, y saltamos a reproducir el sonido, JR beep.

Si el sonido es de tipo 1 o 2, hacemos lo mismo, pero con los valores de cada sonido.

playSound_point: ld hl, C_3 ld de, C_3_FQ jr beep playSound_paddle: ld hl, C_4 ld de, C_4_FQ

Nos ahorramos el último JR, ya que justo después viene la rutina que reproduce el sonido.

beep: push af push bc push ix call BEEPER pop ix pop bc pop af pop hl pop de ret

Preservamos los valores de AF, PUSH AF, de BC, PUSH BC, y de IX, PUSH IX. Llamamos a la rutina de la ROM, CALL BEEPER, y recuperamos los valores de IX, POP IX, de BC, POP BC, de AF, POP AF, de HL, POP HL, y de DE, POP DE. Los valores de HL y DE los preservamos al principio de la rutina PlaySound. Por último, salimos, RET.

El aspecto final del archivo Sound.asm, es el siguiente.

; ----------------------------------------------------------------------------- ; Sound ; Fichero con los sonidos ; ----------------------------------------------------------------------------- ; Punto C_3: EQU $0D07 C_3_FQ: EQU $0082 / $10 ; Pala C_4: EQU $066E C_4_FQ: EQU $0105 / $10 ; Rebote C_5: EQU $0326 C_5_FQ: EQU $020B / $10 ; ----------------------------------------------------------------------------- ; Rutina beeper de la ROM. ; ; Entrada: HL = Nota. ; DE = Duración. ; ; Altera el valor de los registros AF, BC, DE, HL e IX. ; ----------------------------------------------------------------------------- BEEPER: EQU $03B5 ; ----------------------------------------------------------------------------- ; Reproduce el sonido de los rebotes. ; Entrada: A = Tipo de rebote. 1. Punto ; 2. Pala ; 3. Borde ; ----------------------------------------------------------------------------- PlaySound: ; Preserva el valor de los registros push de push hl cp $01 ; Evalúa si se emite el sonido de Punto jr z, playSound_point ; Si es así salta cp $02 ; Evalúa si se emite el sonido de Pala jr z, playSound_paddle ; Si es así salta ; Se emite el sonido de Borde ld hl, C_5 ; Carga en HL la nota ld de, C_5_FQ ; Carga en DE la duración (frecuencia) jr beep ; Salta a emitir el sonido ; Se emite el sonido de Punto playSound_point: ld hl, C_3 ; Carga en HL la nota ld de, C_3_FQ ; Carga en DE la duración (frecuencia) jr beep ; Salta a emitir el sonido ; Se emite el sonido de Pala playSound_paddle: ld hl, C_4 ; Carga en HL la nota ld de, C_4_FQ ; Carga en DE la duración (frecuencia) ; Hace sonar la nota beep: ; Preserva el valor de los registros ya que la rutina BEEPER de la ROM los altera push af push bc push ix call BEEPER ; Llama a la rutina BEEPER de la ROM ; Recupera el valor de los registros pop ix pop bc pop af pop hl pop de ret

Para acabar, tenemos que llamar a nuestra nueva rutina para emitir los sonidos de los rebotes de la bola. Abrimos el archivo Game.asm y localizamos la etiqueta checkBallCross_right. Vamos a añadir dos líneas entre la línea RET NZ, y la línea LD A, (ballSetting).

ld a, $02 call PlaySound

Carga el tipo de sonido en A, LD A, $02, y emite el sonido, CALL PlaySound.

Localizamos la etiqueta checkBallCross_left. Vamos a añadir las mismas dos líneas de antes entre la línea RET NZ, y la línea LD A, (ballSetting).

ld a, $02 call PlaySound

Localizamos la etiqueta moveBall_upChg. Justo debajo de la misma, añadimos dos líneas casi iguales a las anteriores.

ld a, $03 call PlaySound

Localizamos la etiqueta moveBall_downChg. Justo debajo de la misma, añadimos las dos líneas anteriores.

ld a, $03 call PlaySound

Localizamos la etiqueta moveBall_rightChg, y justo debajo añadimos.

ld a, $01 call PlaySound

Cinco líneas más abajo localizamos CALL SetBallLeft. Debajo añadimos.

ld a, $03 call PlaySound

Localizamos la etiqueta moveBall_leftChg, y justo debajo añadimos.

ld a, $01 call PlaySound

Cinco líneas más abajo localizamos CALL SetBallRight. Debajo añadimos.

ld a, $03 call PlaySound

Por último, abrimos el archivo Main.asm, localizamos la rutina Loop y justo encima añadimos las siguientes líneas.

ld a, $03 call PlaySound

Nos vamos al final del fichero, y en la parte de los “includes”, incluimos el archivo Sound.asm.

include "Sound.asm"

Si todo ha ido bien, hemos llegado al final. Compilamos, cargamos en el emulador y…

Ensamblador ZX Spectrum - Borde en blancoEnsamblador ZX Spectrum, borde en blanco

¿Qué le pasa al borde? ¿Por qué es blanco? Bueno, ya advertimos que la rutina BEEPER de la ROM altera muchas cosas, y una de ellas es el color del borde, aunque tiene fácil solución.

Por suerte, tenemos una variable de sistema donde podemos guardar el color del borde. En esta variable se guardan también los atributos de la pantalla inferior. El fondo de dicha pantalla es el color del borde.

Abrimos el archivo Video.asm y al inicio del mismo declaramos una constante con la dirección de memoria de dicha variable del sistema.

BORDCR: EQU $5c48

Localizamos la rutina Cls, y antes de la línea LD HL, $5800, añadimos.

ld a, $07 ; Fondo negro, tinta blanca

Modificamos la línea LD (HL), $07 dejándola así.

ld (hl), a

Por último, antes de RET, añadimos.

ld (BORDCR), a

Compilamos, cargamos en el emulador, y ahora sí. ¿Hemos terminado nuestro PorompomPong?

Ensamblador ZX Spectrum - Borde en negroEnsamblador ZX Spectrum, borde en negro
Compatibilidad con 16K

Todavía nos falta una última cosa por hacer. ¿Es compatible nuestro programa con el modelo 16K? Pues todavía no, pero como no trabajamos con interrupciones, es muy sencillo hacerlo compatible.

Vamos a abrir el archivo Main.asm, vamos a localizar las directivas ORG y END, y vamos a sustituir $8000 por $5dad en el caso de ORG. En el caso de END, vamos a sustituir $8000 por Main, que es la etiqueta de entrada al programa. Si ahora compilamos y cargamos en el emulador con el modelo 16K, nuestro programa es compatible.

Si nos fijamos bien, podemos observar que se ha perdido algo de velocidad. Esta pérdida es debida a que los segundos 16 KiB del ZX Spectrum, que es donde ahora cargamos el programa, es lo que se llama memoria contenida, y está compartida con la ULA; cuando la ULA trabaja, todo se para.

Vamos a volver a cambiar la velocidad a la que va la bola.

Abrimos el archivo Sprite.asm, localizamos la etiqueta ballSetting, comentamos la línea db $21 y escribimos justo debajo.

; or $21 or $19

Ahora la bola se inicia a velocidad 3, que va a ser la más lenta.

Abrimos el archivo Game.asm, localizamos la etiqueta SetBallLeft, comentamos la línea 7, y escribimos justo debajo.

; or $21 or $19

Ahora, cuando reiniciamos la bola para que salga por la izquierda de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta SetBallRight, comentamos la línea 7, y escribimos justo debajo.

; db $61 db $59

Ahora, cuando reiniciamos la bola para que salga por la derecha de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta checkCrossY_1_5, comentamos la línea 7, y escribimos justo debajo.

; or $21 or $19

Ahora la velocidad de la bola es 3 en lugar de 4.

Localizamos la etiqueta checkCrossY_2_5, comentamos la línea 7, y escribimos justo debajo.

; or $1a or $12

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_3_5, comentamos la línea 7, y escribimos justo debajo.

; or $17 or $0f

Ahora la velocidad de la bola es 1 en lugar de 2.

Localizamos la etiqueta checkCrossY_4_5, comentamos la línea 7, y escribimos justo debajo.

; or $9a or $92

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_5_5, comentamos la línea 3, y escribimos justo debajo.

; or $a1 or $99

Ahora la velocidad de la bola es 3 en lugar de 4.

Compilamos, cargamos en el emulador y probamos. ¿Hemos terminado?

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, sonido y optimización

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x0A Ensamblador ZX Spectrum Pong – Sonido y optimización [Espamatica] [Leer]


Con esta entrega de Ensamblador ZX Spectrum Pong, vamos a dar por concluida la segunda parte del tutorial dedicándola al sonido y la optimización, y vamos a continuar justo donde lo dejamos en la entrega anterior.

Como es costumbre, creamos la carpeta Paso10 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso09.

Ensamblador ZX Spectrum – Sonido y optimización

Al final de la entrega anterior comentábamos que había algo que no funcionaba del todo bien cuando la bola golpeaba en el último scanline de la pala. También nos preguntábamos al final de la entrega anterior si la bola no iba lenta; sí, la bola va algo lenta. Esto es debido, en gran parte, a que el marcador se repinta en cada iteración del bucle principal, lo cual no es necesario. Vamos a empezar cambiando el repintado del marcador.

Optimización de repintado de marcador

El marcador solo se debería repintar cuando es borrado por la bola. Modificando este aspecto, vamos a ganar velocidad en la bola, ya que el tiempo de proceso en cada iteración del bucle principal se va a reducir.

Lo primero es localizar el área de la pantalla dónde la bola borra el marcador, definimos una serie de constantes en el archivo Sprite.asm, justo debajo de la constante POINTS_P2.

POINTS_X1_L: EQU $0c POINTS_X1_R: EQU $0f POINTS_X2_L: EQU $10 POINTS_X2_R: EQU $13 POINTS_Y_B: EQU $14

El significado de estas constantes, en orden de aparición, es:

  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la derecha.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la derecha.
  • Tercio, línea y scanline en la que la bola empieza a borrar el marcador por la parte de abajo.

Una vez que hemos definido estas constantes, vamos a modificar las rutinas PrintPoints y ReprintPoints del archivo Video.asm, empezando por localizar la etiqueta printPoint_print, que vamos a sustituir por PrintPoint. Dentro de la rutina PrintPoints, hay tres llamadas a printPoint_print, que vamos a sustituir por PrintPoint.

Compilamos, cargamos en el emulador y comprobamos que no hemos roto nada.

El siguiente paso es modificar la rutina ReprintPoints. En realidad, no la vamos a modificar, la vamos a borrar y a volver a implementar.

ReprintPoints: ld hl, (ballPos) call GetPtrY cp POINTS_Y_B ret nc

Cargamos la posición de la bola en HL, LD HL, (ballPos), obtenemos tercio, línea y scanline de la posición de la bola, CALL GetPtrY, y lo comparamos con la posición donde la bola empieza a borrar el marcador desde abajo, CP POINTS_Y_B. Si no hay acarreo, la bola pasa por debajo del marcador y sale, RET NC.

Si hay acarreo, según la coordenada Y de la bola, ésta podría borrar el marcador.

ld a, l and $1f cp POINTS_X1_L ret c jr z, reprintPoint_1_print

Cargamos la línea y columna de la posición de la bola en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con la coordenada X en la que se empieza a borrar el marcador del jugador 1 por la izquierda, CP POINTS_X1_L. Si hay acarreo, la bola pasa por la izquierda del marcador y sale, RET C. Si las dos coordenadas coinciden, la bola va a borrar el marcador del jugador 1, y salta para repintarlo, JR Z, reprintPoint_1_print.

Si no hemos salido, ni saltado, seguimos con las comprobaciones.

cp POINTS_X2_R jr z, reprintPoint_2_print ret nc

Comparamos la coordenada X donde está la bola con la coordenada donde se empieza a borrar el marcador del jugador 2 por la derecha, CP POINT_X2_R. Si son iguales, salta a repintar el marcador del jugador 2, JR Z, reprintPoint_2_print. Si no salta y no hay acarreo, la bola pasa por la derecha y sale, RET NC.

Si no hemos saltado, ni hemos salido, seguimos con las comprobaciones.

reprintPoint_1: cp POINTS_X1_R jr c, reprintPoint_1_print jr nz, reprintPoint_2

Comparamos la coordenada X de la bola con la coordenada donde la bola empieza a borrar el marcador del jugador 1 por la derecha, CP POINTS_X1_R. Si hay acarreo, está borrando el marcador del jugador 1 y salta para repintarlo, JR C, reprintPoint_1_print. Si no son la misma coordenada, pasa por la derecha del marcador del jugador 1 y salta para comprobar si borra el marcador del jugador 2, JR NZ, reprintPoint_2.

Si está borrando el marcador del jugador 1, lo repinta.

reprintPoint_1_print: ld a, (p1points) call GetPointSprite push hl

Cargamos los puntos del jugador 1 en A, LD A, (p1points), obtenemos la dirección del sprite a pintar, CALL GetPointSprite, y preservamos el valor, PUSH HL.

Empezamos pintando el primer dígito, las decenas.

ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P1 call PrintPoint pop hl

Cargamos en E la parte baja de la dirección de memoria del sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, cargamos la parte alta de la dirección en D, LD D, (HL), cargamos en HL la dirección de memoria donde se pinta el marcador del jugador 1, LD HL, POINTS_P1, pintamos el primer dígito, CALL PrintPoint, y recuperamos el valor de HL, POP HL.

Terminamos pintando el segundo dígito.

inc hl inc hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P1 inc l jr PrintPoint

Apuntamos HL a la dirección de memoria del sprite del segundo dígito, INC HL INC HL, cargamos la parte baja de la dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, la cargamos en D, LD D, (HL), cargamos en HL la dirección de memoria dónde se pinta el marcador del jugador 1, LD HL, POINTS_P1, apuntamos HL a la dirección donde se pinta el segundo dígito, INC L, y pintamos el dígito y salimos, JR PrintPoint.

Posiblemente os estaréis preguntando, ¿cómo salimos? ¡Si no hay ningún RET! Estaréis pensando que en lugar de JR PrintPoint, tendríamos que haber puesto CALL PrintPoint. Y efectivamente esto funciona, pero no es necesario. Además, de la forma que lo hemos implementado, ahorramos tiempo de proceso y bytes.

La última instrucción de PrintPoint es un RET, y este es el RET que utilizamos para salir, por eso podemos poner JR en lugar de CALL y RET. Por eso, y porque no tenemos nada que tengamos que recuperar de la pila. Si hubiéramos dejado algo en la pila, los resultados serían impredecibles.

A continuación, podemos ver la diferencia de ciclos de reloj y bytes entre hacerlo de una manera o de otra.

InstrucciónCiclos de relojBytes
CALL PrintPoint173
RET101
JR PrintPoint122
Ciclos de reloj y bytes de las opciones estudiadas

Nos hemos ahorrado 15 ciclos de reloj y 2 bytes.

También hemos cambiado la forma de repintar. Antes repintábamos los marcadores haciendo OR con lo que hubiera pintado en esa zona, y ahora directamente pintamos el marcador. El resultado es que al pintar el marcador borramos la bola, lo que puede producir algún parpadeo. Como estos parpadeos también existen en el arcade original, lo dejamos así… o podéis cambiarlo.

Vamos ahora a ver cómo repintamos el marcador del jugador 2.

reprintPoint_2: cp POINTS_X2_L ret c

En este punto, solo hay que comprobar que la bola no esté pasando entre los marcadores sin borrarlos. Comparamos con el límite izquierdo del marcador del jugador 2, CP POINTS_X2_L, y si hay acarreo sale pues pasa por la izquierda, RET C.

Si no ha salido, hay que repintar el marcador del jugador 2, lo cual es casi idéntico a lo que hacemos con el marcador del jugador 1, por lo que se marcan las diferencias sin entrar en el detalle.

reprintPoint_2_print: ; ¡CAMBIO! ld a, (p2points) ; ¡CAMBIO! call GetPointSprite push hl ; 1er dígito ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; ¡CAMBIO! call PrintPoint pop hl ; 2º dígito inc hl inc hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; ¡CAMBIO! inc l jr PrintPoint

Siendo el aspecto final de la rutina el siguiente.

; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld hl, (ballPos) ; Carga la posición de la bola en HL call GetPtrY ; Obtiene tercio, línea y scanline de esta posición cp POINTS_Y_B ; Compara con la posición Y donde ; empieza a borrar el marcador ret nc ; Si no hay acarreo, paso por debajo y sale ; Si llega aquí la bola podría borrar el marcador, según su posición Y ld a, l ; Carga línea y columna de la posición ; de la bola en A and $1f ; Se queda con la columna cp POINTS_X1_L ; Compara con la posición donde la bola borrar el ; marcador del jugador 1 por la izquierda ret c ; Si hay acarreo pasa por la izquierda y sale jr z, reprintPoint_1_print ; Si coinciden, la bola va a borrar el marcador ; y repinta ; Sigue con las comprobaciones cp POINTS_X2_R ; Compara la coordenada X de la bola con la ; posición donde borra el marcador 2 por la derecha jr z, reprintPoint_2_print ; Si son iguales, repinta el marcador ret nc ; Si no hay acarreo, pasa por la derecha y sale ; Resto de comprobaciones para averiguar si borra el marcador 1 reprintPoint_1: cp POINTS_X1_R ; Compara la coordenada X de la bola con la ; posición donde borra el marcador 1 por la derecha jr c, reprintPoint_1_print ; Si hay acarreo, borra el marcador y repinta jr nz, reprintPoint_2 ; Si no es 0 para por la derecha del marcador 1 ; y salta ; Repinta el marcador del jugador 1 reprintPoint_1_print: ld a, (p1points) ; Carga en A la puntuación del jugador 1 call GetPointSprite ; Obtiene la dirección del sprite a pintar push hl ; Preserva el valor de HL ld e, (hl) ; Carga en E la parte baja de la dirección ; del sprite inc hl ; Apunta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P1 ; Carga en HL la dirección dónde se pinta el ; marcador 1 call PrintPoint ; Pinta el primer dígito pop hl ; Recupera el valor de HL inc hl inc hl ; Apunta HL al sprite del segundo dígito ld e, (hl) ; Carga la parte baja de la dirección en E inc hl ; A punta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P1 ; Carga en HL la dirección dónde se pinta el ; marcador 1 inc l ; Apunta a la dirección dónde se pinta el segundo ; dígito jr PrintPoint ; Pinta el dígito y sale ; Resto de comprobaciones para averiguar si borra el marcador 2 reprintPoint_2: cp POINTS_X2_L ; Compara la coordenada X de la bola con la ; posición donde borra el marcador 2 por la ; izquierda ret c ; Si hay acarreo, pasa por la izquierda y sale ; Repinta el marcador del jugador 2 reprintPoint_2_print: ld a, (p2points) ; Carga en A la puntuación del jugador 2 call GetPointSprite ; Obtiene la dirección del sprite a pintar push hl ; Preserva el valor de HL ld e, (hl) ; Carga en E la parte baja de la dirección del ; sprite inc hl ; Apunta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P2 ; Carga en HL la dirección dónde se pinta el ; marcador 2 call PrintPoint ; Pinta el primer dígito pop hl ; Recupera el valor de HL inc hl inc hl ; Apunta HL al sprite del segundo dígito ld e, (hl) ; Carga la parte baja de la dirección en E inc hl ; A punta HL a la parte alta de la dirección ld d, (hl) ; La carga en D ld hl, POINTS_P2 ; Carga en HL la dirección dónde se pinta el ; marcador 2 inc l ; Apunta a la dirección dónde se pinta el segundo ; dígito jr PrintPoint ; Pinta el dígito y sale

Compilamos, cargamos en el emulador, y vemos el resultado.

Podemos ver que la bola ahora va más rápida, incluso cuándo tiene que ir lento. También, si nos fijamos cuando es el jugador 2 el que marca el tanto y la bola debe salir por la derecha, parte de la misma se ve durante un corto espacio de tiempo en la izquierda.

Si hacemos memoria, cuando marcamos un punto la pelota sale desde el campo del jugador que ha ganado el punto. Eso nos lleva a la conclusión de que el problema está en la rutina SetBallRight, y más concretamente, en la primera línea.

ld hl, $4d7f

Según esta línea, posicionamos la pelota en tercio el 1, scanline 5, línea 3, columna 31. Además, dos líneas más abajo, cambiamos la rotación de la bola, poniéndola a -1.

ld a, $ff
ld (ballRotation), a

Ahora, si buscamos el sprite correspondiente a esta rotación, vemos que es el siguiente.

db    $00, $78    ; +7/$07    00000000 01111000       -1/$ff

Por lo que en la columna 31 la pintamos en blanco, y en la 32 pintamos $78. Pero es que la columna 32 no existe; las columnas en total son 32, pero van de la 0 a la 31. Al pintar en la 32, estamos pintando en la columna 0.

Una vez visto esto, la solución es sencilla. Editamos la primera línea de la rutina SetBallRight, para posicionar la bola en la columna 30.

ld hl, $4d7e

Y ahora vamos a cambiar la velocidad de la bola para que no vaya tan rápida. La configuración de la bola la tenemos guardada en ballSetting, en el archivo Sprite.asm.

; Velocidad y dirección de la bola. ; bits 0 a 3: Movimientos de la bola para que cambie la posición Y. ; Valores f = semiplano, 2 = semi diagonal, 1 = diagonal ; bits 4 y 5: velocidad de la bola: 1 muy rápido, 2 rápido, 3 lento ; bit 6: dirección X: 0 derecha / 1 izquierda ; bit 7: dirección Y: 0 arriba / 1 abajo ballSetting: db $31 ; 0011 0001

Según vemos en los comentarios, la velocidad de la bola se configura en los bits 4 y 5. Sería tan sencillo como que la velocidad 2 sea muy rápido, la 3 rápido, y la… ¡Cáspita! En 2 bits solo podemos especificar valores del 0 a 3, y el resto de bits lo tenemos ocupados.

Vamos a “robar” un bit a la inclinación de la bola. Como resultado, podremos reducir la velocidad de la bola, y como contraprestación, cuando la bola vaya plana, va a ir un poco más inclinada.

; Velocidad y dirección de la bola. ; bits 0 a 2: Movimientos de la bola para que cambie la posición Y. ; Valores 7 = semiplano, 2 = semi diagonal, 1 = diagonal ; bits 3 y 5: velocidad de la bola: 2 muy rápido, 3 rápido, 4 lento ; bit 6: dirección X: 0 derecha / 1 izquierda ; bit 7: dirección Y: 0 arriba / 1 abajo ballSetting: db $21 ; 0010 0001

Y ahora hay tres rutinas que tenemos que cambiar:

  • CheckCrossY en Game.asm: en esta rutina asignamos inclinación y velocidad de la bola, dependiendo de en qué parte de la pala golpea.
  • MoveBallY en Game.asm: en esta rutina evaluamos si los movimientos acumulados de la bola han alcanzado los necesarios para cambiar la coordenada Y de la misma.
  • SetBallLeft y SetBallRight en Game.asm: en estas rutinas reiniciamos la configuración de la bola.
  • Loop en Main.asm: al inicio de esta rutina, verificamos si se ha llegado al número de iteraciones del bucle necesarias para mover la bola.

Empezamos por CheckCrossY en Game.asm. Localizamos la etiqueta checkCrossY_1_5, y después la línea OR $31.

or    $31   ; Hacia arriba, velocidad 3 e inclinación diagonal

Según la nueva definición, vamos a poner velocidad 4 e inclinación diagonal.

0010 0001

Los bits marcados en rojo especifican la velocidad, y los marcados en amarillo la inclinación. La línea OR $31 debe quedar de la siguiente manera.

or $21

Localizamos la etiqueta checkCrossY_2_5 y ponemos velocidad 3, inclinación semi diagonal.

0001 1010

Modificamos la línea.

or    $22   ; Hacia arriba, velocidad 2 e inclinación semi diagonal

Y la dejamos como.

or $1a

Localizamos la etiqueta checkCrossY_3_5 y ponemos velocidad 2, inclinación semi plana.

0001 0111

Modificamos la línea.

or           $1f         ; Hacia arriba/abajo, velocidad 1 e inclinación semi plana

Y la dejamos como.

or $17

Localizamos la etiqueta checkCrossY_4_5 y ponemos velocidad 3, inclinación semi diagonal.

1001 1010

Modificamos la línea.

or    $a2   ; Hacia abajo, velocidad 2 e inclinación semi diagonal

Y la dejamos como.

or $9a

Localizamos la etiqueta checkCrossY_5_5 y ponemos velocidad 4, inclinación diagonal.

1010 0001

Modificamos la línea.

or    $b1   ; Hacia abajo, velocidad 3 e inclinación diagonal

Y la dejamos como.

or $a1

Con esto hemos acabado con la parte más laboriosa de la modificación.

Localizamos la etiqueta MoveBallY, y modificamos la segunda línea.

and   $0f

Y la dejamos como.

and $07

Con $0f ahora obtendríamos la inclinación y el primer bit de la velocidad. Con $07 sólo obtenemos la inclinación.

Modificamos el reinicio de la configuración de la bola en las rutinas SetBallLeft y SetBallRight.

En SetBallLeft modificamos la línea.

or $31      ; Pone dirección X a derecha, velocidad 3, inclinación diagonal

Y la dejamos como.

or $21

En SetBallRight modificamos la línea.

or    $71   ; Pone dirección X a izquierda, velocidad 3, inclinación diagonal

Y la dejamos como.

or $61

Vamos a terminar modificando el código de la etiqueta Loop de Main.asm. A partir de la segunda línea, nos encontramos 4 instrucciones RRCA. Quitamos una, para rotar sólo 3 veces y dejar en los bits 0, 1 y 2, la velocidad de la bola.

; rrca ; ¡ELIMINAR! rrca rrca rrca

Como ahora tenemos 3 bits para la velocidad, en lugar de dos, modificamos la línea siguiente, que es.

and   $03

Y la dejamos como.

and $07

Compilamos, cargamos en el emulador, y comprobamos que la velocidad de la bola es ahora más llevadera, en detrimento de la inclinación.

Optimización de ScanKeys

Ahora es el momento de optimizar la rutina ScanKeys, tal y como anunciamos en la entrega 0x02.

En la rutina ScanKeys hay cuatro instrucciones BIT, dos BIT $00, A, y otras dos BIT $01, A. Con las instrucciones BIT comprobamos el estado de un BIT en concreto de un registro, sin alterar el valor de dicho registro; cada instrucción BIT ocupa 2 bytes y tarda 8 ciclos de reloj.

Vamos a sustituir las instrucciones BIT por AND, ahorrándonos un ciclo de reloj en cada una. Sustituimos las instrucciones BIT $00, A por AND $01, y las instrucciones BIT $01, A por AND $02. Con esta modificación vamos a ahorrar 4 ciclos de reloj, aunque vamos a alterar el valor del registro A, que en este caso no importa.

Optimización de Cls

En la entrega 0x03, comentamos que la rutina Cls se podía optimizar ahorrándonos 8 ciclos de reloj y 4 bytes.

Vamos a recordar cómo es la rutina actualmente.

; ----------------------------------------------------------------------------- ; Limpia la pantalla, tinta 7, fondo 0. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- Cls: ; Limpia los píxeles de la pantalla ld hl, $4000 ; Carga en HL el inicio de la VideoRAM ld (hl), $00 ; Limpia los píxeles de esa dirección ld de, $4001 ; Carga en DE la siguiente posición de la VideoRAM ld bc, $17ff ; 6143 repeticiones ldir ; Limpia todos los píxeles de la VideoRAM ; Pone la tinta en blanco y el fondo en negro ld hl, $5800 ; Carga en HL el inicio del área de atributos ld (hl), $07 ; Lo pone con la tinta en blanco y el fondo en negro ld de, $5801 ; Carga en DE la siguiente posición del área de atributos ld bc, $2ff ; 767 repeticiones ldir ; Asigna el valor a toda el área de atributos ret

La primera parte de la rutina limpia los píxeles, y la segunda asigna los colores a la pantalla. Es en esta segunda parte donde vamos a realizar la optimización.

Una vez ejecutado el primer LDIR, HL vale $57FF y DE vale $5800. Cargar un valor de 16 bits en un registro de 16 bits consume 10 ciclos de reloj y 3 bytes, por lo que haciendo LD HL, $5800 y LD DE, $5801, consumimos 20 ciclos de reloj y 6 bytes.

Como podemos ver, HL y DE valen uno menos de lo que necesitamos para asignar los atributos a la pantalla, por lo que lo único que necesitamos es incrementar su valor en uno, y es ahí donde vamos a conseguir la optimización; vamos a sustituir LD HL, $5800 y LD DE, $5801 por INC HL e INC DE. Incrementar un registro de 16 bits consume 6 ciclos de reloj y ocupa un byte, por lo que el coste total será de 12 ciclos de reloj y 2 bytes, frente a los 20 ciclos de reloj y 6 bytes actuales, logrando un ahorro de 8 ciclos de reloj y 4 bytes.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Limpia la pantalla, tinta 7, fondo 0. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- Cls: ; Limpia los píxeles de la pantalla ld hl, $4000 ; Carga en HL el inicio de la VideoRAM ld (hl), $00 ; Limpia los píxeles de esa dirección ld de, $4001 ; Carga en DE la siguiente posición de la VideoRAM ld bc, $17ff ; 6143 repeticiones ldir ; Limpia todos los píxeles de la VideoRAM ; Pone la tinta en blanco y el fondo en negro inc hl ; Apunta HL al inicio del área de atributos ld (hl), $07 ; Lo pone con la tinta en blanco y el fondo en negro inc de ; Apunta DE a la siguiente posición del área de atributos ld bc, $2ff ; 767 repeticiones ldir ; Asigna el valor a toda el área de atributos ret
Optimización de MoveBall

En la entrega 0x05 comentamos que se podían ahorrar 5 bytes y 2 ciclos de reloj, lo cual vamos a conseguir modificando cinco líneas del conjunto de rutinas MoveBall, que se encuentran en el archivo Game.asm. En concreto vamos a sustituir las cinco líneas JR moveBall_end por RET; JR ocupa 2 bytes y tarda 12 ciclos de reloj, mientras que RET ocupa 1 byte y tarda 10 ciclos de reloj.

Como podemos observar, en la etiqueta MoveBall_end sólo hay una instrucción, RET, de ahí que podamos sustituir todos los JR moveBall_end por RET.

Hemos dicho que sólo ahorramos 2 ciclos de reloj, lo cual debido a que cada vez que se llama a MoveBall, sólo se ejecuta uno de los JR, por eso solo se ahorran 2 ciclos y no 10, aunque sí se ahorran 5 bytes.

Los JR que vamos a sustituir, los encontramos como última línea de las etiquetas:

  • moveBall_right.
  • moveBall_rightLast.
  • moveBall_rightChg.
  • moveBall_left.
  • moveBall_leftLast.

La etiqueta movelBall_end se puede eliminar, pero no el RET que la sigue, aunque la etiqueta no ocupa nada.

Optimización de ReprintLine

En la entrega 0x06 comentamos que se podían ahorrar 5 bytes y 22 ciclos de reloj, lo cual vamos a conseguir modificando ocho líneas de la rutina ReprintLine del archivo Video.asm.

Lo primero que vamos a hacer es localizar la etiqueta reprintLine_loopCont y la vamos a mover tres líneas más abajo, justo encima de Call NextScan.

El siguiente paso es localizar la línea LD C, LINE y borrar las tres líneas siguientes.

;jr ReprintLine_loopCont ; ¡ELIMINAR! ;ReprintLine_00: ; ¡ELIMINAR! ;ld c, ZERO ; ¡ELIMINAR!

El siguiente paso es localizar las líneas JR C, reprintLine_00 y JR Z, reprintLine_00 y sustituimos reprintLine_00 por reprintLine_loopCont.

El último paso nos lleva al primero. Localizamos la nueva ubicación de la etiqueta reprintLine_loopCont, y cuatro líneas más arriba eliminamos LD C, LINE. Dos líneas más abajo de la línea eliminada, sustituimos OR C por OR LINE.

¿Qué hemos hecho?

El objetivo final de la rutina es repintar la parte de la línea central que se ha borrado, sin borrar la parte de la bola que hay donde se tiene que repintar, para lo cual obtenemos los píxeles que hay en pantalla y los mezclamos con la parte de la línea que hay que pintar, y ahí está la cuestión; si lo que hay que repintar de la línea es la parte que va a ZERO (blanco), no es necesario repintarla.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Repinta la línea central. ; Altera el valor de los registros AF, B y HL. ; ----------------------------------------------------------------------------- ReprintLine: ld hl, (ballPos) ; Carga en HL la posición de la bola ld a, l ; Carga la línea y columna en A and $e0 ; Se queda con la línea or $10 ; Pone la columna a 16 ($10) ld l, a ; Carga el valor en L. HL = Posición inicial ld b, $06 ; Se repintan 6 scanlines reprintLine_loop: ld a, h ; Carga tercio y scanline en A and $07 ; Se queda con el scanline ; Si está en los scanlines 0 o 7 pinta ZERO ; Si está en los scanlines 1, 2, 3, 4, 5 o 6 pinta LINE cp $01 ; Comprueba si está en scanline 1 o superior jr c, reprintLine_loopCont ; Si está por debajo, salta cp $07 ; Comprueba si está en scanline 7 jr z, reprintLine_loopCont ; Si es así, salta ld a, (hl) ; Obtiene los pixeles de la posición actual or LINE ; Los mezcla con C ld (hl), a ; Pinta el resultado en la posición actual reprintLine_loopCont: call NextScan ; Obtiene el scanline siguiente djnz reprintLine_loop ; Hasta que B = 0 ret
Optimización de GetPointSprite

En la entrega 0x08 comentamos que podríamos ahorrar 2 bytes y unos cuantos ciclos de reloj implementando la rutina GetPointSprite de otra manera, lo que vamos a hacer es no usar un bucle.

Actualmente, esta rutina tarda más cuanto mayor sea la puntuación de los jugadores. Mientras el máximo de puntos sea 15 no se aprecia el problema, pero si son 99 o 255, entonces ahí sí que tenemos un problema, tal y como pudimos apreciar cuando hicimos la pruebas y el partido no se paraba al llegar a 15 puntos.

Según la definición de los sprites, cada uno está a 4 bytes del otro, es por eso que lo que hacemos es un bucle partiendo de la dirección de Cero y sumando 4 bytes por cada punto que tiene el jugador del que vamos a pintar el marcador. En realidad, hacer esto sería lo mismo que multiplicar los puntos del jugador por 4, y sumarle el resultado a la dirección del sprite Cero. De esta manera siempre va a tardar lo mismo, sean 0 o 99 puntos, nos ahorramos 2 bytes y unos cuantos ciclos de reloj.

Recordemos que en GetPointSprite, recibimos en A la puntuación, y devolvemos en HL la dirección del sprite a pintar.

¿Cómo multiplicamos por 4 si el Z80 no tiene una instrucción para multiplicar?

Multiplicar no es más que sumar un número tantas veces como dice el multiplicador, o lo que es lo mismo, multiplicar un número por cuatro, sería igual a.

2*4 = 2+2+2+2 = 8

Esto lo podríamos hacer con un bucle, pero lo vamos a simplificar aún más, ya que para multiplicar un número por 4, solo nos hace falta hacer dos sumas.

3*4 = 3+3 = 6     6+6 = 12

Es decir, sumamos el número a sí mismo, y el resultado lo sumamos a si mismo, y ya tenemos hecha la multiplicación por 4. Si ese resultado lo sumamos a si mismo, ya tendríamos la multiplicación por 8, y si seguimos así por 16, 32, 64… o lo que es lo mismo n*2n.

Tenemos dos maneras de implementar GetPointSprite sin necesidad de modificar nada más: con un marcador de hasta 61 puntos o un marcador de hasta 99 puntos.

Vamos con la primera implementación, con un marcador de hasta 61 puntos (61 * 4 = 244 = 1 byte).

; ----------------------------------------------------------------------------- ; Obtiene el sprite correspondiente a pintar en el marcador. ; Entrada: A = puntuación. ; Salida: HL = Dirección del sprite a pintar. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- GetPointSprite: ; HASTA 61 PUNTOS ld hl, Cero ; Carga en HL la dirección del sprite del 0 ; Cada sprite está del anterior a 4 bytes add a, a ; Multiplica A * 2 add a, a ; Multiplica A * 2 = A original por 4 ld b, ZERO ld c, a ; Carga el valor de A en BC add hl, bc ; Se lo suma a HL ret

En este caso, la puntuación máxima sería 61 que al multiplicarlo por 4 da 244, resultado que nos cabe en un byte y por tanto podemos usar el registro A para realizar la multiplicación por 4. Esta rutina ocupa 10 bytes y tarda 50 ciclos de reloj.

Si una partida de Pong a 61 puntos se nos hace corta la podemos hacer a 99, la rutina ocuparía lo mismo que la anterior, pero tardaría 64 ciclos de reloj (en este caso las sumas hay que hacerlas con un registro de 16bits ya que 99*4 = 396 = 2 bytes).

; ----------------------------------------------------------------------------- ; Obtiene el sprite correspondiente a pintar en el marcador. ; Entrada: A = puntuación. ; Salida: HL = Dirección del sprite a pintar. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- GetPointSprite: ; HASTA 255 PUNTOS, 99 SI NO SE CAMBIA LA RUTINA DE IMPRESIÓN DEL MARCADOR ld h, ZERO ld l, a ; Carga en HL los puntos ; Cada sprite está del anterior a 4 bytes add hl, hl ; Multiplica HL * 2 add hl, hl ; Multiplica HL * 2 = HL original por 4 ld bc, Cero ; Carga en BC la dirección del sprite del 0 add hl, bc ; Lo suma a HL para calcular donde está el sprite ; que corresponde a la puntuación ret

Si queremos una puntuación mayor de 99 hay que modificar la rutina de impresión de los marcadores pues ahora solo imprime dos dígitos, y tener en cuenta que estas implementaciones de GetPointSprite tampoco serían válidas (posiblemente habría que repensar todo, empezando por la forma de declarar los sprites).

Optimización de PrintPoint y ReprintPoints.

¡Pero oye! Si ReprintPoints la acabamos de implementar de nuevo, al inicio de esta estrega.

Bueno, en realidad hemos añadido una parte para que repinte el marcador solo cuando sea necesario, pero hemos heredado alguna cosilla de la implementación original.

En la entrega 0x08 comentamos que podríamos ahorrar 2 bytes y 12 ciclos de reloj haciendo una pequeña modificación en la rutina PrintPoints. Pues bien, estamos de enhorabuena ya que en realidad nos vamos a ahorrar 33 bytes y 138 ciclos de reloj; los cambios que vamos realizar en PrintPoints, los vamos a realizar también en ReprintPoints.

En la tercera línea de PrintPoints encontramos PUSH HL, y esta es la primera línea que vamos a cambiar de lugar, ya que preservamos el valor del registro HL antes de tiempo. Cortamos esta línea y la pegamos tres líneas más abajo, justo antes de cargar la dirección de memoria donde se pintan los puntos del jugador 1 en HL, LD HL, POINTS_P1. El motivo de preservar el valor del registro HL es justamente esta instrucción.

Una vez que llamamos a pintar el punto, recuperamos el valor de HL, POP HL, e incrementamos HL dos veces para apuntarlo a la parte baja de la dirección donde está el segundo dígito. Pues bien, como hemos preservado HL después de posicionarnos en la parte alta de la dirección del primer dígito, ahora vamos a quitar uno de estos dos INC HL; nos acabamos de ahorrar 1 byte y 6 ciclos de reloj.

Esta misma modificación tenemos que hacerla al pintar el marcador del jugador 2 y en la rutina ReprintPoints. En total ahorramos 4 bytes y 24 ciclos de reloj.

Spirax comentó otra optimización que podríamos hacer, con la cual podremos quitar cuatro instrucciones INC L, ahorrando otros 4 bytes y 16 ciclos de reloj.

Tanto en PrintPoints como en ReprintPoints, al dibujar el segundo dígito de los marcadores hacemos los siguiente.

ld hl, POINTS_P1
inc l

ld hl, POINTS_P2
inc l

Como esto lo hacemos tanto en PrintPoints como en ReprintPoints, en realidad hacemos cuatro veces INC L, y lo podemos evitar de la siguiente manera.

ld hl, POINTS_P1 + 1

ld hl, POINTS_P2 + 1

De esta forma apuntamos directamente HL a la posición donde se dibuja el segundo dígito, y nos ahorramos los INC L.

Y ahora nos vamos a ahorrar 25 bytes y 138 ciclos de reloj más, gracias otra vez a Spirax.

En la parte final de la rutina ReprintPoints encontramos la etiqueta reprintPoint2_print, y justo por encima de esta etiqueta la instrucción RET C. Bien, vamos a borrar la etiqueta reprintPoint2_print y todo lo que tiene por debajo hasta el final de la rutina. Después de RET C vamos a incluir JR printPoint2_print.

En una implementación anterior, PrintPoints y ReprintPoints pintaban de distinta manera, pues ReprintPoints hacía OR con los píxeles de la pantalla, pero éste ya no es el caso, por lo que vamos a utilizar el código que pinta el marcador del jugador 2 para repintarlo, y nos vamos a ahorrar 25 bytes y 138 ciclos de reloj.

La etiqueta printPoint2_print no existe, por lo que vamos a incluirla. Buscamos la etiqueta PrintPoints, como vemos primero pinta el marcador del jugador 1, y una vez que ha finaliza pinta el marcador del jugador 2, que empieza justo debajo de la segunda llamada a PrintPoint. Pues es ahí, justo debajo del segundo CALL PrintPoint, donde vamos a añadir la etiqueta printPoint_2_print.

¡Muchas gracias Spirax!

El aspecto final de las rutinas es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- PrintPoints: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call PrintPoint ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P1 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 1 call PrintPoint ; Pinta el segundo dígito del marcador del jugador 1 printPoint_2_print: ; 1er dígito del jugador 2 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call PrintPoint ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ; Spirax ld hl, POINTS_P2 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 2 PrintPoint: ld b, $10 ; Cada dígito son 1 byte por 16 (scanlines) printPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz printPoint_printLoop ; Hasta que B = 0 ret ; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene tercio, línea y scanline de la posición de la bola cp POINTS_Y_B ; Lo compara con el límite inferior del marcador ret nc ; Si no hay acarreo, pasa por debajo y sale ld a, l ; Carga en A la línea y columna de la posición de la bola and $1f ; Se queda con la columna cp POINTS_X1_L ; Lo compara con el límite izquierdo del marcador 1 ret c ; Si hay acarreo, pasa por la izquierda y sale jr z, reprintPoint_1_print ; Si es 0, está justo en el margen izquierdo ; y salta para pintar cp POINTS_X2_R ; Lo compara con el límite derecho de marcador 2 jr z, printPoint_2_print ; Si es 0, está justo en el margen derecho ; y salta para pintar ret nc ; Si no hay acarreo, pasa por la derecha y sale reprintPoint_1: cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1 jr c, reprintPoint_1_print ; Si hay acarreo, pasa por el marcador 1 ; y salta para pintar jr nz, reprintPoint_2 ; Si no es cero, pasa por la derecha ; y salta para comprobar paso por marcador 2 reprintPoint_1_print: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador ; 1er dígito ld e, (hl) ; Carga en E la parte baja de la dirección donde ; está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección donde ; está el primer dígito ld d, (hl) ; y la carga en D push hl ; Preserva el valor de HL ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call PrintPoint ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito inc hl ; Apunta HL a la parte baja de la dirección donde ; está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección donde ; está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 + 1 ; Carga en HL la dirección de memoria donde se pinta ; el segundo dígito de los puntos del jugador 1 jr PrintPoint ; Pinta el segundo dígito del marcado del jugador 2 reprintPoint_2: cp POINTS_X2_L ; Lo compara con el límite derecho de marcador 2 ret c ; Si hay acarreo, pasa por la izquierda y sale ; Spirax jr printPoint_2_print ; Pinta el marcador del jugador

Compilamos, cargamos en el emulador y comprobamos que todo sigue funcionando.

Bug del golpeo de la bola en la parte baja de la pala

Es el momento de arreglar un bug que arrastramos desde que implementamos el cambio de velocidad e inclinación de la bola en base a en que parte de la pala golpea. Cuando la bola golpea en el último scanline de la pala, no cambia inclinación, ni velocidad, ni dirección vertical. ¿A qué se debe?

El motivo está en la forma en la que implementamos la detección de colisiones. Antes de evaluar en que parte de la pala golpea, evaluamos si golpea en la pala, y aquí está el error; cuando golpea en el último scanline de la pala sale de la rutina, con el flag Z activado indicando que hay colisión, pero sin evaluar en qué parte de la pala golpea.

Abrimos el archivo Video.asm y localizamos la etiqueta CheckCrossY. Quince líneas más abajo nos encontramos con esto.

ret nc ; Si no hay acarreo la bola pasa por debajo ; de la pala o colisiona en el último scanline. ; En este último caso ya se ha activado el flag Z

Si leemos atentamente los comentarios, salimos de la rutina si no hay acarreo (flag Z desactivado = no hay colisión). El problema es qué si no hay acarreo, el resultado puede ser mayor o igual a 0. Es decir, si el resultado es 0, salimos de la rutina con el flag Z activado (hay colisión) sin evaluar en que parte de la pala ha golpeado.

Para solucionar este aspecto vamos a hacer una doble comprobación y añadir una nueva etiqueta a la que saltar.

El código actual de la parte que vamos a tocar es el siguiente.

ret nc ; Si no hay acarreo la bola pasa por debajo ; de la pala o colisiona en el último scanline. ; En este último caso ya se ha activado el flag Z ; Dependiendo de donde sea la colisión, se asigna grado de inclinación ; y velocidad a la bola ld a, c ; Carga la posición del penúltimo scanline de la pala en A

Vamos a añadir una línea antes de RET NC y una etiqueta antes de LD A, C, dejando el código de la siguiente manera.

jr z, checkCrossY_eval ; Si es cero, choca en el último scanline ret nc ; Si no hay acarreo la bola pasa por debajo y sale. ; Dependiendo de donde sea la colisión, se asigna grado de inclinación ; y velocidad a la bola checkCrossY_eval: ld a, c ; Carga la posición del penúltimo scanline de la pala en A

Incluso ese JR Z, checkCrossY_eval lo podríamos cambiar por JR Z, checkCrossY_5_5, pues sabemos que ha golpeado en la parte inferior de la pala (probad de las dos maneras).

Compilamos, cargamos en el emulador y comprobamos que hemos arreglado el bug.

Sonido

Y abordamos el penúltimo paso; vamos a implementar efectos de sonido cuando la bola golpea con los laterales, las palas, o cuando se marque algún punto.

Añadimos el archivo Sound.asm, y añadimos las contantes y rutinas necesarias para nuestros efectos de sonido, que van a ser los sonidos que se van a reproducir cuando la bola rebota contra los distintos elementos.

Vamos a definir tres sonidos distintos:

  • Cuando se marca un punto.
  • Cuando la bola choca con una pala.
  • Cuando la bola choca con el borde.

Para cada sonido tenemos que definir la nota y la frecuencia. La frecuencia es el tiempo que va a durar la nota, y la vamos a identificar con el sufijo FQ.

; Punto C_3: EQU $0D07 C_3_FQ: EQU $0082 / $10 ; Pala C_4: EQU $066E C_4_FQ: EQU $0105 / $10 ; Rebote C_5: EQU $0326 C_5_FQ: EQU $020B / $10

Todos los sonidos que vamos a usar son DO, aunque en distintas escalas; a mayor escala, el sonido es más agudo. Las frecuencias especificadas son las que hacen que la nota dure un segundo, es por eso que las dividimos por 16. Si las multiplicáramos por 2, la nota duraría 2 segundos. A cada nota, en cada escala, le corresponde una frecuencia propia.

La siguiente constante que vamos a ver, es la dirección de memoria donde está alojada la rutina BEEPER de la ROM.

BEEPER: EQU $03B5

Esta rutina recibe en HL la nota y en DE la duración, y altera el valor de los registros AF, BC, DE, HL e IX, además de otro aspecto que veremos más adelante. Debido a que la rutina BEEPER de la ROM altera tantos registros, es recomendable no llamarla directamente; vamos a implementar una rutina que lo haga.

La rutina que vamos a implementar, recibe en A el tipo de sonido a emitir, 1 = punto, 2 = pala, 3 = borde, y no altera el valor de ningún registro.

PlaySound: push de push hl

Preservamos el valor de los registros DE, PUSH DE, y HL, PUSH HL.

cp $01 jr z, playSound_point

Comprobamos si el sonido a reproducir es de tipo 1 (punto), CP $01, y de ser así saltamos, JR Z, playSound_point.

cp $02 jr z, playSound_paddle

Si el sonido no es de tipo 1, comprobamos si es de tipo 2 (pala), CP $02, y de ser así saltamos, JR Z, playSound_paddle.

Si el sonido no es de tipo 1, ni de tipo 2, es de tipo 3 (borde).

ld hl, C_5 ld de, C_5_FQ jr beep

Cargamos en HL la nota, LD HL, C_5, cargamos en DE la frecuencia (duración), LD DE, C_5_FQ, y saltamos a reproducir el sonido, JR beep.

Si el sonido es de tipo 1 o 2, hacemos lo mismo, pero con los valores de cada sonido.

playSound_point: ld hl, C_3 ld de, C_3_FQ jr beep playSound_paddle: ld hl, C_4 ld de, C_4_FQ

Nos ahorramos el último JR, ya que justo después viene la rutina que reproduce el sonido.

beep: push af push bc push ix call BEEPER pop ix pop bc pop af pop hl pop de ret

Preservamos los valores de AF, PUSH AF, de BC, PUSH BC, y de IX, PUSH IX. Llamamos a la rutina de la ROM, CALL BEEPER, y recuperamos los valores de IX, POP IX, de BC, POP BC, de AF, POP AF, de HL, POP HL, y de DE, POP DE. Los valores de HL y DE los preservamos al principio de la rutina PlaySound. Por último, salimos, RET.

El aspecto final del archivo Sound.asm, es el siguiente.

; ----------------------------------------------------------------------------- ; Sound ; Fichero con los sonidos ; ----------------------------------------------------------------------------- ; Punto C_3: EQU $0D07 C_3_FQ: EQU $0082 / $10 ; Pala C_4: EQU $066E C_4_FQ: EQU $0105 / $10 ; Rebote C_5: EQU $0326 C_5_FQ: EQU $020B / $10 ; ----------------------------------------------------------------------------- ; Rutina beeper de la ROM. ; ; Entrada: HL = Nota. ; DE = Duración. ; ; Altera el valor de los registros AF, BC, DE, HL e IX. ; ----------------------------------------------------------------------------- BEEPER: EQU $03B5 ; ----------------------------------------------------------------------------- ; Reproduce el sonido de los rebotes. ; Entrada: A = Tipo de rebote. 1. Punto ; 2. Pala ; 3. Borde ; ----------------------------------------------------------------------------- PlaySound: ; Preserva el valor de los registros push de push hl cp $01 ; Evalúa si se emite el sonido de Punto jr z, playSound_point ; Si es así salta cp $02 ; Evalúa si se emite el sonido de Pala jr z, playSound_paddle ; Si es así salta ; Se emite el sonido de Borde ld hl, C_5 ; Carga en HL la nota ld de, C_5_FQ ; Carga en DE la duración (frecuencia) jr beep ; Salta a emitir el sonido ; Se emite el sonido de Punto playSound_point: ld hl, C_3 ; Carga en HL la nota ld de, C_3_FQ ; Carga en DE la duración (frecuencia) jr beep ; Salta a emitir el sonido ; Se emite el sonido de Pala playSound_paddle: ld hl, C_4 ; Carga en HL la nota ld de, C_4_FQ ; Carga en DE la duración (frecuencia) ; Hace sonar la nota beep: ; Preserva el valor de los registros ya que la rutina BEEPER de la ROM los altera push af push bc push ix call BEEPER ; Llama a la rutina BEEPER de la ROM ; Recupera el valor de los registros pop ix pop bc pop af pop hl pop de ret

Para acabar, tenemos que llamar a nuestra nueva rutina para emitir los sonidos de los rebotes de la bola. Abrimos el archivo Game.asm y localizamos la etiqueta checkBallCross_right. Vamos a añadir dos líneas entre la línea RET NZ, y la línea LD A, (ballSetting).

ld a, $02 call PlaySound

Carga el tipo de sonido en A, LD A, $02, y emite el sonido, CALL PlaySound.

Localizamos la etiqueta checkBallCross_left. Vamos a añadir las mismas dos líneas de antes entre la línea RET NZ, y la línea LD A, (ballSetting).

ld a, $02 call PlaySound

Localizamos la etiqueta moveBall_upChg. Justo debajo de la misma, añadimos dos líneas casi iguales a las anteriores.

ld a, $03 call PlaySound

Localizamos la etiqueta moveBall_downChg. Justo debajo de la misma, añadimos las dos líneas anteriores.

ld a, $03 call PlaySound

Localizamos la etiqueta moveBall_rightChg, y justo debajo añadimos.

ld a, $01 call PlaySound

Cinco líneas más abajo localizamos CALL SetBallLeft. Debajo añadimos.

ld a, $03 call PlaySound

Localizamos la etiqueta moveBall_leftChg, y justo debajo añadimos.

ld a, $01 call PlaySound

Cinco líneas más abajo localizamos CALL SetBallRight. Debajo añadimos.

ld a, $03 call PlaySound

Por último, abrimos el archivo Main.asm, localizamos la rutina Loop y justo encima añadimos las siguientes líneas.

ld a, $03 call PlaySound

Nos vamos al final del fichero, y en la parte de los “includes”, incluimos el archivo Sound.asm.

include "Sound.asm"

Si todo ha ido bien, hemos llegado al final. Compilamos, cargamos en el emulador y…

Ensamblador ZX Spectrum - Borde en blancoEnsamblador ZX Spectrum, borde en blanco

¿Qué le pasa al borde? ¿Por qué es blanco? Bueno, ya advertimos que la rutina BEEPER de la ROM altera muchas cosas, y una de ellas es el color del borde, aunque tiene fácil solución.

Por suerte, tenemos una variable de sistema donde podemos guardar el color del borde. En esta variable se guardan también los atributos de la pantalla inferior. El fondo de dicha pantalla es el color del borde.

Abrimos el archivo Video.asm y al inicio del mismo declaramos una constante con la dirección de memoria de dicha variable del sistema.

BORDCR: EQU $5c48

Localizamos la rutina Cls, y antes de la línea LD HL, $5800, añadimos.

ld a, $07 ; Fondo negro, tinta blanca

Modificamos la línea LD (HL), $07 dejándola así.

ld (hl), a

Por último, antes de RET, añadimos.

ld (BORDCR), a

Compilamos, cargamos en el emulador, y ahora sí. ¿Hemos terminado nuestro PorompomPong?

Ensamblador ZX Spectrum - Borde en negroEnsamblador ZX Spectrum, borde en negro
Compatibilidad con 16K

Todavía nos falta una última cosa por hacer. ¿Es compatible nuestro programa con el modelo 16K? Pues todavía no, pero como no trabajamos con interrupciones, es muy sencillo hacerlo compatible.

Vamos a abrir el archivo Main.asm, vamos a localizar las directivas ORG y END, y vamos a sustituir $8000 por $5dad en el caso de ORG. En el caso de END, vamos a sustituir $8000 por Main, que es la etiqueta de entrada al programa. Si ahora compilamos y cargamos en el emulador con el modelo 16K, nuestro programa es compatible.

Si nos fijamos bien, podemos observar que se ha perdido algo de velocidad. Esta pérdida es debida a que los segundos 16 KiB del ZX Spectrum, que es donde ahora cargamos el programa, es lo que se llama memoria contenida, y está compartida con la ULA; cuando la ULA trabaja, todo se para.

Vamos a volver a cambiar la velocidad a la que va la bola.

Abrimos el archivo Sprite.asm, localizamos la etiqueta ballSetting, comentamos la línea db $21 y escribimos justo debajo.

; or $21 or $19

Ahora la bola se inicia a velocidad 3, que va a ser la más lenta.

Abrimos el archivo Game.asm, localizamos la etiqueta SetBallLeft, comentamos la línea 7, y escribimos justo debajo.

; or $21 or $19

Ahora, cuando reiniciamos la bola para que salga por la izquierda de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta SetBallRight, comentamos la línea 7, y escribimos justo debajo.

; db $61 db $59

Ahora, cuando reiniciamos la bola para que salga por la derecha de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta checkCrossY_1_5, comentamos la línea 7, y escribimos justo debajo.

; or $21 or $19

Ahora la velocidad de la bola es 3 en lugar de 4.

Localizamos la etiqueta checkCrossY_2_5, comentamos la línea 7, y escribimos justo debajo.

; or $1a or $12

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_3_5, comentamos la línea 7, y escribimos justo debajo.

; or $17 or $0f

Ahora la velocidad de la bola es 1 en lugar de 2.

Localizamos la etiqueta checkCrossY_4_5, comentamos la línea 7, y escribimos justo debajo.

; or $9a or $92

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_5_5, comentamos la línea 3, y escribimos justo debajo.

; or $a1 or $99

Ahora la velocidad de la bola es 3 en lugar de 4.

Compilamos, cargamos en el emulador y probamos. ¿Hemos terminado?

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, sonido y optimización

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x09 Ensamblador ZX Spectrum Pong – Cambio de dirección/velocidad de la bola al golpear la pala [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong, vamos a prescindir de parte de lo que hemos implementado en la entrega anterior. La velocidad de la bola va a cambiar dependiendo de con qué parte de la pala colisione.

Ensamblador ZX Spectrum – Cambio de dirección/velocidad de la bola al golpear la pala

Creamos la carpeta Paso09 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso08.

Lo primero que vamos a hacer es quitar la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3.

Abrimos el archivo Controls.asm y en la rutina ScanKeys, borramos todas las líneas hasta la etiqueta scanKeys_ctrl, quedando el inicio de la rutina de la siguiente manera.

ScanKeys: ld d, $00 scanKeys_A:

Si compilamos y cargamos en el emulador, vemos que la velocidad de la bola no cambia.

Vamos a añadir nuevas constantes y variables en el archivo Sprite.asm, para poder controlar la inclinación de la bola. También vamos a cambiar los sprites de las palas; ambas van a dibujar cuatro píxeles, pero en ambos casos dibujaremos los más cercanos al centro de la pantalla.

Añadimos las constantes que indican la rotación a asignar a la bola cuando se produce la colisión con la pala.

CROSS_LEFT_ROT: EQU $ff CROSS_RIGHT_ROT: EQU $01

Añadimos la posición inicial de la bola, y el número acumulado de movimientos que debe llevar la bola para cambiar la posición Y. Este último dato lo vamos a usar para cambiar la inclinación de la bola.

BALLPOS_INI: EQU $4850 ballMovCount: db $00

Cambiamos la configuración inicial de la bola y la documentación (comentarios) de la misma.

; Velocidad y dirección de la bola. ; bits 0 a 3: Movimientos de la bola para que cambie la posición Y. ; Valores f = semiplano, 2 = semi diagonal, 1 = diagonal ; bits 4 y 5: Velocidad de la bola: 1 muy rápido, 2 (rápido) y 3 (lento) ; bit 6: Dirección X: 0 derecha / 1 izquierda ; bit 7: Dirección Y: 0 arriba / 1 abajo ballSetting: db $31 ; 0011 0001

Según la nueva configuración, la bola inicialmente se mueve hacia la derecha y hacia arriba, con una velocidad lenta, y en cada movimiento cambia la posición Y (va en diagonal).

Añadimos distintos sprites para las palas y eliminamos el anterior.

PADDLE: EQU $3c ; ¡ELIMINAR! PADDLE1: EQU $0f ; ¡NUEVO! PADDLE2: EQU $f0 ; ¡NUEVO!

Por último, añadimos las posiciones iniciales de las palas.

PADDLE1POS_INI: EQU $4861 PADDLE2POS_INI: EQU $487e

Hemos añadido sprites distintos para cada pala y eliminado la constante que usábamos para pintar las palas; si compilamos, nos dará errores. Vamos a solucionar esos errores modificando la rutina PrintPaddle de Video.asm.

La rutina PrintPaddle recibe en el registro HL la posición de la pala. En el registro C recibirá el sprite de la pala.

Modificamos la línea justo debajo de la etiqueta printPaddle_loop.

ld (hl), PADDLE

Y la dejamos como sigue.

ld (hl), c

Compilamos, y aunque no da ningún error, al cargar en el emulador vemos que los resultados no son los deseados.

Ensamblador ZX Spectrum - No pinta bien las palasEnsamblador ZX Spectrum, no pinta bien las palas

La pala que pinta no se corresponde con el sprite que hemos definido, esto es debido a que no hemos cargado en C el sprite que debe pintar.

Abrimos el archivo Main.asm, y buscamos la etiqueta loop_continue. A partir de la línea 5 es donde imprimimos las palas, cargando el HL la posición de la pala y llamando al pintado de la misma. Antes de llamar al pintado de la pala, debemos especificar qué sprite debe pintar.

Este es el aspecto una vez hecha la modificación.

ld hl, (paddle1pos) ld c, PADDLE1 ; ¡NUEVO! call PrintPaddle ld hl, (paddle2pos) ld c, PADDLE2 ; ¡NUEVO! call PrintPaddle

Compilamos, abrimos en el emulador, y comprobamos que las palas se vuelven a pintar bien.

Ensamblador ZX Spectrum - Pinta bien las palasEnsamblador ZX Spectrum, pinta bien las palas

Aprovechando que estamos en Main.asm, vamos a cambiar un comportamiento del que quizá no os habéis percatado. Cuando se acaba un partido, y al iniciar otro, las palas siguen en la misma posición donde estaban al acabar el partido anterior, y la bola sale desde el campo del jugador que anotó el último punto.

Para modificar este comportamiento, vamos a añadir las siguientes líneas antes de la etiqueta Loop.

ld hl, BALLPOS_INI ld (ballPos), hl ld hl, PADDLE1POS_INI ld (paddle1pos), hl ld hl, PADDLE2POS_INI ld (paddle2pos), hl

Con estas líneas situamos la bola y las palas en sus posiciones iniciales.

Si compilamos, vemos que nos da un error.

ERROR on line 68 of file Main.asm
ERROR: Relative jump out of range

Este error es debido a que, al ir añadiendo líneas, tenemos algún JR que está fuera de rango. JR solo puede saltar 127 bytes hacia adelante o 128 hacia atrás, y tenemos algún JR que salta a alguna dirección fuera de este rango. En concreto, tenemos al final del archivo Main.asm, dos JR Main y un JR Loop. Sustituimos estos tres JR por JP, y solucionamos el error. JP ocupa un byte más que JR, por lo que nuestro programa acaba de crecer 3 bytes, pero hemos reducido 6 ciclos de reloj.

Compilamos, cargamos en el emulador y comprobamos que, al acabar la partida e iniciar otra, tanto la bola como las palas vuelven a su posición inicial.

Cambio de velocidad, inclinación y dirección

Vamos a implementar el cambio de velocidad, inclinación y dirección de la bola al colisionar con las palas.

Abrimos el archivo Game.asm y buscamos la etiqueta checkBallCross_left. Tres líneas por encima encontramos.

ld a, $ff

Modificamos esta línea y la dejamos como sigue.

ld a, CROSS_LEFT_ROT

Buscamos la etiqueta CheckCrossX. Tres líneas por encima encontramos.

ld a, $01

Modificamos esta línea y la dejamos como sigue.

ld a, CROSS_RIGHT_ROT

Hemos cambiado los valores por constantes, para si en un futuro hay que cambiar los valores tenerlos mejor localizados.

El siguiente paso es cambiar la configuración de la bola, dependiendo de en qué parte de la pala colisiona. Vamos a dividir la pala en 5 partes, dependiendo de dónde colisione la bola el comportamiento será como sigue.

Zona de golpeoDirección verticalInclinaciónVelocidad
1/5ArribaDiagonal3 lento
2/5ArribaSemi diagonal2 normal
3/5No cambiaSemi plano1 rápido
4/5AbajoSemi diagonal2 normal
5/5AbajoDiagonal3 lento
Ensamblador ZX Spectrum, comportamiento de la bola

Localizamos la etiqueta CheckCrossY, nos vamos a la penúltima línea, XOR A, e implementamos justo antes de ella.

ld a, c sub $15 ld c, a ld a, b add a, $04 ld b, a

Cuando llegamos a este punto, en C tenemos la posición del penúltimo scanline de la pala, y en B la posición de la bola. Ambas posiciones están en formato TTLLLSSS.

Cargamos en A la posición del penúltimo scanline de la pala, LD A, C, nos posicionamos en el primero, SUB $15, y volvemos a cargar el valor en C, LD C, A. Cargamos en A la posición de la bola, LD A, B, nos posicionamos en la parte baja de la bola, ADD A, $04, y volvemos a cargar el valor en B, LD B, A.

A partir de aquí implementamos el cambio de comportamiento, dependiendo del lugar de colisión de la bola.

checkCrossY_1_5: ld a, c add a, $04 cp b jr c, checkCrossY_2_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la primera parte, ADD A, $04, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_2_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $40 or $31 jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 3 e inclinación diagonal, OR $31. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la primera parte de la pala, comprobamos si lo ha hecho con la segunda.

checkCrossY_2_5: ld a, c add a, $09 cp b jr c, checkCrossY_3_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la segunda parte, ADD A, $09, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_3_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $40 or $22 jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 2 e inclinación semi diagonal, OR $22. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la segunda parte de la pala, comprobamos si lo ha hecho con la tercera.

checkCrossY_3_5: ld a, c add a, $0d cp b jr c, checkCrossY_4_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la tercera parte, ADD A, $0D, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_4_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $c0 or $1f jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal y con la vertical (ya vienen calculadas), AND $C0, y ponemos velocidad 1 e inclinación semi plana, OR $1F. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la tercera parte de la pala, comprobamos si lo ha hecho con la cuarta.

checkCrossY_4_5: ld a, c add a, $11 cp b jr c, checkCrossY_5_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la cuarta parte, ADD A, $11, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_5_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $40 or $a2 jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 2 e inclinación semi diagonal, OR $A2. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la cuarta parte de la pala, lo ha hecho con la quinta.

checkCrossY_5_5: ld a, (ballSetting) and $40 or $b1

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 3 e inclinación diagonal, OR $B1.

Por último, justo por encima de XOR A, vamos a añadir la etiqueta de fin de función a la que hemos estado haciendo referencia, y vamos a cargar la nueva configuración de la bola en memoria.

checkCrossY_end: ld (ballSetting), a

Después de XOR A, vamos a poner el contador de movimientos de la bola a 0.

ld (ballMovCount), a

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Evalúa si la bola colisiona en el eje Y con la pala. ; En el caso de colisionar, actualiza la configuración de la bola. ; Entrada: HL = Posición de la pala ; Salida: Z = Colisiona. ; NZ = No colisiona. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- CheckCrossY: call GetPtrY ; Obtiene la posición vertical de la pala (TTLLLSSS) ; La posición devuelta apunta al primer scanline de la pala que está a 0 ; apunta al siguiente inc a ld c, a ; Carga el valor en C ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene la posición vertical de la bola (TTLLLSSS) ld b, a ; Carga el valor en B ; Comprueba si la bola pasa por encima de la pala ; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0 ; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º add a, $04 ; Apunta la posición de la bola al 5º scanline sub c ; Resta a la posición de la bola, la posición de la pala ret c ; Si hay acarreo sale porque la bola pasa por encima ; Comprueba si la bola pasa por debajo de la pala ld a, c ; Carga la posición vertical de la pala en A add a, $16 ; Le suma 22 para apuntar al penúltimo scanline, ; último que no es 0 ld c, a ; Lo vuelve a cargar en C ld a, b ; Carga la posición vertical de la bola inc a ; Le suma 1 para apuntar el scanline 1, primero que no es 0 sub c ; Resta a la posición de la bola, la posición de la pala ret nc ; Si no hay acarreo la bola pasa por debajo ; de la pala o colisiona en el último scanline. ; En este último caso se activa el flag Z ; Dependiendo de donde sea la colisión, se asigna grado de inclinación ; y velocidad a la bola ld a, c ; Carga la posición del penúltimo scanline de la pala en A sub $15 ; Lo vuelve a posicionar en el primero ld c, a ; Carga el valor en C ld a, b ; Carga en A la posición de la bola add a, $04 ; Se posiciona en la parte baja de la bola ld b, a ; Carga el valor en B checkCrossY_1_5: ld a, c ; Carga la posición vertical de la pala en A add a, $04 ; Se posiciona en el último scanline de 1/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_2_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $31 ; Hacia arriba, velocidad 3 e inclinación diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_2_5: ld a, c ; Carga la posición vertical de la pala en A add a, $09 ; Se posiciona en el último byte de 2/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_3_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $22 ; Hacia arriba, velocidad 2 e inclinación semi diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_3_5: ld a, c ; Carga la posición vertical de la pala en A add a, $0d ; Se posiciona en el último byte de 3/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_4_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $c0 ; Se queda con la dirección horizontal y vertical or $1f ; Hacia arriba/abajo, velocidad 1 e inclinación semi plano jr checkCrossY_end ; Fin de la rutina checkCrossY_4_5: ld a, c ; Carga la posición vertical de la pala en A add a, $11 ; Se posiciona en el último byte de 4/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_5_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal y vertical or $a2 ; Hacia abajo, velocidad 2 e inclinación semi diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_5_5: ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $b1 ; Hacia abajo, velocidad 3 e inclinación diagonal ; Hay colisión checkCrossY_end: ld (ballSetting), a ; Carga en memoria la configuración actual de la bola xor a ; Activa el flag Z y pone A = 0 ld (ballMovCount), a ; Pone el contador de movimientos de la bola a 0 ret

Compilamos, cargamos en el emulador y vemos los resultados.

Vemos que la velocidad sí cambia dependiendo de dónde colisiona la bola, pero no la inclinación. Además, al marcar un tanto, la velocidad no se reinicia, lo cual hace que sea muy difícil seguir jugando si la bola va a la velocidad máxima.

¿Por qué cambia la velocidad, pero no la inclinación?

Si hacemos memoria, en el paso anterior implementamos la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3. De hecho, este paso lo iniciamos avisando de que íbamos a prescindir de esta implementación, pero de lo que no se ha prescindido es del cambio que hicimos en Main.asm para tener en cuenta la velocidad de la bola que marque la configuración; por eso la velocidad cambia.

Nos falta la implementación para tener en cuenta la inclinación, y para que cuando se marca un punto, velocidad e inclinación de la bola se reinicien.

Vamos a empezar con el cambio de inclinación. Seguimos en el archivo Game.asm, implementando la rutina que va a cambiar la posición Y de la bola. La vamos a implementar después del RET de la etiqueta moveBall_end.

MoveBallY: ld a, (ballSetting) and $0f ld d, a

Cargamos en A la configuración de la bola, LD A, (ballSetting), nos quedamos con la inclinación, AND $0F, y cargamos el valor en D, LD A, D.

ld a, (ballMovCount) inc a ld (ballMovCount), a cp d ret nz

Cargamos los movimientos de la bola en A, LD A, (ballMovCount), lo incrementamos en 1, INC A, cargamos el valor en memoria, LD (ballMovCount), A, y lo comparamos con D, que contiene el número de movimientos necesarios para cambiar la posición Y de la bola, CP D. Si no son iguales, no se ha llegado al valor necesario y salimos, RET NZ.

xor a ld (ballMovCount), a ret

Si hemos llegado al valor, ponemos A = 0 y activamos el flag Z, XOR A, ponemos a 0 los movimientos acumulados de la bola, LD (ballMovCount), A, y salimos, RET. Al activar el flag Z se indica, a quien llame, que se debe cambiar la posición Y de la bola.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Cambia la posición Y de la bola ; Altera el valor de los registros AF y D. ; ----------------------------------------------------------------------------- MoveBallY: ld a, (ballSetting) ; Carga en A la configuración de la bola and $0f ; Se queda con la inclinación ld d, a ; Carga el valor en D ld a, (ballMovCount) ; Carga en A los movimientos acumulados de la bola inc a ; Incrementa A ld (ballMovCount), a ; Carga el valor en memoria cp d ; Lo compara con la inclinación ret nz ; Si no son iguales, sale. No se cambia la posición ; La posición debe cambiar xor a ; Pone A = 0 y activa el flag Z ld (ballMovCount), a ; Pone los movimientos acumulados de la bola a 0 ret

Localizamos la etiqueta moveBall_up, y entre las líneas JR Z, moveBall_upChg y CALL PreviousScan, añadimos las siguientes líneas.

call MoveBallY jr nz, moveBall_x

Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.

Localizamos la etiqueta moveBall_down, y entre las líneas JR Z, moveBall_downChg y CALL NextScan, añadimos las siguientes líneas.

call MoveBallY jr nz, moveBall_x

Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.

Compilamos, cargamos en el emulador, y comprobamos que ahora cambian la inclinación y la velocidad.

Por último, vamos a hacer que cuando se marque un punto, se reinicien la velocidad y la inclinación de la bola.

Localizamos la rutina SetBallLeft, eliminamos la línea AND $BF, y la sustituimos por las siguientes.

and $80 or $31

Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la derecha, velocidad 3 e inclinación diagonal, OR $31.

Antes de la instrucción RET, añadimos las siguientes líneas.

ld a, $00 ld (ballMovCount), a

Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.

Localizamos la rutina SetBallRight y eliminamos la línea OR $40 y la sustituimos por las siguientes.

and $80 or $71

Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la izquierda, velocidad 3 e inclinación diagonal, OR $11.

Antes de la instrucción RET, añadimos las siguientes líneas.

ld a, $00 ld (ballMovCount), a

Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.

El aspecto final de ambas rutinas es el siguiente.

; ----------------------------------------------------------------------------- ; Posiciona la bola a la izquierda. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallLeft: ld hl, $4d60 ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $01 ; Carga 1 en A ld (ballRotation), a ; Lo carga en memoria Rotación = 1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $80 ; Se queda con la dirección Y or $31 ; Pone dirección X a derecha, velocidad 3 ; e inclinación diagonal ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $00 ld (ballMovCount), a ret ; ----------------------------------------------------------------------------- ; Posiciona la bola a la derecha. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallRight: ld hl, $4d7e ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $ff ; Carga -1 en A ld (ballRotation), a ; Lo carga en memoria Rotación = -1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $80 ; Se queda con la dirección Y or $71 ; Pone dirección X a izquierda, velocidad 3 ; e inclinación diagonal ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $00 ld (ballMovCount), a ret

Compilamos, cargamos en el emulador y vemos los resultados, que deben ser los esperados, aunque la bola va algo lenta, ¿o no?

¿Os habéis fijado que cuando la bola golpea en la parte más baja de la pala no cambia ni dirección vertical, ni inclinación, ni velocidad? ¿Sabéis a qué se debe? Al final del tutorial veremos el por qué.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x09 Ensamblador ZX Spectrum Pong – Cambio de dirección/velocidad de la bola al golpear la pala [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong, vamos a prescindir de parte de lo que hemos implementado en la entrega anterior. La velocidad de la bola va a cambiar dependiendo de con qué parte de la pala colisione.

Ensamblador ZX Spectrum – Cambio de dirección/velocidad de la bola al golpear la pala

Creamos la carpeta Paso09 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso08.

Lo primero que vamos a hacer es quitar la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3.

Abrimos el archivo Controls.asm y en la rutina ScanKeys, borramos todas las líneas hasta la etiqueta scanKeys_ctrl, quedando el inicio de la rutina de la siguiente manera.

ScanKeys: ld d, $00 scanKeys_A:

Si compilamos y cargamos en el emulador, vemos que la velocidad de la bola no cambia.

Vamos a añadir nuevas constantes y variables en el archivo Sprite.asm, para poder controlar la inclinación de la bola. También vamos a cambiar los sprites de las palas; ambas van a dibujar cuatro píxeles, pero en ambos casos dibujaremos los más cercanos al centro de la pantalla.

Añadimos las constantes que indican la rotación a asignar a la bola cuando se produce la colisión con la pala.

CROSS_LEFT_ROT: EQU $ff CROSS_RIGHT_ROT: EQU $01

Añadimos la posición inicial de la bola, y el número acumulado de movimientos que debe llevar la bola para cambiar la posición Y. Este último dato lo vamos a usar para cambiar la inclinación de la bola.

BALLPOS_INI: EQU $4850 ballMovCount: db $00

Cambiamos la configuración inicial de la bola y la documentación (comentarios) de la misma.

; Velocidad y dirección de la bola. ; bits 0 a 3: Movimientos de la bola para que cambie la posición Y. ; Valores f = semiplano, 2 = semi diagonal, 1 = diagonal ; bits 4 y 5: Velocidad de la bola: 1 muy rápido, 2 (rápido) y 3 (lento) ; bit 6: Dirección X: 0 derecha / 1 izquierda ; bit 7: Dirección Y: 0 arriba / 1 abajo ballSetting: db $31 ; 0011 0001

Según la nueva configuración, la bola inicialmente se mueve hacia la derecha y hacia arriba, con una velocidad lenta, y en cada movimiento cambia la posición Y (va en diagonal).

Añadimos distintos sprites para las palas y eliminamos el anterior.

PADDLE: EQU $3c ; ¡ELIMINAR! PADDLE1: EQU $0f ; ¡NUEVO! PADDLE2: EQU $f0 ; ¡NUEVO!

Por último, añadimos las posiciones iniciales de las palas.

PADDLE1POS_INI: EQU $4861 PADDLE2POS_INI: EQU $487e

Hemos añadido sprites distintos para cada pala y eliminado la constante que usábamos para pintar las palas; si compilamos, nos dará errores. Vamos a solucionar esos errores modificando la rutina PrintPaddle de Video.asm.

La rutina PrintPaddle recibe en el registro HL la posición de la pala. En el registro C recibirá el sprite de la pala.

Modificamos la línea justo debajo de la etiqueta printPaddle_loop.

ld (hl), PADDLE

Y la dejamos como sigue.

ld (hl), c

Compilamos, y aunque no da ningún error, al cargar en el emulador vemos que los resultados no son los deseados.

Ensamblador ZX Spectrum - No pinta bien las palasEnsamblador ZX Spectrum, no pinta bien las palas

La pala que pinta no se corresponde con el sprite que hemos definido, esto es debido a que no hemos cargado en C el sprite que debe pintar.

Abrimos el archivo Main.asm, y buscamos la etiqueta loop_continue. A partir de la línea 5 es donde imprimimos las palas, cargando el HL la posición de la pala y llamando al pintado de la misma. Antes de llamar al pintado de la pala, debemos especificar qué sprite debe pintar.

Este es el aspecto una vez hecha la modificación.

ld hl, (paddle1pos) ld c, PADDLE1 ; ¡NUEVO! call PrintPaddle ld hl, (paddle2pos) ld c, PADDLE2 ; ¡NUEVO! call PrintPaddle

Compilamos, abrimos en el emulador, y comprobamos que las palas se vuelven a pintar bien.

Ensamblador ZX Spectrum - Pinta bien las palasEnsamblador ZX Spectrum, pinta bien las palas

Aprovechando que estamos en Main.asm, vamos a cambiar un comportamiento del que quizá no os habéis percatado. Cuando se acaba un partido, y al iniciar otro, las palas siguen en la misma posición donde estaban al acabar el partido anterior, y la bola sale desde el campo del jugador que anotó el último punto.

Para modificar este comportamiento, vamos a añadir las siguientes líneas antes de la etiqueta Loop.

ld hl, BALLPOS_INI ld (ballPos), hl ld hl, PADDLE1POS_INI ld (paddle1pos), hl ld hl, PADDLE2POS_INI ld (paddle2pos), hl

Con estas líneas situamos la bola y las palas en sus posiciones iniciales.

Si compilamos, vemos que nos da un error.

ERROR on line 68 of file Main.asm
ERROR: Relative jump out of range

Este error es debido a que, al ir añadiendo líneas, tenemos algún JR que está fuera de rango. JR solo puede saltar 127 bytes hacia adelante o 128 hacia atrás, y tenemos algún JR que salta a alguna dirección fuera de este rango. En concreto, tenemos al final del archivo Main.asm, dos JR Main y un JR Loop. Sustituimos estos tres JR por JP, y solucionamos el error. JP ocupa un byte más que JR, por lo que nuestro programa acaba de crecer 3 bytes, pero hemos reducido 6 ciclos de reloj.

Compilamos, cargamos en el emulador y comprobamos que, al acabar la partida e iniciar otra, tanto la bola como las palas vuelven a su posición inicial.

Cambio de velocidad, inclinación y dirección

Vamos a implementar el cambio de velocidad, inclinación y dirección de la bola al colisionar con las palas.

Abrimos el archivo Game.asm y buscamos la etiqueta checkBallCross_left. Tres líneas por encima encontramos.

ld a, $ff

Modificamos esta línea y la dejamos como sigue.

ld a, CROSS_LEFT_ROT

Buscamos la etiqueta CheckCrossX. Tres líneas por encima encontramos.

ld a, $01

Modificamos esta línea y la dejamos como sigue.

ld a, CROSS_RIGHT_ROT

Hemos cambiado los valores por constantes, para si en un futuro hay que cambiar los valores tenerlos mejor localizados.

El siguiente paso es cambiar la configuración de la bola, dependiendo de en qué parte de la pala colisiona. Vamos a dividir la pala en 5 partes, dependiendo de dónde colisione la bola el comportamiento será como sigue.

Zona de golpeoDirección verticalInclinaciónVelocidad
1/5ArribaDiagonal3 lento
2/5ArribaSemi diagonal2 normal
3/5No cambiaSemi plano1 rápido
4/5AbajoSemi diagonal2 normal
5/5AbajoDiagonal3 lento
Ensamblador ZX Spectrum, comportamiento de la bola

Localizamos la etiqueta CheckCrossY, nos vamos a la penúltima línea, XOR A, e implementamos justo antes de ella.

ld a, c sub $15 ld c, a ld a, b add a, $04 ld b, a

Cuando llegamos a este punto, en C tenemos la posición del penúltimo scanline de la pala, y en B la posición de la bola. Ambas posiciones están en formato TTLLLSSS.

Cargamos en A la posición del penúltimo scanline de la pala, LD A, C, nos posicionamos en el primero, SUB $15, y volvemos a cargar el valor en C, LD C, A. Cargamos en A la posición de la bola, LD A, B, nos posicionamos en la parte baja de la bola, ADD A, $04, y volvemos a cargar el valor en B, LD B, A.

A partir de aquí implementamos el cambio de comportamiento, dependiendo del lugar de colisión de la bola.

checkCrossY_1_5: ld a, c add a, $04 cp b jr c, checkCrossY_2_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la primera parte, ADD A, $04, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_2_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $40 or $31 jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 3 e inclinación diagonal, OR $31. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la primera parte de la pala, comprobamos si lo ha hecho con la segunda.

checkCrossY_2_5: ld a, c add a, $09 cp b jr c, checkCrossY_3_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la segunda parte, ADD A, $09, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_3_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $40 or $22 jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 2 e inclinación semi diagonal, OR $22. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la segunda parte de la pala, comprobamos si lo ha hecho con la tercera.

checkCrossY_3_5: ld a, c add a, $0d cp b jr c, checkCrossY_4_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la tercera parte, ADD A, $0D, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_4_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $c0 or $1f jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal y con la vertical (ya vienen calculadas), AND $C0, y ponemos velocidad 1 e inclinación semi plana, OR $1F. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la tercera parte de la pala, comprobamos si lo ha hecho con la cuarta.

checkCrossY_4_5: ld a, c add a, $11 cp b jr c, checkCrossY_5_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la cuarta parte, ADD A, $11, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_5_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.

ld a, (ballSetting) and $40 or $a2 jr checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 2 e inclinación semi diagonal, OR $A2. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la cuarta parte de la pala, lo ha hecho con la quinta.

checkCrossY_5_5: ld a, (ballSetting) and $40 or $b1

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 3 e inclinación diagonal, OR $B1.

Por último, justo por encima de XOR A, vamos a añadir la etiqueta de fin de función a la que hemos estado haciendo referencia, y vamos a cargar la nueva configuración de la bola en memoria.

checkCrossY_end: ld (ballSetting), a

Después de XOR A, vamos a poner el contador de movimientos de la bola a 0.

ld (ballMovCount), a

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Evalúa si la bola colisiona en el eje Y con la pala. ; En el caso de colisionar, actualiza la configuración de la bola. ; Entrada: HL = Posición de la pala ; Salida: Z = Colisiona. ; NZ = No colisiona. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- CheckCrossY: call GetPtrY ; Obtiene la posición vertical de la pala (TTLLLSSS) ; La posición devuelta apunta al primer scanline de la pala que está a 0 ; apunta al siguiente inc a ld c, a ; Carga el valor en C ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene la posición vertical de la bola (TTLLLSSS) ld b, a ; Carga el valor en B ; Comprueba si la bola pasa por encima de la pala ; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0 ; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º add a, $04 ; Apunta la posición de la bola al 5º scanline sub c ; Resta a la posición de la bola, la posición de la pala ret c ; Si hay acarreo sale porque la bola pasa por encima ; Comprueba si la bola pasa por debajo de la pala ld a, c ; Carga la posición vertical de la pala en A add a, $16 ; Le suma 22 para apuntar al penúltimo scanline, ; último que no es 0 ld c, a ; Lo vuelve a cargar en C ld a, b ; Carga la posición vertical de la bola inc a ; Le suma 1 para apuntar el scanline 1, primero que no es 0 sub c ; Resta a la posición de la bola, la posición de la pala ret nc ; Si no hay acarreo la bola pasa por debajo ; de la pala o colisiona en el último scanline. ; En este último caso se activa el flag Z ; Dependiendo de donde sea la colisión, se asigna grado de inclinación ; y velocidad a la bola ld a, c ; Carga la posición del penúltimo scanline de la pala en A sub $15 ; Lo vuelve a posicionar en el primero ld c, a ; Carga el valor en C ld a, b ; Carga en A la posición de la bola add a, $04 ; Se posiciona en la parte baja de la bola ld b, a ; Carga el valor en B checkCrossY_1_5: ld a, c ; Carga la posición vertical de la pala en A add a, $04 ; Se posiciona en el último scanline de 1/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_2_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $31 ; Hacia arriba, velocidad 3 e inclinación diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_2_5: ld a, c ; Carga la posición vertical de la pala en A add a, $09 ; Se posiciona en el último byte de 2/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_3_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $22 ; Hacia arriba, velocidad 2 e inclinación semi diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_3_5: ld a, c ; Carga la posición vertical de la pala en A add a, $0d ; Se posiciona en el último byte de 3/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_4_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $c0 ; Se queda con la dirección horizontal y vertical or $1f ; Hacia arriba/abajo, velocidad 1 e inclinación semi plano jr checkCrossY_end ; Fin de la rutina checkCrossY_4_5: ld a, c ; Carga la posición vertical de la pala en A add a, $11 ; Se posiciona en el último byte de 4/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_5_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal y vertical or $a2 ; Hacia abajo, velocidad 2 e inclinación semi diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_5_5: ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $b1 ; Hacia abajo, velocidad 3 e inclinación diagonal ; Hay colisión checkCrossY_end: ld (ballSetting), a ; Carga en memoria la configuración actual de la bola xor a ; Activa el flag Z y pone A = 0 ld (ballMovCount), a ; Pone el contador de movimientos de la bola a 0 ret

Compilamos, cargamos en el emulador y vemos los resultados.

Vemos que la velocidad sí cambia dependiendo de dónde colisiona la bola, pero no la inclinación. Además, al marcar un tanto, la velocidad no se reinicia, lo cual hace que sea muy difícil seguir jugando si la bola va a la velocidad máxima.

¿Por qué cambia la velocidad, pero no la inclinación?

Si hacemos memoria, en el paso anterior implementamos la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3. De hecho, este paso lo iniciamos avisando de que íbamos a prescindir de esta implementación, pero de lo que no se ha prescindido es del cambio que hicimos en Main.asm para tener en cuenta la velocidad de la bola que marque la configuración; por eso la velocidad cambia.

Nos falta la implementación para tener en cuenta la inclinación, y para que cuando se marca un punto, velocidad e inclinación de la bola se reinicien.

Vamos a empezar con el cambio de inclinación. Seguimos en el archivo Game.asm, implementando la rutina que va a cambiar la posición Y de la bola. La vamos a implementar después del RET de la etiqueta moveBall_end.

MoveBallY: ld a, (ballSetting) and $0f ld d, a

Cargamos en A la configuración de la bola, LD A, (ballSetting), nos quedamos con la inclinación, AND $0F, y cargamos el valor en D, LD A, D.

ld a, (ballMovCount) inc a ld (ballMovCount), a cp d ret nz

Cargamos los movimientos de la bola en A, LD A, (ballMovCount), lo incrementamos en 1, INC A, cargamos el valor en memoria, LD (ballMovCount), A, y lo comparamos con D, que contiene el número de movimientos necesarios para cambiar la posición Y de la bola, CP D. Si no son iguales, no se ha llegado al valor necesario y salimos, RET NZ.

xor a ld (ballMovCount), a ret

Si hemos llegado al valor, ponemos A = 0 y activamos el flag Z, XOR A, ponemos a 0 los movimientos acumulados de la bola, LD (ballMovCount), A, y salimos, RET. Al activar el flag Z se indica, a quien llame, que se debe cambiar la posición Y de la bola.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Cambia la posición Y de la bola ; Altera el valor de los registros AF y D. ; ----------------------------------------------------------------------------- MoveBallY: ld a, (ballSetting) ; Carga en A la configuración de la bola and $0f ; Se queda con la inclinación ld d, a ; Carga el valor en D ld a, (ballMovCount) ; Carga en A los movimientos acumulados de la bola inc a ; Incrementa A ld (ballMovCount), a ; Carga el valor en memoria cp d ; Lo compara con la inclinación ret nz ; Si no son iguales, sale. No se cambia la posición ; La posición debe cambiar xor a ; Pone A = 0 y activa el flag Z ld (ballMovCount), a ; Pone los movimientos acumulados de la bola a 0 ret

Localizamos la etiqueta moveBall_up, y entre las líneas JR Z, moveBall_upChg y CALL PreviousScan, añadimos las siguientes líneas.

call MoveBallY jr nz, moveBall_x

Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.

Localizamos la etiqueta moveBall_down, y entre las líneas JR Z, moveBall_downChg y CALL NextScan, añadimos las siguientes líneas.

call MoveBallY jr nz, moveBall_x

Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.

Compilamos, cargamos en el emulador, y comprobamos que ahora cambian la inclinación y la velocidad.

Por último, vamos a hacer que cuando se marque un punto, se reinicien la velocidad y la inclinación de la bola.

Localizamos la rutina SetBallLeft, eliminamos la línea AND $BF, y la sustituimos por las siguientes.

and $80 or $31

Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la derecha, velocidad 3 e inclinación diagonal, OR $31.

Antes de la instrucción RET, añadimos las siguientes líneas.

ld a, $00 ld (ballMovCount), a

Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.

Localizamos la rutina SetBallRight y eliminamos la línea OR $40 y la sustituimos por las siguientes.

and $80 or $71

Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la izquierda, velocidad 3 e inclinación diagonal, OR $11.

Antes de la instrucción RET, añadimos las siguientes líneas.

ld a, $00 ld (ballMovCount), a

Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.

El aspecto final de ambas rutinas es el siguiente.

; ----------------------------------------------------------------------------- ; Posiciona la bola a la izquierda. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallLeft: ld hl, $4d60 ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $01 ; Carga 1 en A ld (ballRotation), a ; Lo carga en memoria Rotación = 1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $80 ; Se queda con la dirección Y or $31 ; Pone dirección X a derecha, velocidad 3 ; e inclinación diagonal ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $00 ld (ballMovCount), a ret ; ----------------------------------------------------------------------------- ; Posiciona la bola a la derecha. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallRight: ld hl, $4d7e ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $ff ; Carga -1 en A ld (ballRotation), a ; Lo carga en memoria Rotación = -1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $80 ; Se queda con la dirección Y or $71 ; Pone dirección X a izquierda, velocidad 3 ; e inclinación diagonal ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $00 ld (ballMovCount), a ret

Compilamos, cargamos en el emulador y vemos los resultados, que deben ser los esperados, aunque la bola va algo lenta, ¿o no?

¿Os habéis fijado que cuando la bola golpea en la parte más baja de la pala no cambia ni dirección vertical, ni inclinación, ni velocidad? ¿Sabéis a qué se debe? Al final del tutorial veremos el por qué.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x08 Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a implementar la partida a dos jugadores, con marcador, y la posibilidad de cambiar la velocidad de la bola.

Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola

Creamos la carpeta Paso08 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso07.

Vamos a empezar por el marcador, definiendo la posición donde vamos a pintar la puntuación, y definiendo también los sprites necesarios en el archivo Sprite.asm.

Marcadores
POINTS_P1: EQU $450d POINTS_P2: EQU $4511

Cada dígito de los marcadores ocupa 8×16 píxeles, o lo que es lo mismo, un carácter de ancho por dos de alto (1 byte x 16 bytes/scanlines).

Blanco_sprite: ds $10 ; 16 espacios = 16 bytes a $00 Cero_sprite: db $00, $7e, $7e, $66, $66, $66, $66, $66 db $66, $66, $66, $66, $66, $7e, $7e, $00 Uno_sprite: db $00, $18, $18, $18, $18, $18, $18, $18 db $18, $18, $18, $18, $18, $18, $18, $00 Dos_sprite: db $00, $7e, $7e, $06, $06, $06, $06, $7e db $7e, $60, $60, $60, $60, $7e, $7e, $00 Tres_sprite: db $00, $7e, $7e, $06, $06, $06, $06, $3e db $3e, $06, $06, $06, $06, $7e, $7e, $00 Cuatro_sprite: db $00, $66, $66, $66, $66, $66, $66, $7e db $7e, $06, $06, $06, $06, $06, $06, $00 Cinco_sprite: db $00, $7e, $7e, $60, $60, $60, $60, $7e db $7e, $06, $06, $06, $06, $7e, $7e, $00 Seis_sprite: db $00, $7e, $7e, $60, $60, $60, $60, $7e db $7e, $66, $66, $66, $66, $7e, $7e, $00 Siete_sprite: db $00, $7e, $7e, $06, $06, $06, $06, $06 db $06, $06, $06, $06, $06, $06, $06, $00 Ocho_sprite: db $00, $7e, $7e, $66, $66, $66, $66, $7e db $7e, $66, $66, $66, $66, $7e, $7e, $00 Nueve_sprite: db $00, $7e, $7e, $66, $66, $66, $66, $7e db $7e, $06, $06, $06, $06, $7e, $7e, $00

Una vez que hemos definido los sprites, definimos la composición de los números haciendo referencia a las etiquetas de los sprites.

Cero: dw Blanco_sprite, Cero_sprite Uno: dw Blanco_sprite, Uno_sprite Dos: dw Blanco_sprite, Dos_sprite Tres: dw Blanco_sprite, Tres_sprite Cuatro: dw Blanco_sprite, Cuatro_sprite Cinco: dw Blanco_sprite, Cinco_sprite Seis: dw Blanco_sprite, Seis_sprite Siete: dw Blanco_sprite, Siete_sprite Ocho: dw Blanco_sprite, Ocho_sprite Nueve: dw Blanco_sprite, Nueve_sprite Diez: dw Uno_sprite, Cero_sprite Once: dw Uno_sprite, Uno_sprite Doce: dw Uno_sprite, Dos_sprite Trece: dw Uno_sprite, Tres_sprite Catorce: dw Uno_sprite, Cuatro_sprite Quince: dw Uno_sprite, Cinco_sprite

Ahora necesitamos definir el lugar donde vamos a guardar la puntuación de cada jugador. Abrimos el archivo Main.asm y añadimos las siguientes variables antes de END $8000.

p1points: db $00 p2points: db $00

Ya tenemos todo listo para empezar a implementar el marcador.

Lo primero que tenemos que saber es que sprite tenemos que pintar, dependiendo del marcador de cada jugador. Para saber qué sprite pintar, vamos a implementar una rutina que recibe en A la puntuación, y devuelve en HL la dirección del sprite a pintar.

Abrimos el archivo Video.asm e implementamos justo antes de la rutina NextScan.

GetPointSprite: ld hl, Cero ld bc, $04 inc a

Cargamos en HL la dirección del sprite para el cero, LD HL, Cero. Como cada sprite está a 4 bytes del anterior, cargamos este desplazamiento en BC, LD BC, $04, e incrementamos A para que el bucle no empiece en 0, INC A, en el caso de que la puntuación sea 0.

Ahora hacemos un bucle para que HL apunte al sprite correcto.

getPointSprite_loop: dec a ret z add hl, bc jr getPointSprite_loop

Decrementamos A, DEC A, y si hemos llegado a 0, HL ya apunta al sprite correcto y salimos, RET Z. Si todavía no hemos llegado a 0, sumamos el desplazamiento a HL, ADD HL, BC, y volvemos a ejecutar el bucle, JR getPointSprite_loop.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Obtiene el sprite correspondiente a pintar en el marcador. ; Entrada: A = puntuación. ; Salida: HL = Dirección del sprite a pintar. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- GetPointSprite: ld hl, Cero ; Carga en HL la dirección del sprite del 0 ld bc, $04 ; Cada sprite está del anterior a 4 bytes inc a ; Incrementa A para que el inicio del bucle no sea 0 getPointSprite_loop: dec a ; Decrementa A ret z ; Si ha llegado a 0, fin de rutina add hl, bc ; Suma 4 a la dirección del sprite; siguiente sprite jr getPointSprite_loop ; Bucle hasta que A = 0 ret

Y ahora vamos a implementar la rutina que pinta los marcadores, al final del archivo Video.asm.

PrintPoints: ld a, (p1points) call GetPointSprite

Cargamos la puntuación del jugador 1 en A, LD A, (p1points), y obtenemos la dirección de memoria donde está la definición del sprite correspondiente a dicha puntuación, CALL GetPointSprite.

GetPointSprite nos devuelve en HL la dirección de memoria donde está definido el sprite. Si la puntuación es cero, HL nos traerá la dirección de memoria donde está definida la etiqueta Cero, cuya definición es la siguiente.

Cero:
dw Blanco_sprite, Cero_sprite

Como podemos ver, Cero está definido por otras dos direcciones de memoria: la primera es la dirección de memoria donde está definido el sprite blanco, usado para justificar a dos dígitos, y la segunda es la dirección de memoria donde está definido el sprite del cero.

Si las direcciones de memoria fueran las siguientes.

$9000 Blanco_sprite
$9020 Cero_sprite
$9040 Cero

La definición de la etiqueta Cero, una vez que se sustituyen las etiquetas Blanco_sprite y Cero_sprite por las direcciones de memoria donde están definidas, sería.

Cero:
dw $9000, $9020

El valor que tendría HL tras llamar a GetPointSprite con el marcador a 0 sería $9040, o lo que es lo mismo, la dirección de memoria donde se define la etiqueta Cero.

Como el Z80 es Little Endian, los valores de las direcciones de memoria desde $9040 en adelante serían-

$9040$00
$9041$90
$9042$20
$9043$90

O lo que es lo mismo, las direcciones de memoria donde están definidos los sprites para Blanco_sprite y para Cero_sprite.

Esta explicación es necesaria para entender el funcionamiento del resto de la rutina.

push hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P1 call printPoint_print

Vamos a pintar el primer dígito del marcador del jugador 1. Preservamos el valor de HL, que apunta al sprite del marcador que tenemos que pintar, PUSH HL, cargamos en E la parte baja de la dirección donde está el sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).

Cargamos en HL la dirección de memoria de pantalla donde se pinta el primer dígito del marcador del jugador 1, LD HL, POINTS_P1, y llamamos al pintado del dígito, CALL printPoint_print.

Ahora pintamos el segundo dígito del marcador del jugador 1.

pop hl inc hl inc hl

Recuperamos el valor de HL, POP HL, y lo apuntamos a la parte baja de la dirección donde está definido el sprite del segundo dígito, INC HL INC HL.

ld e, (hl) inc hl ld d, (hl)

Cargamos la parte baja de dicha dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).

ld hl, POINTS_P1 inc l call printPoint_print

Por último, cargamos en HL la posición de memoria de la pantalla donde se pinta el marcador del jugador 1, LD HL, POINTS_P1. Como cada dígito ocupa 1 byte (columna) de ancho, situamos HL en la columna dónde se pinta el segundo dígito, INC L, y lo pintamos.

La forma de pintar el marcador del jugador 2 es casi igual a la del jugador 1, por lo que mostramos el código marcando los cambios y sin entrar en detalle.

ld a, (p2points) ; !CAMBIO! call GetPointSprite push hl ; 1er dígito ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; !CAMBIO! call printPoint_print pop hl ; 2º dígito inc hl inc hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; !CAMBIO! inc l

Como se puede observar, los cambios son pocos. Se ha quitado la última línea al no ser necesario llamar a pintar el segundo dígito del jugador 2, ya que lo vamos a implementar a continuación del último INC L.

Recordemos que cada dígito ocupa 8×16 píxeles (una columna x 16 scanlines).

printPoint_print: ld b, $10 push de push hl

Cargamos en B el número de scanlines que vamos a pintar, LD B, $10, y preservamos el valor del registro DE, PUSH DE, y de HL, PUSH HL.

printPoint_printLoop: ld a, (de) ld (hl), a inc de call NextScan djnz printPoint_printLoop

Cargamos en A el byte a pintar, LD A, (DE), y lo pintamos en pantalla, LD (HL), A. Apuntamos DE al siguiente byte a pintar, obtenemos la dirección del siguiente scanline, CALL NextScan, y repetimos la operación hasta que B sea 0 y hayamos pintado los 16 scanlines, DJNZ printPoint_printLoop.

Para finalizar, recuperamos los valores de HL y DE y salimos.

pop hl pop de ret

El aspecto final de la rutina de pintado del marcador es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- PrintPoints: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call printPoint_print ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito call printPoint_print ; Pinta el segundo dígito del marcador del jugador 1 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 2 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call printPoint_print ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito ; Pinta el segundo dígito del marcador del jugador 2 printPoint_print: ld b, $10 ; Cada dígito son 1 byte por 16 (scanlines) push de ; Preserva el valor de DE push hl ; Preserva el valor de HL printPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz printPoint_printLoop ; Hasta que B = 0 pop hl ; Recupera el valor de HL pop de ; Recupera el valor de DE ret

En esta rutina es sencillo ahorrar 12 ciclos de reloj y 2 bytes. Para ello hay que cambiar dos instrucciones de lugar, lo que nos permite quitar otras dos; lo veremos al final del tutorial.

Y ahora solo nos queda ver si lo que hemos implementado funciona. Abrimos Main.asm, y debajo de la llamada a PrintBorder, justo antes de Loop, añadimos la siguiente línea.

call PrintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - Bola borra marcadorEnsamblador ZX Spectrum, bola borra marcador

En principio todo va bien, pero según se va moviendo la bola vemos que volvemos a tener un problema, viejo conocido nuestro, y es que la bola borra el marcador a su paso, cosa que vamos a solucionar a continuación.

Para evitar que la bola borre el marcador, hacemos lo mismo que hicimos con la línea central, vamos a repintar el marcador. Implementamos la rutina al final del archivo Video.asm.

En realidad, la rutina de repintado del marcador es prácticamente igual que la de pintado, cambiando el nombre de las etiquetas y añadiendo una línea. Vamos a copiar toda la rutina de pintado de marcador y la vamos a pegar al final del archivo Video.asm. Cambiamos los nombres de las etiquetas y añadimos una línea.

A continuación, mostramos el aspecto final, marcando los cambios producidos con respecto a la rutina de pintado del marcador.

; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 bytes de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call reprintPoint_print ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito call reprintPoint_print ; Pinta el segundo dígito del marcador del jugador 1 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 2 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call reprintPoint_print ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito ; Pinta el segundo dígito del marcador del jugador 2 reprintPoint_print: ld b, $10 ; Cada dígito es de 1 byte por 16 (scanlines) push de push hl ; Preserva el valor de los registros DE y HL reprintPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar or (hl) ; Lo mezcla con lo que hay pintado en pantalla ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz reprintPoint_printLoop ; Hasta que B = 0 pop hl pop de ; Recupera el valor de los registros HL y DE ret

Vamos a explicar la línea que hemos añadido.

ld a, (de)
or (hl)
ld (hl), a

Lo que hacemos con OR (HL) es agregar los píxeles que hay en pantalla a los píxeles del sprite del número. De esta manera repintamos el número sin borrar la bola.

Ahora queda ver si funciona. Abrimos el archivo Main.asm y añadimos la siguiente línea después de la llamada a ReprintLine.

call ReprintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - Bola no borra marcadorEnsamblador ZX Spectrum, bola no borra marcador

Efectivamente, hemos solucionado un problema, pero ha surgido otro. El marcador ya no se borra, pero la bola va muy lenta. Por suerte la solución es sencilla, ya que la velocidad de la bola es una de las cosas que controlamos nosotros.

Como recordaréis, la bola se mueve una de cada seis iteraciones del bucle principal, por lo que lo único que tenemos que hacer es reducir este intervalo en Main.asm, por ejemplo a dos.

ld (countLoopBall), a cp $02 ; ¡CAMBIO! jr nz, loop_paddle

Compilamos, cargamos en el emulador y comprobamos que la velocidad de la bola ha aumentado.

Cambio de velocidad de la bola

Como recodaremos, en la variable ballSetting definimos la velocidad de la bola en los bits 4 y 5, pudiendo ser 1 la más rápida y 3 la más lenta. Vamos a utilizar este aspecto para definir y modificar la velocidad de la bola.

Lo primero es modificar el valor inicial de esta variable.

ballSetting: db $20

De esta manera el valor inicial es:

  • Dirección vertical hacia arriba.
  • Dirección horizontal hacia la derecha.
  • Velocidad de la bola 2.

Y ahora vamos a usar este valor para controlar el intervalo para mover la bola. Abrimos Main.asm, localizamos la etiqueta Loop, y añadimos justo debajo.

ld a, (ballSetting) rrca rrca rrca rrca and $03 ld b, a

Cargamos la configuración de la bola en A, LD A, (ballSetting), pasamos el valor de los bits 4 y 5 a los bits 0 y 1, RRCA RRCA RRCA RRCA, nos quedamos con el valor de los bits 0 y 1 (velocidad de la bola), AND $03, y cargamos el valor en B, LD B, A.

Cuatro líneas más abajo, cambiamos la línea CP $02.

cp b

Compilamos y comprobamos que todo sigue funcionando igual. La única diferencia es que ahora la velocidad de la bola la tomamos desde la configuración de la misma, y podremos cambiarla.

Para cambiar la velocidad de la bola, vamos a usar las teclas del 1 al 3. Abrimos el archivo Controls.asm y empezamos a escribir tras la etiqueta ScanKeys.

scanKeys_speed: ld a, $00 ld (countLoopBall), a scanKeys_ctrl:

Si se ha pulsado alguna de las teclas de cambio de velocidad, hay que poner a 0 el contador de vueltas de bucle para pintar la bola, de lo contrario, si el contador está en 2 y ponemos la velocidad a 1, habrá que esperar 254 iteraciones hasta que la bola se vuelva a mover.

Ponemos A = 0, LD A, $00, y ponemos el contador de iteraciones para la bola a 0, LD (countLoopBall), A.

La etiqueta scanKeys_ctrl marca el punto donde empieza la rutina tal y como la tenemos ahora. La nueva implementación la vamos a hacer entre las etiquetas ScanKeys y scanKeys_speed.

ld a, $f7 in a, ($fe)

Cargamos la semifila 1-5 en A, LD A, $F7, y leemos del puerto del teclado, IN A, ($FE).

bit $00, a jr nz, scanKeys_2

Comprobamos si se ha pulsado el 1, BIT $00, A, y en caso de no haberlo pulsado saltamos a comprobar si se ha pulsado el 2, JR NZ, scanKeys_2.

Si se ha pulsado el 1, cambiamos la velocidad de la bola.

ld a, (ballSetting) and $cf or $10 ld (ballSetting), a jr scanKeys_speed

Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos los bits de la velocidad a 0, AND $CF, ponemos la velocidad a 1, OR $10, cargamos la configuración en memoria, LD (ballSetting), A, y saltamos a poner a 0 el contador de iteraciones para la bola, JR scanKeys_speed.

La comprobación para el 2 y el 3 es muy parecida a la comprobación del 1, por lo que vemos el código completo y marcamos las diferencias.

scanKeys_2: bit $01, a ; ¡CAMBIO! jr nz, scanKeys_3 ; ¡CAMBIO! ld a, (ballSetting) and $cf or $20 ; ¡CAMBIO! ld (ballSetting), a jr scanKeys_speed scanKeys_3: bit $02, a ; ¡CAMBIO! jr nz, scanKeys_ctrl ; ¡CAMBIO! ld a, (ballSetting) and $cf ; ¡BORRAR! or $30 ; ¡CAMBIO! ld (ballSetting), a

El aspecto final de la rutina, una vez modificada, queda de la siguiente manera.

; ----------------------------------------------------------------------------- ; ScanKeys ; Escanea las teclas de control y devuelve las pulsadas. ; Salida: D = Teclas pulsadas. ; Bit 0 = A pulsada 0/1. ; Bit 1 = Z pulsada 0/1. ; Bit 2 = 0 pulsada 0/1. ; Bit 3 = O pulsada 0/1. ; Altera el valor de los registros AF y D. ; ----------------------------------------------------------------------------- ScanKeys: ld a, $f7 ; Carga en A la semifila 1-5 in a, ($fe) ; Lee el estado de la semifila bit $00, a ; Comprueba si se ha pulsado el 1 jr nz, scanKeys_2 ; Si no se ha pulsado salta ; Se ha pulsado; cambia la velocidad de la bola 1 (rápido) ld a, (ballSetting) ; Carga la configuración de la bola en A and $cf ; Pone los bits de velocidad a 0 or $10 ; Pone los bits de velocidad a 1 ld (ballSetting), a ; Carga el valor en memoria jr scanKeys_speed ; Salta para comprobar los controles scanKeys_2: bit $01, a ; Comprueba si se ha pulsado el 2 jr nz, scanKeys_3 ; Si no se ha pulsado salta ; Se ha pulsado; cambia la velocidad de la bola 2 (medio) ld a, (ballSetting) ; Carga la configuración de la bola en A and $cf ; Pone los bits de velocidad a 0 or $20 ; Pone los bits de velocidad a 2 ld (ballSetting), a ; Carga el valor en memoria jr scanKeys_speed ; Salta para comprobar los controles scanKeys_3: bit $02, a ; Comprueba si se ha pulsado el 3 jr nz, scanKeys_ctrl ; Si no se ha pulsado salta ; Se ha pulsado; cambia la velocidad de la bola 3 (lento) ld a, (ballSetting) ; Carga la configuración de la bola en A or $30 ; Pone los bits de velocidad a 3 ld (ballSetting), a ; Carga el valor en memoria scanKeys_speed: ld a, $00 ; Pone A = 0 ld ( countLoopBall), a ; Pone el contador de iteraciones para la bola a 0 scanKeys_ctrl: ld d, $00 ; Pone el registro D a 0. ; Resto de la rutina desde ScanKeys_A

Es el momento de compilar y cargar en el emulador para comprobar cómo se comporta esta modificación. Si todo ha ido bien, podemos cambiar la velocidad de la bola pulsando las teclas 1, 2 y 3.

Puntuación

Lo último que tenemos que hacer es contabilizar los puntos de cada jugador, para lo cual vamos a modificar la rutina MoveBall, en concreto moveBall_rightChg y moveBall_leftChg. Estas rutinas se encargan de cambiar la dirección de la bola cuando llega al límite izquierdo o derecho. Vamos a implementar lo necesario para que marque los puntos.

El código nuevo lo vamos a poner justo debajo de dichas etiquetas, empezando por moveBall_rightChg.

moveBall_rightChg: ld hl, p1points inc (hl) call PrintPoints

Cargamos en HL la dirección de memoria donde se encuentra el marcador del jugador 1, LD HL, p1points, lo incrementamos, INC (HL), y pintamos el marcador, CALL PrintPoints. El resto de la rutina se queda como estaba.

Las modificaciones en la etiqueta moveBall_leftChg son prácticamente las mismas.

moveBall_leftChg: ld hl, p2points ; ¡CAMBIO! inc (hl) call PrintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - PuntuaciónEnsamblador ZX Spectrum, puntuación
Inicio y fin de la partida

Ya tenemos marcador, pero la partida continúa interminablemente y en el momento que pasamos de 15 puntos, empieza a pintar cosas sin sentido.

Ensamblador ZX Spectrum - Puntuación sin sentidoEnsamblador ZX Spectrum, puntuación sin sentido

También podemos apreciar que cada vez va más lento. ¿Pero por qué? Pintamos el marcador en cada iteración, y para localizar el sprite del número a pintar hacemos un bucle, y no es lo mismo un bucle con 15 iteraciones como máximo, que un bucle con hasta 255 iteraciones, ¿a qué no? En la última entrega veremos la forma de implementar GetPointSprite de tal manera que siempre tarde lo mismo, y de paso nos ahorraremos 2 bytes y unos cuantos ciclos de reloj.

Lo que tenemos que hacer ahora es parar la partida cuando alguno de los dos jugadores llegue a 15 puntos; de igual manera vamos a implementar un modo de iniciar la partida, por ejemplo, pulsando el 5.

Al final del archivo Controls.asm vamos a implementar la rutina que espere a que se pulse el 5 para iniciar la partida.

WaitStart: ld a, $f7 in a, ($fe) bit $04, a jr nz, WaitStart ret

Cargamos en A la semifila 1-5, LD A, $F7, leemos el teclado, IN A, ($FE), evaluamos si se ha pulsado el 5, BIT $04, A, y repetimos la operación hasta que se pulse, JR NZ, WaitStart.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; WaitStart. ; Espera que se pulse la tecla 5 para empezar la partida. ; Altera el valor de los registros AF. ; ----------------------------------------------------------------------------- WaitStart: ld a, $f7 ; Carga en A la semifila 1-5 in a, ($fe) ; Lee el teclado bit $04, a ; Evalúa si se ha pulsado el 5 jr nz, WaitStart ; Bucle hasta que se pulse el 5 ret

Volvemos a Main.asm y después de la llamada a PrintPoints, ponemos la siguiente línea.

call WaitStart

Si compilamos y cargamos en el emulador, hasta que no pulsemos el 5, no empezaremos la partida, pero con esto no es suficiente, ya que la partida no finaliza cuando uno de los jugadores llega a 15 puntos.

Seguimos en Main.asm, pero esta vez al final de la rutina loop_continue, justo antes de JR Loop. Es aquí donde vamos a implementar el control de la puntuación.

ld a, (p1points) cp $0f jr z, Main

Cargamos la puntuación del jugador 1 en A, LD A, (p1points), la comparamos con 15, CP $0F, y si es quince saltamos al inicio del programa, JR Z, Main.

Hacemos lo mismo con la puntuación del jugador 2.

ld a, (p2points) ; ¡CAMBIO! cp $0f jr z, Main

Compilamos, cargamos en el emulador y comprobamos que cuando uno de los dos jugadores llega a quince puntos, la partida finaliza.

Ensamblador ZX Spectrum - Y el ganador es...Ensamblador ZX Spectrum, y el ganador es…

¿Pero qué pasa si volvemos a pulsar el 5? Ya no hay forma de iniciar la partida. En ningún momento ponemos el marcador a 0. Si dejamos pulsado el 5, veremos como a cada iteración del bucle, vuelve al inicio y se para.

Para solucionar esto, volvemos al inicio del archivo Main.asm, y justo después de la llamada a WaitStart, vamos a poner los marcadores a 0.

ld a, ZERO ld (p1points), a ld (p2points), a call PrintPoints

Ponemos A = 0, LD A, ZERO, ponemos la puntuación del jugador 1 a 0, LD (p1points), A, ponemos la puntuación del jugador 2 a 0, LD (p2points), A, y pintamos el marcador, CALL PrintPoints. De esta manera, cada vez que iniciamos partida, ponemos los marcadores a 0 y los pintamos.

Compilamos y cargamos en el emulador para ver los resultados. Esto empieza a tomar forma.

Todavía nos quedan ajustes por realizar. Vamos a hacer que cuando se marque un tanto, la bola salga por el lado contrario, es decir, como si sacara el jugador que ha marcado. Vamos a implementar una rutina para borrar la bola, otra para situarla en la parte derecha de la pantalla, y otra para situarla en la parte izquierda.

La rutina para borrar la bola la vamos a implementar en el archivo Video.asm, justo antes de la rutina Cls.

ClearBall: ld hl, (ballPos) ld a, l and $1f cp $10 jr c, clearBall_continue inc l

Cargamos la posición de la bola en HL, LD HL, (ballPos), cargamos la fila y la columna en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con el centro de la pantalla, CP $10.

Si hay acarreo, solo puede estar en el margen izquierdo. Saltamos a borrar la bola, JR C, clearBall_continue. Si no salta, está en el margen derecho, pero la bola en realidad está pintada una columna más a la derecha (la bola se pinta en dos bytes/columnas); apuntamos HL a la columna dónde está pintada la bola, INC L.

clearBall_continue: ld b, $06 clearBall_loop: ld (hl), ZERO call NextScan djnz clearBall_loop ret

Cargamos en B el número de scanlines que vamos a borrar, LD B, $06, borramos la posición apuntada por HL, LD (HL), ZERO, apuntamos HL al siguiente scanline, CALL NextScan, repetimos la operación hasta que B valga 0, DJNZ clearBall_loop, y salimos, RET.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Borra la bola. ; Altera el valor de los registros AF, B y HL. ; ----------------------------------------------------------------------------- ClearBall: ld hl, (ballPos) ; Carga la posición de la bola en HL ld a, l ; Carga la fila y columna en A and $1f ; Se queda con la columna cp $10 ; Lo compara con el centro de la pantalla jr c, clearBall_continue ; Si está a la izquierda salta inc l ; Incrementa la columna clearBall_continue: ld b, $06 ; Bucle por 6 scanlines clearBall_loop: ld (hl), ZERO ; Borra el byte apuntado por HL call NextScan ; Obtiene el scanline siguiente djnz clearBall_loop ; Hasta que B = 0 ret

Las otras dos rutinas las vamos a implementar al final del archivo Game.asm.

SetBallLeft: ld hl, $4d60 ld (ballPos), hl ld a, $01 ld (ballRotation), a ld a, (ballSetting) and $bf ld (ballSetting), a ret

Cargamos en HL la nueva posición de la bola, LD HL, $4D60, y lo cargamos en memoria, LD (ballPos), HL. Cargamos la rotación de la bola en A, LD A, $01, y lo cargamos en memoria, LD (ballRotation), A. Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos la dirección horizontal hacia la derecha, AND $BF, lo cargamos en memoria, LD (ballSetting), A, y salimos, RET.

La rutina para posicionar la bola a la derecha es prácticamente igual; marcamos las diferencias sin entrar en detalle.

SetBallRight: ; ¡CAMBIO! ld hl, $4d7e ; ¡CAMBIO! ld (ballPos), hl ld a, $ff ; ¡CAMBIO! ld (ballRotation), a ld a, (ballSetting) or $40 ; ¡CAMBIO! ld (ballSetting), a ret

El aspecto final de las dos rutinas es el siguiente.

; ----------------------------------------------------------------------------- ; Posiciona la bola a la izquierda. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallLeft: ld hl, $4d60 ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $01 ; Carga 1 en A ld (ballRotation), a ; Lo carga en memoria, Rotación = 1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $bf ; Pone la dirección horizontal hacia la derecha ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ret ; ----------------------------------------------------------------------------- ; Posiciona la bola a la derecha. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallRight: ld hl, $4d7e ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $ff ; Carga -1 en A ld (ballRotation), a ; Lo carga en memoria, Rotación = -1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola or $40 ; Pone la dirección horizontal hacia la izquierda ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ret

Para acabar con este paso, solo nos queda utilizar estas rutinas. Vamos a modificar las rutinas moveBall_rightChg y moveBall_leftChg del archivo Game.asm.

En la rutina moveBall_rightChg, borramos las líneas que hay entre CALL PrintPoints y JR moveBall_end, y las sustituimos por las siguientes.

call ClearBall call SetBallLeft

El aspecto final de la rutina es el siguiente.

moveBall_rightChg: ; Ha llegado al límite derecho, ¡PUNTO! ld hl, p1points ; Carga en HL la dirección de la puntuación del jugador 1 inc (hl) ; Lo incrementa call PrintPoints ; Pinta el marcador call ClearBall ; Borra la bola call SetBallLeft ; Pone la bola a la izquierda jr moveBall_end ; Fin de la rutina

En la rutina moveBall_leftChg, borramos las líneas que hay entre CALL PrintPoints y la etiqueta moveBall_end, y las sustituimos por las siguientes.

call ClearBall call SetBallRight

El aspecto final de la rutina es el siguiente.

moveBall_leftChg: ; Ha llegado al límite izquierdo, ¡PUNTO! ld hl, p2points ; Carga en HL la dirección de la puntuación del jugador 2 inc (hl) ; Lo incrementa call PrintPoints ; Pinta el marcador call ClearBall ; Borra la bola call SetBallRight ; Pone la bola a la derecha

Compilamos, cargamos en el emulador, y ya podemos empezar a jugar nuestras primeras partidas a dos jugadores, aunque todavía quedan cosas por hacer.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, partida a dos jugadores y cambio de velocidad de la bola

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x08 Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a implementar la partida a dos jugadores, con marcador, y la posibilidad de cambiar la velocidad de la bola.

Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola

Creamos la carpeta Paso08 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso07.

Vamos a empezar por el marcador, definiendo la posición donde vamos a pintar la puntuación, y definiendo también los sprites necesarios en el archivo Sprite.asm.

Marcadores
POINTS_P1: EQU $450d POINTS_P2: EQU $4511

Cada dígito de los marcadores ocupa 8×16 píxeles, o lo que es lo mismo, un carácter de ancho por dos de alto (1 byte x 16 bytes/scanlines).

Blanco_sprite: ds $10 ; 16 espacios = 16 bytes a $00 Cero_sprite: db $00, $7e, $7e, $66, $66, $66, $66, $66 db $66, $66, $66, $66, $66, $7e, $7e, $00 Uno_sprite: db $00, $18, $18, $18, $18, $18, $18, $18 db $18, $18, $18, $18, $18, $18, $18, $00 Dos_sprite: db $00, $7e, $7e, $06, $06, $06, $06, $7e db $7e, $60, $60, $60, $60, $7e, $7e, $00 Tres_sprite: db $00, $7e, $7e, $06, $06, $06, $06, $3e db $3e, $06, $06, $06, $06, $7e, $7e, $00 Cuatro_sprite: db $00, $66, $66, $66, $66, $66, $66, $7e db $7e, $06, $06, $06, $06, $06, $06, $00 Cinco_sprite: db $00, $7e, $7e, $60, $60, $60, $60, $7e db $7e, $06, $06, $06, $06, $7e, $7e, $00 Seis_sprite: db $00, $7e, $7e, $60, $60, $60, $60, $7e db $7e, $66, $66, $66, $66, $7e, $7e, $00 Siete_sprite: db $00, $7e, $7e, $06, $06, $06, $06, $06 db $06, $06, $06, $06, $06, $06, $06, $00 Ocho_sprite: db $00, $7e, $7e, $66, $66, $66, $66, $7e db $7e, $66, $66, $66, $66, $7e, $7e, $00 Nueve_sprite: db $00, $7e, $7e, $66, $66, $66, $66, $7e db $7e, $06, $06, $06, $06, $7e, $7e, $00

Una vez que hemos definido los sprites, definimos la composición de los números haciendo referencia a las etiquetas de los sprites.

Cero: dw Blanco_sprite, Cero_sprite Uno: dw Blanco_sprite, Uno_sprite Dos: dw Blanco_sprite, Dos_sprite Tres: dw Blanco_sprite, Tres_sprite Cuatro: dw Blanco_sprite, Cuatro_sprite Cinco: dw Blanco_sprite, Cinco_sprite Seis: dw Blanco_sprite, Seis_sprite Siete: dw Blanco_sprite, Siete_sprite Ocho: dw Blanco_sprite, Ocho_sprite Nueve: dw Blanco_sprite, Nueve_sprite Diez: dw Uno_sprite, Cero_sprite Once: dw Uno_sprite, Uno_sprite Doce: dw Uno_sprite, Dos_sprite Trece: dw Uno_sprite, Tres_sprite Catorce: dw Uno_sprite, Cuatro_sprite Quince: dw Uno_sprite, Cinco_sprite

Ahora necesitamos definir el lugar donde vamos a guardar la puntuación de cada jugador. Abrimos el archivo Main.asm y añadimos las siguientes variables antes de END $8000.

p1points: db $00 p2points: db $00

Ya tenemos todo listo para empezar a implementar el marcador.

Lo primero que tenemos que saber es que sprite tenemos que pintar, dependiendo del marcador de cada jugador. Para saber qué sprite pintar, vamos a implementar una rutina que recibe en A la puntuación, y devuelve en HL la dirección del sprite a pintar.

Abrimos el archivo Video.asm e implementamos justo antes de la rutina NextScan.

GetPointSprite: ld hl, Cero ld bc, $04 inc a

Cargamos en HL la dirección del sprite para el cero, LD HL, Cero. Como cada sprite está a 4 bytes del anterior, cargamos este desplazamiento en BC, LD BC, $04, e incrementamos A para que el bucle no empiece en 0, INC A, en el caso de que la puntuación sea 0.

Ahora hacemos un bucle para que HL apunte al sprite correcto.

getPointSprite_loop: dec a ret z add hl, bc jr getPointSprite_loop

Decrementamos A, DEC A, y si hemos llegado a 0, HL ya apunta al sprite correcto y salimos, RET Z. Si todavía no hemos llegado a 0, sumamos el desplazamiento a HL, ADD HL, BC, y volvemos a ejecutar el bucle, JR getPointSprite_loop.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Obtiene el sprite correspondiente a pintar en el marcador. ; Entrada: A = puntuación. ; Salida: HL = Dirección del sprite a pintar. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- GetPointSprite: ld hl, Cero ; Carga en HL la dirección del sprite del 0 ld bc, $04 ; Cada sprite está del anterior a 4 bytes inc a ; Incrementa A para que el inicio del bucle no sea 0 getPointSprite_loop: dec a ; Decrementa A ret z ; Si ha llegado a 0, fin de rutina add hl, bc ; Suma 4 a la dirección del sprite; siguiente sprite jr getPointSprite_loop ; Bucle hasta que A = 0 ret

Y ahora vamos a implementar la rutina que pinta los marcadores, al final del archivo Video.asm.

PrintPoints: ld a, (p1points) call GetPointSprite

Cargamos la puntuación del jugador 1 en A, LD A, (p1points), y obtenemos la dirección de memoria donde está la definición del sprite correspondiente a dicha puntuación, CALL GetPointSprite.

GetPointSprite nos devuelve en HL la dirección de memoria donde está definido el sprite. Si la puntuación es cero, HL nos traerá la dirección de memoria donde está definida la etiqueta Cero, cuya definición es la siguiente.

Cero:
dw Blanco_sprite, Cero_sprite

Como podemos ver, Cero está definido por otras dos direcciones de memoria: la primera es la dirección de memoria donde está definido el sprite blanco, usado para justificar a dos dígitos, y la segunda es la dirección de memoria donde está definido el sprite del cero.

Si las direcciones de memoria fueran las siguientes.

$9000 Blanco_sprite
$9020 Cero_sprite
$9040 Cero

La definición de la etiqueta Cero, una vez que se sustituyen las etiquetas Blanco_sprite y Cero_sprite por las direcciones de memoria donde están definidas, sería.

Cero:
dw $9000, $9020

El valor que tendría HL tras llamar a GetPointSprite con el marcador a 0 sería $9040, o lo que es lo mismo, la dirección de memoria donde se define la etiqueta Cero.

Como el Z80 es Little Endian, los valores de las direcciones de memoria desde $9040 en adelante serían-

$9040$00
$9041$90
$9042$20
$9043$90

O lo que es lo mismo, las direcciones de memoria donde están definidos los sprites para Blanco_sprite y para Cero_sprite.

Esta explicación es necesaria para entender el funcionamiento del resto de la rutina.

push hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P1 call printPoint_print

Vamos a pintar el primer dígito del marcador del jugador 1. Preservamos el valor de HL, que apunta al sprite del marcador que tenemos que pintar, PUSH HL, cargamos en E la parte baja de la dirección donde está el sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).

Cargamos en HL la dirección de memoria de pantalla donde se pinta el primer dígito del marcador del jugador 1, LD HL, POINTS_P1, y llamamos al pintado del dígito, CALL printPoint_print.

Ahora pintamos el segundo dígito del marcador del jugador 1.

pop hl inc hl inc hl

Recuperamos el valor de HL, POP HL, y lo apuntamos a la parte baja de la dirección donde está definido el sprite del segundo dígito, INC HL INC HL.

ld e, (hl) inc hl ld d, (hl)

Cargamos la parte baja de dicha dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).

ld hl, POINTS_P1 inc l call printPoint_print

Por último, cargamos en HL la posición de memoria de la pantalla donde se pinta el marcador del jugador 1, LD HL, POINTS_P1. Como cada dígito ocupa 1 byte (columna) de ancho, situamos HL en la columna dónde se pinta el segundo dígito, INC L, y lo pintamos.

La forma de pintar el marcador del jugador 2 es casi igual a la del jugador 1, por lo que mostramos el código marcando los cambios y sin entrar en detalle.

ld a, (p2points) ; !CAMBIO! call GetPointSprite push hl ; 1er dígito ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; !CAMBIO! call printPoint_print pop hl ; 2º dígito inc hl inc hl ld e, (hl) inc hl ld d, (hl) ld hl, POINTS_P2 ; !CAMBIO! inc l

Como se puede observar, los cambios son pocos. Se ha quitado la última línea al no ser necesario llamar a pintar el segundo dígito del jugador 2, ya que lo vamos a implementar a continuación del último INC L.

Recordemos que cada dígito ocupa 8×16 píxeles (una columna x 16 scanlines).

printPoint_print: ld b, $10 push de push hl

Cargamos en B el número de scanlines que vamos a pintar, LD B, $10, y preservamos el valor del registro DE, PUSH DE, y de HL, PUSH HL.

printPoint_printLoop: ld a, (de) ld (hl), a inc de call NextScan djnz printPoint_printLoop

Cargamos en A el byte a pintar, LD A, (DE), y lo pintamos en pantalla, LD (HL), A. Apuntamos DE al siguiente byte a pintar, obtenemos la dirección del siguiente scanline, CALL NextScan, y repetimos la operación hasta que B sea 0 y hayamos pintado los 16 scanlines, DJNZ printPoint_printLoop.

Para finalizar, recuperamos los valores de HL y DE y salimos.

pop hl pop de ret

El aspecto final de la rutina de pintado del marcador es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el marcador. ; Cada número consta de 1 byte de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- PrintPoints: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call printPoint_print ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito call printPoint_print ; Pinta el segundo dígito del marcador del jugador 1 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 2 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call printPoint_print ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito ; Pinta el segundo dígito del marcador del jugador 2 printPoint_print: ld b, $10 ; Cada dígito son 1 byte por 16 (scanlines) push de ; Preserva el valor de DE push hl ; Preserva el valor de HL printPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz printPoint_printLoop ; Hasta que B = 0 pop hl ; Recupera el valor de HL pop de ; Recupera el valor de DE ret

En esta rutina es sencillo ahorrar 12 ciclos de reloj y 2 bytes. Para ello hay que cambiar dos instrucciones de lugar, lo que nos permite quitar otras dos; lo veremos al final del tutorial.

Y ahora solo nos queda ver si lo que hemos implementado funciona. Abrimos Main.asm, y debajo de la llamada a PrintBorder, justo antes de Loop, añadimos la siguiente línea.

call PrintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - Bola borra marcadorEnsamblador ZX Spectrum, bola borra marcador

En principio todo va bien, pero según se va moviendo la bola vemos que volvemos a tener un problema, viejo conocido nuestro, y es que la bola borra el marcador a su paso, cosa que vamos a solucionar a continuación.

Para evitar que la bola borre el marcador, hacemos lo mismo que hicimos con la línea central, vamos a repintar el marcador. Implementamos la rutina al final del archivo Video.asm.

En realidad, la rutina de repintado del marcador es prácticamente igual que la de pintado, cambiando el nombre de las etiquetas y añadiendo una línea. Vamos a copiar toda la rutina de pintado de marcador y la vamos a pegar al final del archivo Video.asm. Cambiamos los nombres de las etiquetas y añadimos una línea.

A continuación, mostramos el aspecto final, marcando los cambios producidos con respecto a la rutina de pintado del marcador.

; ----------------------------------------------------------------------------- ; Repinta el marcador. ; Cada número consta de 1 bytes de ancho por 16 de alto. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- ReprintPoints: ld a, (p1points) ; Carga en A los puntos del jugador 1 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 1 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 call reprintPoint_print ; Pinta el primer dígito del marcador del jugador 1 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 1 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 1 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito call reprintPoint_print ; Pinta el segundo dígito del marcador del jugador 1 ld a, (p2points) ; Carga en A los puntos del jugador 2 call GetPointSprite ; Obtiene el sprite a pintar en el marcador push hl ; Preserva el valor de HL ; 1er dígito del jugador 2 ld e, (hl) ; Carga en E la parte baja de la dirección ; donde está el primer dígito inc hl ; Apunta HL a la parte alta de la dirección ; donde está el primer dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 call reprintPoint_print ; Pinta el primer dígito del marcador del jugador 2 pop hl ; Recupera el valor de HL ; 2º dígito del jugador 2 inc hl inc hl ; Apunta HL a la parte baja de la dirección ; donde está el segundo dígito ld e, (hl) ; y la carga en E inc hl ; Apunta HL a la parte alta de la dirección ; donde está el segundo dígito ld d, (hl) ; y la carga en D ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan ; los puntos del jugador 2 inc l ; Apunta HL a la dirección donde se pinta el segundo dígito ; Pinta el segundo dígito del marcador del jugador 2 reprintPoint_print: ld b, $10 ; Cada dígito es de 1 byte por 16 (scanlines) push de push hl ; Preserva el valor de los registros DE y HL reprintPoint_printLoop: ld a, (de) ; Carga en A el byte a pintar or (hl) ; Lo mezcla con lo que hay pintado en pantalla ld (hl), a ; Pinta el byte inc de ; Apunta DE al siguiente byte call NextScan ; Apunta HL al siguiente scanline djnz reprintPoint_printLoop ; Hasta que B = 0 pop hl pop de ; Recupera el valor de los registros HL y DE ret

Vamos a explicar la línea que hemos añadido.

ld a, (de)
or (hl)
ld (hl), a

Lo que hacemos con OR (HL) es agregar los píxeles que hay en pantalla a los píxeles del sprite del número. De esta manera repintamos el número sin borrar la bola.

Ahora queda ver si funciona. Abrimos el archivo Main.asm y añadimos la siguiente línea después de la llamada a ReprintLine.

call ReprintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - Bola no borra marcadorEnsamblador ZX Spectrum, bola no borra marcador

Efectivamente, hemos solucionado un problema, pero ha surgido otro. El marcador ya no se borra, pero la bola va muy lenta. Por suerte la solución es sencilla, ya que la velocidad de la bola es una de las cosas que controlamos nosotros.

Como recordaréis, la bola se mueve una de cada seis iteraciones del bucle principal, por lo que lo único que tenemos que hacer es reducir este intervalo en Main.asm, por ejemplo a dos.

ld (countLoopBall), a cp $02 ; ¡CAMBIO! jr nz, loop_paddle

Compilamos, cargamos en el emulador y comprobamos que la velocidad de la bola ha aumentado.

Cambio de velocidad de la bola

Como recodaremos, en la variable ballSetting definimos la velocidad de la bola en los bits 4 y 5, pudiendo ser 1 la más rápida y 3 la más lenta. Vamos a utilizar este aspecto para definir y modificar la velocidad de la bola.

Lo primero es modificar el valor inicial de esta variable.

ballSetting: db $20

De esta manera el valor inicial es:

  • Dirección vertical hacia arriba.
  • Dirección horizontal hacia la derecha.
  • Velocidad de la bola 2.

Y ahora vamos a usar este valor para controlar el intervalo para mover la bola. Abrimos Main.asm, localizamos la etiqueta Loop, y añadimos justo debajo.

ld a, (ballSetting) rrca rrca rrca rrca and $03 ld b, a

Cargamos la configuración de la bola en A, LD A, (ballSetting), pasamos el valor de los bits 4 y 5 a los bits 0 y 1, RRCA RRCA RRCA RRCA, nos quedamos con el valor de los bits 0 y 1 (velocidad de la bola), AND $03, y cargamos el valor en B, LD B, A.

Cuatro líneas más abajo, cambiamos la línea CP $02.

cp b

Compilamos y comprobamos que todo sigue funcionando igual. La única diferencia es que ahora la velocidad de la bola la tomamos desde la configuración de la misma, y podremos cambiarla.

Para cambiar la velocidad de la bola, vamos a usar las teclas del 1 al 3. Abrimos el archivo Controls.asm y empezamos a escribir tras la etiqueta ScanKeys.

scanKeys_speed: ld a, $00 ld (countLoopBall), a scanKeys_ctrl:

Si se ha pulsado alguna de las teclas de cambio de velocidad, hay que poner a 0 el contador de vueltas de bucle para pintar la bola, de lo contrario, si el contador está en 2 y ponemos la velocidad a 1, habrá que esperar 254 iteraciones hasta que la bola se vuelva a mover.

Ponemos A = 0, LD A, $00, y ponemos el contador de iteraciones para la bola a 0, LD (countLoopBall), A.

La etiqueta scanKeys_ctrl marca el punto donde empieza la rutina tal y como la tenemos ahora. La nueva implementación la vamos a hacer entre las etiquetas ScanKeys y scanKeys_speed.

ld a, $f7 in a, ($fe)

Cargamos la semifila 1-5 en A, LD A, $F7, y leemos del puerto del teclado, IN A, ($FE).

bit $00, a jr nz, scanKeys_2

Comprobamos si se ha pulsado el 1, BIT $00, A, y en caso de no haberlo pulsado saltamos a comprobar si se ha pulsado el 2, JR NZ, scanKeys_2.

Si se ha pulsado el 1, cambiamos la velocidad de la bola.

ld a, (ballSetting) and $cf or $10 ld (ballSetting), a jr scanKeys_speed

Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos los bits de la velocidad a 0, AND $CF, ponemos la velocidad a 1, OR $10, cargamos la configuración en memoria, LD (ballSetting), A, y saltamos a poner a 0 el contador de iteraciones para la bola, JR scanKeys_speed.

La comprobación para el 2 y el 3 es muy parecida a la comprobación del 1, por lo que vemos el código completo y marcamos las diferencias.

scanKeys_2: bit $01, a ; ¡CAMBIO! jr nz, scanKeys_3 ; ¡CAMBIO! ld a, (ballSetting) and $cf or $20 ; ¡CAMBIO! ld (ballSetting), a jr scanKeys_speed scanKeys_3: bit $02, a ; ¡CAMBIO! jr nz, scanKeys_ctrl ; ¡CAMBIO! ld a, (ballSetting) and $cf ; ¡BORRAR! or $30 ; ¡CAMBIO! ld (ballSetting), a

El aspecto final de la rutina, una vez modificada, queda de la siguiente manera.

; ----------------------------------------------------------------------------- ; ScanKeys ; Escanea las teclas de control y devuelve las pulsadas. ; Salida: D = Teclas pulsadas. ; Bit 0 = A pulsada 0/1. ; Bit 1 = Z pulsada 0/1. ; Bit 2 = 0 pulsada 0/1. ; Bit 3 = O pulsada 0/1. ; Altera el valor de los registros AF y D. ; ----------------------------------------------------------------------------- ScanKeys: ld a, $f7 ; Carga en A la semifila 1-5 in a, ($fe) ; Lee el estado de la semifila bit $00, a ; Comprueba si se ha pulsado el 1 jr nz, scanKeys_2 ; Si no se ha pulsado salta ; Se ha pulsado; cambia la velocidad de la bola 1 (rápido) ld a, (ballSetting) ; Carga la configuración de la bola en A and $cf ; Pone los bits de velocidad a 0 or $10 ; Pone los bits de velocidad a 1 ld (ballSetting), a ; Carga el valor en memoria jr scanKeys_speed ; Salta para comprobar los controles scanKeys_2: bit $01, a ; Comprueba si se ha pulsado el 2 jr nz, scanKeys_3 ; Si no se ha pulsado salta ; Se ha pulsado; cambia la velocidad de la bola 2 (medio) ld a, (ballSetting) ; Carga la configuración de la bola en A and $cf ; Pone los bits de velocidad a 0 or $20 ; Pone los bits de velocidad a 2 ld (ballSetting), a ; Carga el valor en memoria jr scanKeys_speed ; Salta para comprobar los controles scanKeys_3: bit $02, a ; Comprueba si se ha pulsado el 3 jr nz, scanKeys_ctrl ; Si no se ha pulsado salta ; Se ha pulsado; cambia la velocidad de la bola 3 (lento) ld a, (ballSetting) ; Carga la configuración de la bola en A or $30 ; Pone los bits de velocidad a 3 ld (ballSetting), a ; Carga el valor en memoria scanKeys_speed: ld a, $00 ; Pone A = 0 ld ( countLoopBall), a ; Pone el contador de iteraciones para la bola a 0 scanKeys_ctrl: ld d, $00 ; Pone el registro D a 0. ; Resto de la rutina desde ScanKeys_A

Es el momento de compilar y cargar en el emulador para comprobar cómo se comporta esta modificación. Si todo ha ido bien, podemos cambiar la velocidad de la bola pulsando las teclas 1, 2 y 3.

Puntuación

Lo último que tenemos que hacer es contabilizar los puntos de cada jugador, para lo cual vamos a modificar la rutina MoveBall, en concreto moveBall_rightChg y moveBall_leftChg. Estas rutinas se encargan de cambiar la dirección de la bola cuando llega al límite izquierdo o derecho. Vamos a implementar lo necesario para que marque los puntos.

El código nuevo lo vamos a poner justo debajo de dichas etiquetas, empezando por moveBall_rightChg.

moveBall_rightChg: ld hl, p1points inc (hl) call PrintPoints

Cargamos en HL la dirección de memoria donde se encuentra el marcador del jugador 1, LD HL, p1points, lo incrementamos, INC (HL), y pintamos el marcador, CALL PrintPoints. El resto de la rutina se queda como estaba.

Las modificaciones en la etiqueta moveBall_leftChg son prácticamente las mismas.

moveBall_leftChg: ld hl, p2points ; ¡CAMBIO! inc (hl) call PrintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - PuntuaciónEnsamblador ZX Spectrum, puntuación
Inicio y fin de la partida

Ya tenemos marcador, pero la partida continúa interminablemente y en el momento que pasamos de 15 puntos, empieza a pintar cosas sin sentido.

Ensamblador ZX Spectrum - Puntuación sin sentidoEnsamblador ZX Spectrum, puntuación sin sentido

También podemos apreciar que cada vez va más lento. ¿Pero por qué? Pintamos el marcador en cada iteración, y para localizar el sprite del número a pintar hacemos un bucle, y no es lo mismo un bucle con 15 iteraciones como máximo, que un bucle con hasta 255 iteraciones, ¿a qué no? En la última entrega veremos la forma de implementar GetPointSprite de tal manera que siempre tarde lo mismo, y de paso nos ahorraremos 2 bytes y unos cuantos ciclos de reloj.

Lo que tenemos que hacer ahora es parar la partida cuando alguno de los dos jugadores llegue a 15 puntos; de igual manera vamos a implementar un modo de iniciar la partida, por ejemplo, pulsando el 5.

Al final del archivo Controls.asm vamos a implementar la rutina que espere a que se pulse el 5 para iniciar la partida.

WaitStart: ld a, $f7 in a, ($fe) bit $04, a jr nz, WaitStart ret

Cargamos en A la semifila 1-5, LD A, $F7, leemos el teclado, IN A, ($FE), evaluamos si se ha pulsado el 5, BIT $04, A, y repetimos la operación hasta que se pulse, JR NZ, WaitStart.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; WaitStart. ; Espera que se pulse la tecla 5 para empezar la partida. ; Altera el valor de los registros AF. ; ----------------------------------------------------------------------------- WaitStart: ld a, $f7 ; Carga en A la semifila 1-5 in a, ($fe) ; Lee el teclado bit $04, a ; Evalúa si se ha pulsado el 5 jr nz, WaitStart ; Bucle hasta que se pulse el 5 ret

Volvemos a Main.asm y después de la llamada a PrintPoints, ponemos la siguiente línea.

call WaitStart

Si compilamos y cargamos en el emulador, hasta que no pulsemos el 5, no empezaremos la partida, pero con esto no es suficiente, ya que la partida no finaliza cuando uno de los jugadores llega a 15 puntos.

Seguimos en Main.asm, pero esta vez al final de la rutina loop_continue, justo antes de JR Loop. Es aquí donde vamos a implementar el control de la puntuación.

ld a, (p1points) cp $0f jr z, Main

Cargamos la puntuación del jugador 1 en A, LD A, (p1points), la comparamos con 15, CP $0F, y si es quince saltamos al inicio del programa, JR Z, Main.

Hacemos lo mismo con la puntuación del jugador 2.

ld a, (p2points) ; ¡CAMBIO! cp $0f jr z, Main

Compilamos, cargamos en el emulador y comprobamos que cuando uno de los dos jugadores llega a quince puntos, la partida finaliza.

Ensamblador ZX Spectrum - Y el ganador es...Ensamblador ZX Spectrum, y el ganador es…

¿Pero qué pasa si volvemos a pulsar el 5? Ya no hay forma de iniciar la partida. En ningún momento ponemos el marcador a 0. Si dejamos pulsado el 5, veremos como a cada iteración del bucle, vuelve al inicio y se para.

Para solucionar esto, volvemos al inicio del archivo Main.asm, y justo después de la llamada a WaitStart, vamos a poner los marcadores a 0.

ld a, ZERO ld (p1points), a ld (p2points), a call PrintPoints

Ponemos A = 0, LD A, ZERO, ponemos la puntuación del jugador 1 a 0, LD (p1points), A, ponemos la puntuación del jugador 2 a 0, LD (p2points), A, y pintamos el marcador, CALL PrintPoints. De esta manera, cada vez que iniciamos partida, ponemos los marcadores a 0 y los pintamos.

Compilamos y cargamos en el emulador para ver los resultados. Esto empieza a tomar forma.

Todavía nos quedan ajustes por realizar. Vamos a hacer que cuando se marque un tanto, la bola salga por el lado contrario, es decir, como si sacara el jugador que ha marcado. Vamos a implementar una rutina para borrar la bola, otra para situarla en la parte derecha de la pantalla, y otra para situarla en la parte izquierda.

La rutina para borrar la bola la vamos a implementar en el archivo Video.asm, justo antes de la rutina Cls.

ClearBall: ld hl, (ballPos) ld a, l and $1f cp $10 jr c, clearBall_continue inc l

Cargamos la posición de la bola en HL, LD HL, (ballPos), cargamos la fila y la columna en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con el centro de la pantalla, CP $10.

Si hay acarreo, solo puede estar en el margen izquierdo. Saltamos a borrar la bola, JR C, clearBall_continue. Si no salta, está en el margen derecho, pero la bola en realidad está pintada una columna más a la derecha (la bola se pinta en dos bytes/columnas); apuntamos HL a la columna dónde está pintada la bola, INC L.

clearBall_continue: ld b, $06 clearBall_loop: ld (hl), ZERO call NextScan djnz clearBall_loop ret

Cargamos en B el número de scanlines que vamos a borrar, LD B, $06, borramos la posición apuntada por HL, LD (HL), ZERO, apuntamos HL al siguiente scanline, CALL NextScan, repetimos la operación hasta que B valga 0, DJNZ clearBall_loop, y salimos, RET.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Borra la bola. ; Altera el valor de los registros AF, B y HL. ; ----------------------------------------------------------------------------- ClearBall: ld hl, (ballPos) ; Carga la posición de la bola en HL ld a, l ; Carga la fila y columna en A and $1f ; Se queda con la columna cp $10 ; Lo compara con el centro de la pantalla jr c, clearBall_continue ; Si está a la izquierda salta inc l ; Incrementa la columna clearBall_continue: ld b, $06 ; Bucle por 6 scanlines clearBall_loop: ld (hl), ZERO ; Borra el byte apuntado por HL call NextScan ; Obtiene el scanline siguiente djnz clearBall_loop ; Hasta que B = 0 ret

Las otras dos rutinas las vamos a implementar al final del archivo Game.asm.

SetBallLeft: ld hl, $4d60 ld (ballPos), hl ld a, $01 ld (ballRotation), a ld a, (ballSetting) and $bf ld (ballSetting), a ret

Cargamos en HL la nueva posición de la bola, LD HL, $4D60, y lo cargamos en memoria, LD (ballPos), HL. Cargamos la rotación de la bola en A, LD A, $01, y lo cargamos en memoria, LD (ballRotation), A. Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos la dirección horizontal hacia la derecha, AND $BF, lo cargamos en memoria, LD (ballSetting), A, y salimos, RET.

La rutina para posicionar la bola a la derecha es prácticamente igual; marcamos las diferencias sin entrar en detalle.

SetBallRight: ; ¡CAMBIO! ld hl, $4d7e ; ¡CAMBIO! ld (ballPos), hl ld a, $ff ; ¡CAMBIO! ld (ballRotation), a ld a, (ballSetting) or $40 ; ¡CAMBIO! ld (ballSetting), a ret

El aspecto final de las dos rutinas es el siguiente.

; ----------------------------------------------------------------------------- ; Posiciona la bola a la izquierda. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallLeft: ld hl, $4d60 ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $01 ; Carga 1 en A ld (ballRotation), a ; Lo carga en memoria, Rotación = 1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $bf ; Pone la dirección horizontal hacia la derecha ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ret ; ----------------------------------------------------------------------------- ; Posiciona la bola a la derecha. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- SetBallRight: ld hl, $4d7e ; Carga en HL la posición de la bola ld (ballPos), hl ; Carga el valor en memoria ld a, $ff ; Carga -1 en A ld (ballRotation), a ; Lo carga en memoria, Rotación = -1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola or $40 ; Pone la dirección horizontal hacia la izquierda ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ret

Para acabar con este paso, solo nos queda utilizar estas rutinas. Vamos a modificar las rutinas moveBall_rightChg y moveBall_leftChg del archivo Game.asm.

En la rutina moveBall_rightChg, borramos las líneas que hay entre CALL PrintPoints y JR moveBall_end, y las sustituimos por las siguientes.

call ClearBall call SetBallLeft

El aspecto final de la rutina es el siguiente.

moveBall_rightChg: ; Ha llegado al límite derecho, ¡PUNTO! ld hl, p1points ; Carga en HL la dirección de la puntuación del jugador 1 inc (hl) ; Lo incrementa call PrintPoints ; Pinta el marcador call ClearBall ; Borra la bola call SetBallLeft ; Pone la bola a la izquierda jr moveBall_end ; Fin de la rutina

En la rutina moveBall_leftChg, borramos las líneas que hay entre CALL PrintPoints y la etiqueta moveBall_end, y las sustituimos por las siguientes.

call ClearBall call SetBallRight

El aspecto final de la rutina es el siguiente.

moveBall_leftChg: ; Ha llegado al límite izquierdo, ¡PUNTO! ld hl, p2points ; Carga en HL la dirección de la puntuación del jugador 2 inc (hl) ; Lo incrementa call PrintPoints ; Pinta el marcador call ClearBall ; Borra la bola call SetBallRight ; Pone la bola a la derecha

Compilamos, cargamos en el emulador, y ya podemos empezar a jugar nuestras primeras partidas a dos jugadores, aunque todavía quedan cosas por hacer.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, partida a dos jugadores y cambio de velocidad de la bola

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x07 Ensamblador ZX Spectrum Pong – Detección de colisiones [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a empezar con la detección de colisiones, para que la bola rebote contra las palas.

Ensamblador ZX Spectrum Pong – Detección de colisiones

Creamos la carpeta Paso07 y copiamos desde la carpeta Paso06 los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm.

A partir de aquí, vamos a utilizar todo lo que hemos implementando hasta ahora, evolucionándolo.

Vamos a implementar la detección de colisiones de la bola con las palas. Para ello necesitamos definir la columna en la que se produce dicha colisión, lo que vamos a hacer en Sprite.asm.

CROSS_LEFT: EQU $01 CROSS_RIGHT: EQU $1d

Para comprobar la colisión en la coordenada X, vamos a usar la columna. Para comprobar la colisión en la coordenada Y vamos a usar tercio, línea y scanline.

GetPtrY

Como hemos visto en entregas anteriores, la composición de la coordenada Y se encuentra en dos bytes distintos (010T TSSS LLLC CCCC), por lo que vamos a implementar una rutina que reciba una posición de memoria de la pantalla y devuelva la coordenada Y (TTLLLSSS).

La rutina la vamos a implementar en Video.asm, tras la rutina Cls, y recibe la posición de memoria de la pantalla en HL y devuelve la coordenada Y obtenida en A.

GetPtrY: ld a, h and $18 rlca rlca rlca ld e, a

Cargamos el tercio y scanline en A, LD A, H, nos quedamos con el tercio, AND $18, lo pasamos a los bits 6 y 7, RLCA RLCA RLCA, y cargamos el resultado en E, LD E, A.

ld a, h and $07 or e ld e, a

Volvemos a cargar tercio y scanline en A, LD A, H, nos quedamos con el scanline, AND $07, le agregamos el tercio, OR E, y cargamos el resultado en E, LD E, A.

ld a, l and $e0 rrca rrca or e ret

Cargamos la línea y la columna en A, LD A, L, nos quedamos con la línea, AND $E0, ponemos el valor en los bits 3 a 5, RRCA RRCA, y le añadimos tercio y scanline, OR E.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Obtiene tercio, línea y scanline de una posición de memoria. ; Entrada: HL = Posición de memoria. ; Salida: A = Tercio, línea y scanline obtenido. ; Altera el valor de los registros AF y E. ; ----------------------------------------------------------------------------- GetPtrY: ld a, h ; Carga en A el valor de H (tercio y scanline) and $18 ; Se queda con el tercio rlca rlca rlca ; Pasa el valor del tercio a los bits 6 y 7 ld e, a ; Carga el valor en E ld a, h ; Carga en A el valor de H (tercio y scanline) and $07 ; Se queda con el scanline or e ; Lo mezcla con E ld e, a ; Carga el valor en E TT***SSS ld a, l ; Carga en A el valor de L (línea y columna) and $e0 ; Se queda con la línea rrca rrca ; Pasa el valor a los bits 3 a 5 or e ; Lo mezcla con E (TTLLLSSS) ret

Este tipo de conversión ya la hicimos en la rutina checkVerticalLimit, pero al ser necesaria en más de una rutina, la hemos implementado como una rutina aparte.

Para probarla, vamos a modificar la rutina checkVerticalLimit, sustituyendo casi toda ella por una llamada a GetPtrY, quedando de la siguiente manera.

; ----------------------------------------------------------------------------- ; Evalúa si se ha alcanzado el límite vertical. ; Entrada: A = Límite vertical (TTLLLSSS). ; HL = Posición actual (010TTSSS LLLCCCCC). ; Altera el valor de los registros AF y BC. ; ---------------------------------------------------------------------------- checkVerticalLimit: ld b, a ; Guarda el valor de A en B call GetPtrY ; Obtiene la coordenada Y (TTLLLSSSS) ; de la posición actual cp b ; Lo compara con B. B = valor original de A = Límite vertical ret

Compilamos, cargamos en el emulador y comprobamos que no se ha roto nada.

Detección de colisiones

Ahora vamos a implementar la detección de colisiones, en el archivo Game.asm.

Empezamos por la rutina que evalúa si hay colisión en el eje X. Esta rutina recibe en C la columna donde se produce la colisión, y activa el flag Z si se ha producido.

CheckCrossX: ld a, (ballPos) and $1f cp c ret

Cargamos la posición de la bola en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y comparamos el valor resultante con la columna de colisión, CP C.

El siguiente paso es implementar la rutina que evalúa si hay colisión en el eje Y. Esta rutina recibe en HL la posición de la pala, y activa el flag Z si hay colisión.

CheckCrossY: call GetPtrY inc a ld c, a

Obtenemos la coordenada Y de la pala, CALL GetPtrY. Como el primer scanline de la pala es blanco, no lo tenemos en cuenta para las colisiones, así que pasamos al siguiente, INC A, y cargamos el valor en C, LD C, A.

ld hl, (ballPos) call GetPtrY ld b, a

Cargamos la posición de la bola en HL, LD HL, (ballPos), obtenemos la coordenada Y, CALL GetPtrY, y cargamos el valor en B, LD B, A.

add a, $04 sub c ret c

En A tenemos la coordenada Y de la bola. Apuntamos A al penúltimo scanline de la bola, el último que no es blanco, ADD A, $04, le restamos la coordenada Y de la pala, SUB C, y si hay acarreo salimos, RET C, ya que la bola pasa por encima de la pala.

Si no hemos salido, tenemos que comprobar si la bola pasa por debajo de la pala.

ld a, c add a, $16 ld c, a ld a, b inc a sub c ret nc xor a ret

Cargamos la coordenada Y de la pala en A, LD A, C, le sumamos $16 (22) para posicionarnos en el penúltimo scanline, el último que no está a 0, ADD A, $16, y cargamos el valor en C, LD C, A.

Cargamos la coordenada Y de la bola en A, LD A, B, la apuntamos al scanline 1, el primero que no está a 0, INC A, y le restamos la coordenada Y de la pala, SUB C.

Si tras la resta no hay acarreo salimos, RET NC, pues o bien la bola pasa por debajo, o colisiona en el último scanline de la pala, que está en la misma coordenada Y que el primer scanline de la bola, y al restar se activa el flag Z.

Si hay acarreo, la bola colisiona con el resto de la pala, por lo que activamos el flag Z, XOR A, y salimos, RET.

El siguiente paso es implementar la rutina principal a la que vamos a llamar para comprobar si hay colisión, en cuyo caso vamos a realizar las acciones necesarias.

CheckBallCross: ld a, (ballSetting) and $40 jr nz, checkBallCross_left

Cargamos la configuración de la bola en A, LD A, (ballSetting), y nos quedamos con el bit 6, AND $40, que especifica si la bola va hacia la derecha o hacia la izquierda. Si el bit 6 está a 1, la bola va hacia la izquierda y saltamos a comprobar si se produce colisión con la pala del jugador 1, JR NZ, checkBallCross_left.

Si no se ha producido el salto, la bola va hacia la derecha y comprobamos si hay colisión con la pala del jugador 2.

checkBallCross_right: ld c, CROSS_RIGHT call CheckCrossX ret nz ld hl, (paddle2pos) call CheckCrossY ret nz

Cargamos la columna de colisión en C, LD C, CROSS_RIGHT, evaluamos si se produce colisión en el eje X, CALL CheckCrossX. Si no se produce la colisión salimos de la rutina, RET NZ.

Si se ha producido colisión en el eje X, cargamos la posición de la pala 2 en HL, LD HL, (paddle2pos), y evaluamos si se produce colisión en el eje Y, CALL CheckCrossY. Si no se produce colisión, salimos de la rutina, RET NZ.

Si no hemos salido de la rutina, se ha producido colisión.

ld a, (ballSetting) or $40 ld (ballSetting), a ld a, $ff ld (ballRotation), a ret

Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos el bit 6 a 1 para cambiar la dirección de la bola hacia la izquierda, OR $40, y cargamos el valor en memoria, LD (ballSetting), A.

Cargamos -1 en A, LD A, $FF, cambiamos la rotación de la bola, LD (ballRotation), A, y salimos de la rutina, RET.

La comprobación de si hay colisión con la pala del jugador 1 es similar a lo visto anteriormente, por lo que solo vamos a poner el código y a marcar las diferencias, sin entrar en detalle.

checkBallCross_left: ; ¡CAMBIO! ld c, CROSS_LEFT ; ¡CAMBIO! call CheckCrossX ret nz ld hl, (paddle1pos) ; ¡CAMBIO! call CheckCrossY ret nz ld a, (ballSetting) and $bf ; ¡CAMBIO! ld (ballSetting), a ld a, $01 ; ¡CAMBIO! ld (ballRotation), a ret

El aspecto final de las rutinas de comprobación de colisiones entre las palas y la bola es el siguiente.

; ----------------------------------------------------------------------------- ; Evalúa si hay colisión entre la bola y las palas. ; Altera el valor de los registros AF, C y HL. ; ----------------------------------------------------------------------------- CheckBallCross: ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A and $40 ; Se queda con el bit 6 (izquierda/derecha) jr nz, checkBallCross_left ; Si no está a 0 va hacia la izquierda y salta checkBallCross_right: ld c, CROSS_RIGHT ; Carga la columna de colisión en C call CheckCrossX ; Evalúa si se produce colisión en el eje X ret nz ; Si no se produce, fin de la rutina ld hl, (paddle2pos) ; Carga la posición de la pala 2 en HL call CheckCrossY ; Evalúa si se produce colisión en el eje Y ret nz ; Si no se produce colisión, fin de la rutina ; Si llega aquí hay colisión ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A or $40 ; Cambia la dirección, la pone hacia la izquierda ld (ballSetting), a ; Carga el valor en memoria ld a, $ff ; Cambia la rotación de la bola ld (ballRotation), a ; La carga en memoria ret ; Fin de la rutina checkBallCross_left: ; La bola va hacia la izquierda ld c, CROSS_LEFT ; Carga la columna de colisión en C call CheckCrossX ; Evalúa si se produce colisión en el eje X ret nz ; Si no se produce, fin de la rutina ld hl, (paddle1pos) ; Carga la posición de la pala 1 en HL call CheckCrossY ; Evalúa si se produce colisión en el eje Y ret nz ; Si no se produce colisión, fin de la rutina ; Si llega aquí hay colisión ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A and $bf ; Cambia la dirección, la pone hacia la derecha ld (ballSetting), a ; Carga el valor en memoria ld a, $01 ; Cambia la rotación de la bola ld (ballRotation), a ; La carga en memoria ret ; Fin de la rutina ; ----------------------------------------------------------------------------- ; Evalúa si la bola colisiona en el eje X con la pala. ; Entrada: C = Columna dónde se produce la colisión. ; Salida: Z = Colisiona. ; NZ = No colisiona. ; Altera el valor de los registros AF. ; ----------------------------------------------------------------------------- CheckCrossX: ld a, (ballPos) ; Carga la línea y columna donde está la bola and $1f ; Se queda con la columna cp c ; Lo compara con la columna de colisión ret ; ----------------------------------------------------------------------------- ; Evalúa si la bola colisiona en el eje Y con la pala. ; Entrada: HL = Posición de la pala ; Salida: Z = Colisiona. ; NZ = No colisiona. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- CheckCrossY: call GetPtrY ; Obtiene la posición vertical de la pala (TTLLLSSS) ; La posición devuelta apunta al primer scanline de la pala que está a 0, ; apunta al siguiente inc a ld c, a ; Carga el valor en C ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene la posición vertical de la bola (TTLLLSSS) ld b, a ; Carga el valor en B ; Comprueba si la bola pasa por encima de la pala ; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0 ; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º add a, $04 ; Apunta la posición de la bola al 5º scanline sub c ; Resta a la posición de la bola, la posición de la pala ret c ; Si hay acarreo sale porque la bola pasa por encima ; Comprueba si la bola pasa por debajo de la pala ld a, c ; Carga la posición vertical de la pala en A add a, $16 ; Le suma 22 para apuntar al penúltimo scanline, ; último que no es 0 ld c, a ; Lo vuelve a cargar en C ld a, b ; Carga la posición vertical de la bola inc a ; Le suma 1 para apuntar el scanline 1, primero que no es 0 sub c ; Resta a la posición de la bola, la posición de la pala ret nc ; Si no hay acarreo la bola pasa por debajo de la pala ; o colisiona en el último scanline. ; En este último caso se activa el flag Z ; Hay colisión xor a ; Activa el flag Z ret

Ahora ya solo nos queda ver si lo que hemos implementado hace lo que pretendemos.

Abrimos el archivo Main.asm y justo debajo de la etiqueta loop_continue, añadimos la siguiente línea.

call CheckBallCross

Compilamos y cargamos en el emulador para ver los resultados. Si todo ha ido bien, la bola choca contra las palas; la detección de colisiones está funcionando.

Ensamblador ZX Spectrum - ColisionesEnsamblador ZX Spectrum – colisiones

El aspecto final del archivo Main.asm es el siguiente.

; Detección de colisiones org $8000 ; ----------------------------------------------------------------------------- ; Entrada al programa ; ----------------------------------------------------------------------------- Main: ld a, $00 ; A = 0 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Imprime la línea central call PrintBorder ; Imprime el borde del campo Loop: ld a, (countLoopBall) ; Carga el contador de vueltas de la bola inc a ; Lo incrementa ld (countLoopBall), a ; Lo carga en memoria cp $06 ; Comprueba si ha llegado a 6 jr nz, loop_paddle ; Si no ha llegado a 6 salta call MoveBall ; Mueve la bola ld a, ZERO ; Pone el contador a 0 ld (countLoopBall), a ; Lo carga en memoria loop_paddle: ld a, (countLoopPaddle) ; Carga el contador de vueltas de las palas inc a ; Lo incrementa ld (countLoopPaddle), a ; Lo carga en memoria cp $02 ; Comprueba si ha llegado a 2 jr nz, loop_continue ; Si no ha llegado a 2 salta call ScanKeys ; Escanea las teclas pulsadas call MovePaddle ; Mueva las palas ld a, ZERO ; Pone el contador a 0 ld (countLoopPaddle), a ; Lo carga en memoria loop_continue: call CheckBallCross ; Evalúa si hay colisión entre la bola y las palas call PrintBall ; Pinta la bola call ReprintLine ; Repinta la línea ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 call PrintPaddle ; Pinta la pala 1 ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 call PrintPaddle ; Pinta la pala 2 jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador de vueltas de la bola countLoopPaddle: db $00 ; Contador de vueltas de las palas end $8000

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x07 Ensamblador ZX Spectrum Pong – Detección de colisiones [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a empezar con la detección de colisiones, para que la bola rebote contra las palas.

Ensamblador ZX Spectrum Pong – Detección de colisiones

Creamos la carpeta Paso07 y copiamos desde la carpeta Paso06 los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm.

A partir de aquí, vamos a utilizar todo lo que hemos implementando hasta ahora, evolucionándolo.

Vamos a implementar la detección de colisiones de la bola con las palas. Para ello necesitamos definir la columna en la que se produce dicha colisión, lo que vamos a hacer en Sprite.asm.

CROSS_LEFT: EQU $01 CROSS_RIGHT: EQU $1d

Para comprobar la colisión en la coordenada X, vamos a usar la columna. Para comprobar la colisión en la coordenada Y vamos a usar tercio, línea y scanline.

GetPtrY

Como hemos visto en entregas anteriores, la composición de la coordenada Y se encuentra en dos bytes distintos (010T TSSS LLLC CCCC), por lo que vamos a implementar una rutina que reciba una posición de memoria de la pantalla y devuelva la coordenada Y (TTLLLSSS).

La rutina la vamos a implementar en Video.asm, tras la rutina Cls, y recibe la posición de memoria de la pantalla en HL y devuelve la coordenada Y obtenida en A.

GetPtrY: ld a, h and $18 rlca rlca rlca ld e, a

Cargamos el tercio y scanline en A, LD A, H, nos quedamos con el tercio, AND $18, lo pasamos a los bits 6 y 7, RLCA RLCA RLCA, y cargamos el resultado en E, LD E, A.

ld a, h and $07 or e ld e, a

Volvemos a cargar tercio y scanline en A, LD A, H, nos quedamos con el scanline, AND $07, le agregamos el tercio, OR E, y cargamos el resultado en E, LD E, A.

ld a, l and $e0 rrca rrca or e ret

Cargamos la línea y la columna en A, LD A, L, nos quedamos con la línea, AND $E0, ponemos el valor en los bits 3 a 5, RRCA RRCA, y le añadimos tercio y scanline, OR E.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Obtiene tercio, línea y scanline de una posición de memoria. ; Entrada: HL = Posición de memoria. ; Salida: A = Tercio, línea y scanline obtenido. ; Altera el valor de los registros AF y E. ; ----------------------------------------------------------------------------- GetPtrY: ld a, h ; Carga en A el valor de H (tercio y scanline) and $18 ; Se queda con el tercio rlca rlca rlca ; Pasa el valor del tercio a los bits 6 y 7 ld e, a ; Carga el valor en E ld a, h ; Carga en A el valor de H (tercio y scanline) and $07 ; Se queda con el scanline or e ; Lo mezcla con E ld e, a ; Carga el valor en E TT***SSS ld a, l ; Carga en A el valor de L (línea y columna) and $e0 ; Se queda con la línea rrca rrca ; Pasa el valor a los bits 3 a 5 or e ; Lo mezcla con E (TTLLLSSS) ret

Este tipo de conversión ya la hicimos en la rutina checkVerticalLimit, pero al ser necesaria en más de una rutina, la hemos implementado como una rutina aparte.

Para probarla, vamos a modificar la rutina checkVerticalLimit, sustituyendo casi toda ella por una llamada a GetPtrY, quedando de la siguiente manera.

; ----------------------------------------------------------------------------- ; Evalúa si se ha alcanzado el límite vertical. ; Entrada: A = Límite vertical (TTLLLSSS). ; HL = Posición actual (010TTSSS LLLCCCCC). ; Altera el valor de los registros AF y BC. ; ---------------------------------------------------------------------------- checkVerticalLimit: ld b, a ; Guarda el valor de A en B call GetPtrY ; Obtiene la coordenada Y (TTLLLSSSS) ; de la posición actual cp b ; Lo compara con B. B = valor original de A = Límite vertical ret

Compilamos, cargamos en el emulador y comprobamos que no se ha roto nada.

Detección de colisiones

Ahora vamos a implementar la detección de colisiones, en el archivo Game.asm.

Empezamos por la rutina que evalúa si hay colisión en el eje X. Esta rutina recibe en C la columna donde se produce la colisión, y activa el flag Z si se ha producido.

CheckCrossX: ld a, (ballPos) and $1f cp c ret

Cargamos la posición de la bola en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y comparamos el valor resultante con la columna de colisión, CP C.

El siguiente paso es implementar la rutina que evalúa si hay colisión en el eje Y. Esta rutina recibe en HL la posición de la pala, y activa el flag Z si hay colisión.

CheckCrossY: call GetPtrY inc a ld c, a

Obtenemos la coordenada Y de la pala, CALL GetPtrY. Como el primer scanline de la pala es blanco, no lo tenemos en cuenta para las colisiones, así que pasamos al siguiente, INC A, y cargamos el valor en C, LD C, A.

ld hl, (ballPos) call GetPtrY ld b, a

Cargamos la posición de la bola en HL, LD HL, (ballPos), obtenemos la coordenada Y, CALL GetPtrY, y cargamos el valor en B, LD B, A.

add a, $04 sub c ret c

En A tenemos la coordenada Y de la bola. Apuntamos A al penúltimo scanline de la bola, el último que no es blanco, ADD A, $04, le restamos la coordenada Y de la pala, SUB C, y si hay acarreo salimos, RET C, ya que la bola pasa por encima de la pala.

Si no hemos salido, tenemos que comprobar si la bola pasa por debajo de la pala.

ld a, c add a, $16 ld c, a ld a, b inc a sub c ret nc xor a ret

Cargamos la coordenada Y de la pala en A, LD A, C, le sumamos $16 (22) para posicionarnos en el penúltimo scanline, el último que no está a 0, ADD A, $16, y cargamos el valor en C, LD C, A.

Cargamos la coordenada Y de la bola en A, LD A, B, la apuntamos al scanline 1, el primero que no está a 0, INC A, y le restamos la coordenada Y de la pala, SUB C.

Si tras la resta no hay acarreo salimos, RET NC, pues o bien la bola pasa por debajo, o colisiona en el último scanline de la pala, que está en la misma coordenada Y que el primer scanline de la bola, y al restar se activa el flag Z.

Si hay acarreo, la bola colisiona con el resto de la pala, por lo que activamos el flag Z, XOR A, y salimos, RET.

El siguiente paso es implementar la rutina principal a la que vamos a llamar para comprobar si hay colisión, en cuyo caso vamos a realizar las acciones necesarias.

CheckBallCross: ld a, (ballSetting) and $40 jr nz, checkBallCross_left

Cargamos la configuración de la bola en A, LD A, (ballSetting), y nos quedamos con el bit 6, AND $40, que especifica si la bola va hacia la derecha o hacia la izquierda. Si el bit 6 está a 1, la bola va hacia la izquierda y saltamos a comprobar si se produce colisión con la pala del jugador 1, JR NZ, checkBallCross_left.

Si no se ha producido el salto, la bola va hacia la derecha y comprobamos si hay colisión con la pala del jugador 2.

checkBallCross_right: ld c, CROSS_RIGHT call CheckCrossX ret nz ld hl, (paddle2pos) call CheckCrossY ret nz

Cargamos la columna de colisión en C, LD C, CROSS_RIGHT, evaluamos si se produce colisión en el eje X, CALL CheckCrossX. Si no se produce la colisión salimos de la rutina, RET NZ.

Si se ha producido colisión en el eje X, cargamos la posición de la pala 2 en HL, LD HL, (paddle2pos), y evaluamos si se produce colisión en el eje Y, CALL CheckCrossY. Si no se produce colisión, salimos de la rutina, RET NZ.

Si no hemos salido de la rutina, se ha producido colisión.

ld a, (ballSetting) or $40 ld (ballSetting), a ld a, $ff ld (ballRotation), a ret

Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos el bit 6 a 1 para cambiar la dirección de la bola hacia la izquierda, OR $40, y cargamos el valor en memoria, LD (ballSetting), A.

Cargamos -1 en A, LD A, $FF, cambiamos la rotación de la bola, LD (ballRotation), A, y salimos de la rutina, RET.

La comprobación de si hay colisión con la pala del jugador 1 es similar a lo visto anteriormente, por lo que solo vamos a poner el código y a marcar las diferencias, sin entrar en detalle.

checkBallCross_left: ; ¡CAMBIO! ld c, CROSS_LEFT ; ¡CAMBIO! call CheckCrossX ret nz ld hl, (paddle1pos) ; ¡CAMBIO! call CheckCrossY ret nz ld a, (ballSetting) and $bf ; ¡CAMBIO! ld (ballSetting), a ld a, $01 ; ¡CAMBIO! ld (ballRotation), a ret

El aspecto final de las rutinas de comprobación de colisiones entre las palas y la bola es el siguiente.

; ----------------------------------------------------------------------------- ; Evalúa si hay colisión entre la bola y las palas. ; Altera el valor de los registros AF, C y HL. ; ----------------------------------------------------------------------------- CheckBallCross: ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A and $40 ; Se queda con el bit 6 (izquierda/derecha) jr nz, checkBallCross_left ; Si no está a 0 va hacia la izquierda y salta checkBallCross_right: ld c, CROSS_RIGHT ; Carga la columna de colisión en C call CheckCrossX ; Evalúa si se produce colisión en el eje X ret nz ; Si no se produce, fin de la rutina ld hl, (paddle2pos) ; Carga la posición de la pala 2 en HL call CheckCrossY ; Evalúa si se produce colisión en el eje Y ret nz ; Si no se produce colisión, fin de la rutina ; Si llega aquí hay colisión ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A or $40 ; Cambia la dirección, la pone hacia la izquierda ld (ballSetting), a ; Carga el valor en memoria ld a, $ff ; Cambia la rotación de la bola ld (ballRotation), a ; La carga en memoria ret ; Fin de la rutina checkBallCross_left: ; La bola va hacia la izquierda ld c, CROSS_LEFT ; Carga la columna de colisión en C call CheckCrossX ; Evalúa si se produce colisión en el eje X ret nz ; Si no se produce, fin de la rutina ld hl, (paddle1pos) ; Carga la posición de la pala 1 en HL call CheckCrossY ; Evalúa si se produce colisión en el eje Y ret nz ; Si no se produce colisión, fin de la rutina ; Si llega aquí hay colisión ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A and $bf ; Cambia la dirección, la pone hacia la derecha ld (ballSetting), a ; Carga el valor en memoria ld a, $01 ; Cambia la rotación de la bola ld (ballRotation), a ; La carga en memoria ret ; Fin de la rutina ; ----------------------------------------------------------------------------- ; Evalúa si la bola colisiona en el eje X con la pala. ; Entrada: C = Columna dónde se produce la colisión. ; Salida: Z = Colisiona. ; NZ = No colisiona. ; Altera el valor de los registros AF. ; ----------------------------------------------------------------------------- CheckCrossX: ld a, (ballPos) ; Carga la línea y columna donde está la bola and $1f ; Se queda con la columna cp c ; Lo compara con la columna de colisión ret ; ----------------------------------------------------------------------------- ; Evalúa si la bola colisiona en el eje Y con la pala. ; Entrada: HL = Posición de la pala ; Salida: Z = Colisiona. ; NZ = No colisiona. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- CheckCrossY: call GetPtrY ; Obtiene la posición vertical de la pala (TTLLLSSS) ; La posición devuelta apunta al primer scanline de la pala que está a 0, ; apunta al siguiente inc a ld c, a ; Carga el valor en C ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene la posición vertical de la bola (TTLLLSSS) ld b, a ; Carga el valor en B ; Comprueba si la bola pasa por encima de la pala ; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0 ; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º add a, $04 ; Apunta la posición de la bola al 5º scanline sub c ; Resta a la posición de la bola, la posición de la pala ret c ; Si hay acarreo sale porque la bola pasa por encima ; Comprueba si la bola pasa por debajo de la pala ld a, c ; Carga la posición vertical de la pala en A add a, $16 ; Le suma 22 para apuntar al penúltimo scanline, ; último que no es 0 ld c, a ; Lo vuelve a cargar en C ld a, b ; Carga la posición vertical de la bola inc a ; Le suma 1 para apuntar el scanline 1, primero que no es 0 sub c ; Resta a la posición de la bola, la posición de la pala ret nc ; Si no hay acarreo la bola pasa por debajo de la pala ; o colisiona en el último scanline. ; En este último caso se activa el flag Z ; Hay colisión xor a ; Activa el flag Z ret

Ahora ya solo nos queda ver si lo que hemos implementado hace lo que pretendemos.

Abrimos el archivo Main.asm y justo debajo de la etiqueta loop_continue, añadimos la siguiente línea.

call CheckBallCross

Compilamos y cargamos en el emulador para ver los resultados. Si todo ha ido bien, la bola choca contra las palas; la detección de colisiones está funcionando.

Ensamblador ZX Spectrum - ColisionesEnsamblador ZX Spectrum – colisiones

El aspecto final del archivo Main.asm es el siguiente.

; Detección de colisiones org $8000 ; ----------------------------------------------------------------------------- ; Entrada al programa ; ----------------------------------------------------------------------------- Main: ld a, $00 ; A = 0 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Imprime la línea central call PrintBorder ; Imprime el borde del campo Loop: ld a, (countLoopBall) ; Carga el contador de vueltas de la bola inc a ; Lo incrementa ld (countLoopBall), a ; Lo carga en memoria cp $06 ; Comprueba si ha llegado a 6 jr nz, loop_paddle ; Si no ha llegado a 6 salta call MoveBall ; Mueve la bola ld a, ZERO ; Pone el contador a 0 ld (countLoopBall), a ; Lo carga en memoria loop_paddle: ld a, (countLoopPaddle) ; Carga el contador de vueltas de las palas inc a ; Lo incrementa ld (countLoopPaddle), a ; Lo carga en memoria cp $02 ; Comprueba si ha llegado a 2 jr nz, loop_continue ; Si no ha llegado a 2 salta call ScanKeys ; Escanea las teclas pulsadas call MovePaddle ; Mueva las palas ld a, ZERO ; Pone el contador a 0 ld (countLoopPaddle), a ; Lo carga en memoria loop_continue: call CheckBallCross ; Evalúa si hay colisión entre la bola y las palas call PrintBall ; Pinta la bola call ReprintLine ; Repinta la línea ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 call PrintPaddle ; Pinta la pala 1 ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 call PrintPaddle ; Pinta la pala 2 jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador de vueltas de la bola countLoopPaddle: db $00 ; Contador de vueltas de las palas end $8000

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x06 Ensamblador ZX Spectrum Pong – Campo, palas, bola y temporización [Espamatica] [Leer]


Llegamos a una nueva entrega de Ensamblador ZX Spectrum Pong, y con ella inauguramos la segunda parte del tutorial. En esta entrega vamos a dibujar el campo, las palas, la bola, lo vamos a mover todo y a temporizar.

Ensamblador ZX Spectrum Pong – Campo, palas, bola y temporización

Creamos la carpeta Paso06 y copiamos desde la carpeta Paso05 los archivos Game.asm, Sprite.asm y Video.asm, y desde la carpeta Paso03 el archivo Controls.asm. También creamos el archivo Main.asm.

Bucle principal

Empezamos editando el archivo Main.asm, indicando la posición de carga, poniendo el borde en negro, limpiando la pantalla, pintando la línea central y haciendo un bucle infinito para no volver al Basic.

También vamos a incluir el resto de ficheros e indicarle a PASMO dónde llamar al cargar el programa.

org $8000 Main: ld a, $00 out ($fe), a call Cls call PrintLine Loop: jr Loop include "Controls.asm" include "Game.asm" include "Sprite.asm" include "Video.asm" end $8000

Compilamos y vemos el resultado en el emulador.

Ensamblador ZX Spectrum Pong - Bucle principalEnsamblador ZX Spectrum, bucle principal
Borde del campo

El siguiente paso es pintar el borde del campo.

Incluimos al inicio del fichero Sprite.asm una nueva constante.

FILL: EQU $ff

La rutina para pintar el borde la implementamos en el archivo Video.asm, antes de la rutina PrintLine.

PrintBorder: ld hl, $4100 ld de, $56e0 ld b, $20 ld a, FILL

Cargamos en HL la dirección del tercio 0, línea 0, scanline 1, LD HL, $4100, cargamos en DE la dirección del tercio 2, línea 7, scanline 6, LD DE, $56E0, y en B las 32 columnas en las que pintar el borde, LD B, $20. Por último, cargamos el sprite del borde en A, LD A, FILL.

Implementamos el bucle para pintar el borde.

printBorder_loop: ld (hl), a ld (de), a inc l inc e djnz printBorder_loop ret

Pintamos el sprite del borde en la dirección a la que apunta HL, LD (HL), A, y hacemos lo mismo con la dirección a la que apunta DE, LD (DE), A.

Apuntamos HL a la siguiente columna, INC L, y hacemos los mismo con DE, INC E. Repetimos hasta que B valga 0, DJNZ printBorder_loop, tras lo cual salimos de la rutina, RET.

El aspecto final de la rutina PrintBorder es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el borde del campo. ; Altera el valor de los registros AD, B, DE y HL. ; ----------------------------------------------------------------------------- PrintBorder: ld hl, $4100 ; Carga en HL la dirección del tercio 0, línea 0 y scanline 1 ld de, $56e0 ; Carga en DE la dirección del tercio 2, línea 7 y scanline 6 ld b, $20 ; Carga en B las 32 columnas en las que pintar ld a, FILL ; Carga en A el byte a pintar printBorder_loop: ld (hl), a ; Pinta en la dirección apuntada por HL ld (de), a ; Pinta en la dirección apuntada por DE inc l ; Apunta HL a la siguiente columna inc e ; Apunta DE a la siguiente columna djnz printBorder_loop ; Bucle hasta que B llegue a 0 ret

Para probar esta rutina, volvemos al archivo Main.asm y tras la llamada a PrintLine ponemos la llamada a la nueva rutina.

call PrintBorder

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - Borde del campo.pngEnsamblador ZX Spectrum, borde del campo

Ya tenemos dibujado el campo donde se va a desarrollar la acción.

Introducimos la bola

Vamos a introducir la bola en nuestro campo. Como la vamos a estar moviendo y pintando constantemente, vamos a introducir las llamadas a mover y pintar la bola dentro del bucle, entre Loop y JR Loop.

call MoveBall call PrintBall

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - La bola borra el campoEnsamblador ZX Spectrum, la bola borra el campo

Al ver el resultado, observamos dos problemas: la bola borra la línea central y el borde, y se mueve a una velocidad endiablada.

Velocidad de la bola

Lo primero que vamos a abordar es la velocidad a la que se mueve la bola.

En el paso anterior poníamos un HALT para esperar el refresco de la pantalla, pero esto hace que vaya demasiado lenta. Para reducir la velocidad de la bola, vamos a hacer que no se mueva cada vez que pase por el bucle; se va a mover una de cada N veces.

Seguimos en el archivo Main.asm y, antes de END $8000, declaramos la variable donde vamos a llevar la cuenta de las veces que se ha pasado por el bucle.

countLoopBall: db $00

Y ahora vamos a implementar la parte en la que comprobamos si ha pasado las veces suficientes para que movamos la bola, justo después de la etiqueta Loop.

ld a, (countLoopBall) inc a ld (countLoopBall), a cp $0f jr nz, loop_continue call MoveBall

Cargamos el contador donde guardamos las veces que se ha pasado por el bucle sin mover la bola en A, LD A, (countLoopBall), lo incrementamos, INC A, y lo guardamos en memoria, LD (countLoopBall), A.

Comparamos si el contador ha llegado al número de veces necesarias para mover la bola, CP $0F, y si no ha llegado salta, JR NZ, loop_continue.

Si ya hemos llegado al número de veces necesarias de pasadas por el bucle para mover la bola, la movemos, CALL MoveBall.

La etiqueta loop_continue es nueva y la vamos a poner justo encima de la llamada a PrintBall.

loop_continue: call PrintBall

Tenemos que hacer una última cosa. Si el contador ha llegado al número de veces necesario para mover la bola, después de moverla hay que volver a poner el contador a cero, de lo contrario habría que esperar otras 255 veces, en lugar de las que hemos puesto.

Añadimos las siguientes líneas después de la llamada a MoveBall y antes de la etiqueta loop_continue para poner el contador a 0.

ld a, ZERO ld (countLoopBall), a

La implementación de Main.asm quedaría así.

org $8000 Main: ld a, $00 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Pinta la línea central call PrintBorder ; Pinta el borde Loop: ld a, (countLoopBall) ; Carga en A el contador de la bola inc a ; Incrementa el contador ld (countLoopBall), a ; Carga el valor en memoria cp $0f ; Comprueba si el contador ha llegado a 15 jr nz, loop_continue ; Si no ha llegado, salta call MoveBall ; Mueve la posición de la bola ld a, ZERO ; Pone A = 0 ld (countLoopBall), a ; Carga el valor en memoria loop_continue: call PrintBall ; Pinta la bola jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador para controlar cuando se mueve la bola end $8000

Compilamos y vemos el resultado en el emulador. Esta vez sí vemos como se mueve la bola a una velocidad más aceptable.

No hemos definido una constante para la comparación con el contador de la bola ya que, en un futuro, la velocidad será variable.

Repintamos línea central y borde

Ahora vamos a abordar el problema de las partes que va borrando la bola a su paso, y vamos a empezar por la línea central.

En una primera aproximación, vamos a repintar la parte de la línea que coincide en la coordenada Y con la bola, sin importar si la bola está pasando por encima o no. Parece innecesario, pero nos va a ayudar a temporizar.

Abrimos el archivo Video.asm e implementamos después de la rutina PrintLine.

ReprintLine: ld hl, (ballPos) ld a, l and $e0 or $10 ld l, a

Cargamos la posición de la bola en HL, LD HL, (ballPos), cargamos línea y columna en A, LD A, L, nos quedamos con el valor de la línea, AND $E0, ponemos la columna a 16, que es donde está la línea vertical, OR $10, y cargamos el valor en L, LD L, A.

Vamos a repintar 6 scanlines, que son los mismos que tiene la bola.

ld b, $06 reprintLine_loop: ld a, h

Cargamos en B el número de scanlines que se repintan, LD B, $06, y cargamos tercio y scanline en A, LD A, H.

Para pintar la línea, en los scanlines 0 y 7 pintábamos en blanco, y en el resto la parte visible de la línea.

and $07 cp $01 jr c, reprintLine_00 cp $07 jr z, reprintLine_00

Nos quedamos con la parte del scanline, AND $07, y comprobamos si es 1, CP $01. Si el scanline en menor que 1 saltamos, JR C, reprintLine_00, en el caso contrario comprobamos si es 7, CP $07. Si el scanline es 7 saltamos, JR Z, reprintLine_00.

Si no hemos saltado, el scanline está entre 1 y 6.

ld c, LINE jr reprintLine_loopCont

Cargamos el sprite de la línea en C, LD C, LINE, y saltamos, JR reprintLine_loopCont.

Si anteriormente saltamos, el scanline es 0 o 7.

reprintLine_00: ld c, ZERO

Cargamos el sprite en blanco en C, LD C, ZERO, y pintamos lo que corresponda.

reprintLine_loopCont: ld a, (hl) or c ld (hl), a call NextScan djnz reprintLine_loop ret

Cargamos el valor de la dirección de memoria del byte que vamos a repintar en A, LD A, (HL), le añadimos los píxeles del repintado de la línea, OR C, y lo pintamos en pantalla, LD (HL), A. Calculamos la dirección de memoria del scanline siguiente, CALL NextScan, y repetimos la operación hasta que B valga 0, DJNZ reprintLine_loop. Por último, salimos, RET.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Repinta la línea central. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- ReprintLine: ld hl, (ballPos) ; Carga en HL la posición de la bola ld a, l ; Carga la línea y columna en A and $e0 ; Se queda con la línea or $10 ; Pone la columna a 16 ($10) ld l, a ; Carga el valor en L. HL = Posición inicial repintar ld b, $06 ; Se repintan 6 scanlines reprintLine_loop: ld a, h ; Carga tercio y scanline en A and $07 ; Se queda con el scanline ; Si está en los scanline 0 o 7 pinta ZERO ; Si está en los scanline 1, 2, 3, 4, 5 o 6 pinta LINE cp $01 ; Comprueba si está en scanline 1 o superior jr c, reprintLine_00 ; Si está por debajo, pinta $00 cp $07 ; Comprueba si está en scanline 7 jr z, reprintLine_00 ; Si es así, pinta ZERO ld c, LINE ; Esta en scanline 1 a 6, pinta LINE jr reprintLine_loopCont ; Salta reprintLine_00: ld c, ZERO ; Está en scanline 0 o 7, pinta ZERO reprintLine_loopCont: ld a, (hl) ; Obtiene los pixeles de la posición actual or c ; Los mezcla con C ld (hl), a ; Pinta el resultado en la posición actual call NextScan ; Obtiene el scanline siguiente djnz reprintLine_loop ; Hasta que B = 0 ret

Podemos ahorrar 4 bytes y 19 ciclos de reloj modificando seis líneas de la rutina. Lo dejamos en vuestras manos y veremos la forma de hacerlo al final del tutorial.

Ya solo queda probar lo que hemos implementado, para lo cual abrimos el archivo Main.asm y después de la llamada a PrintBall incluimos la llamada a ReprintLine.

call ReprintLine

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - Repinta la lneaEnsamblador ZX Spectrum, repinta la línea

La línea central ya no se borra, pero podemos apreciar que la velocidad de la bola ha disminuido. Hay que tener en cuenta que ahora realizamos más operaciones que antes. Según avancemos iremos ajustando la velocidad de la bola.

Vamos ahora a evitar que se borre el borde, para lo cual vamos a modificar los límites superior e inferior de la bola, en el fichero Sprite.asm.

BALL_BOTTOM: EQU $b8 BALL_TOP: EQU $02

Compilamos, cargamos en el emulador y comprobamos que ya no se borra el borde.

Incluimos las palas

Ahora vamos a empezar con las palas. Volvemos a Main.asm y añadimos las siguientes líneas entre CALL ReprintLine y JR Loop.

ld hl, (paddle1pos) call PrintPaddle ld hl, (paddle2pos) call PrintPaddle

Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), y la pintamos, CALL PrintPaddle. Hacemos lo mismo con la pala 2.

Como se puede apreciar, las palas se pintan en todas las iteraciones del bucle, al igual que la bola y el repintado de línea.

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - No borra el bordeEnsamblador ZX Spectrum, no borra el borde

Se dibujan las palas y la bola no las borra al pasar. También se aprecia que ahora la bola va mucho más lenta, debido a que hacemos más operaciones en cada iteración del bucle.

Para que la bola vuelva a ir más rápida, vamos a cambiar en Main.asm el valor que tenía que alcanzar el contador para que la bola se moviese.

ld (countLoopBall), a cp $06 ; ¡CAMBIO! jr nz, loop_continue

Compilamos, cargamos en el emulador y comprobamos que la bola vuelve a ir más rápido.

Movemos las palas

Ahora vamos a implementar la rutina para mover las palas; ya vimos como hacerlo en el paso 03. Editamos el archivo Game.asm y vamos al final del mismo.

La rutina que vamos a implementar, recibe en el registro D las pulsaciones de las teclas de control.

MovePaddle: bit $00, d jr z, movePaddle_1Down

Evaluamos si se ha pulsado la tecla arriba del jugador 1, BIT $00, D. Si no se ha pulsado saltamos a comprobar si se ha pulsado la tecla abajo, JR Z, movePaddle_1Down.

Si no salta, se ha pulsado la tecla arriba del jugador 1.

ld hl, (paddle1pos) ld a, PADDLE_TOP call CheckTop jr z, movePaddle_2Up

Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), el límite superior para las palas en A, LD A, PADDLE_TOP, y comprobamos si se ha alcanzado, CALL CheckTop. Si se ha alcanzado el límite, saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.

Si no se ha alcanzado el límite superior, movemos la pala 1.

call PreviousScan ld (paddle1pos), hl jr movePaddle_2Up

Calculamos la nueva posición para la pala 1, CALL PreviousScan, la cargamos en memoria, LD (paddle1pos), HL, y saltamos a comprobar los controles del jugador 2, JR movePaddle_2Up.

Si no se ha pulsado la tecla arriba del jugador 1, comprobamos si se ha pulsado la tecla abajo.

movePaddle_1Down: bit $01, d jr z, movePaddle_2Up

Evaluamos si se ha pulsado la tecla abajo del jugador 1, BIT $01, D. Si no se ha pulsado saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.

Si no salta, se ha pulsado la tecla abajo del jugador 1.

ld hl, (paddle1pos) ld a, PADDLE_BOTTOM call CheckBottom jr z, movePaddle_2Up

Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), el límite inferior para las palas en A, LD A, PADDLE_BOTTOM, y comprobamos si se ha alcanzado, CALL CheckBottom. Si se ha alcanzado el límite saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.

Si no se ha alcanzado el límite inferior, mueve la pala 1.

call NextScan ld (paddle1pos), hl

Calculamos la nueva posición para la pala 1, CALL NextScan, y la cargamos en memoria, LD (paddle1pos), HL.

Hacemos las comprobaciones con los controles del jugador 2. Dada la semejanza, simplemente marcamos los cambios con respecto a la comprobación del jugador 1.

movePaddle_2Up: bit $02, d ; ¡CAMBIO! jr z, movePaddle_2Down ; ¡CAMBIO! ld hl, (paddle2pos) ; ¡CAMBIO! ld a, PADDLE_TOP call CheckTop jr z, movePaddle_End ; ¡CAMBIO! call PreviousScan ld (paddle2pos), hl ; ¡CAMBIO! jr movePaddle_End ; ¡CAMBIO! movePaddle_2Down: ; ¡CAMBIO! bit $03, d ; ¡CAMBIO! jr z, movePaddle_End ; ¡CAMBIO! ld hl, (paddle2pos) ; ¡CAMBIO! ld a, PADDLE_BOTTOM call CheckBottom jr z, movePaddle_End ; ¡CAMBIO! call NextScan ld (paddle2pos), hl ; ¡CAMBIO! movePaddle_End: ; ¡NUEVA! ret ; ¡NUEVA!

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Calcula la posición de las palas para moverlas. ; Entrada: D = Pulsaciones de los controles ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- MovePaddle: bit $00, d ; Evalúa si se ha pulsado la A jr z, movePaddle_1Down ; Si no se ha pulsado salta ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 ld a, PADDLE_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, movePaddle_2Up ; Si se ha alcanzado, salta call PreviousScan ; Obtiene el scanline anterior a la posición de la pala 1 ld (paddle1pos), hl ; Carga en memoria la nueva posición de la pala 1 jr movePaddle_2Up ; Salta movePaddle_1Down: bit $01, d ; Evalúa si se ha pulsado la Z jr z, movePaddle_2Up ; Si no se ha pulsado salta ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 ld a, PADDLE_BOTTOM ; Carga en A el margen inferior call CheckBottom ; Evalúa si se ha alcanzado el margen inferior jr z, movePaddle_2Up ; Si se ha alcanzado, salta call NextScan ; Obtiene el scanline siguiente a la posición de la pala 1 ld (paddle1pos), hl ; Carga en memoria la nueva posición de la pala 1 movePaddle_2Up: bit $02, d ; Evalúa si se ha pulsado el 0 jr z, movePaddle_2Down ; Si no se ha pulsado salta ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 ld a, PADDLE_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, movePaddle_End ; Si se ha alcanzado, salta call PreviousScan ; Obtiene el scanline anterior a la posición de la pala 2 ld (paddle2pos), hl ; Carga en memoria la nueva posición de la pala 2 jr movePaddle_End ; Salta movePaddle_2Down: bit $03, d ; Evalúa si se ha pulsado la O jr z, movePaddle_End ; Si no se ha pulsado salta ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 ld a, PADDLE_BOTTOM ; Carga en A el margen inferior call CheckBottom ; Evalúa si se ha alcanzado el margen inferior jr z, movePaddle_End ; Si se ha alcanzado, salta call NextScan ; Obtiene el scanline siguiente a la posición de la pala 2 ld (paddle2pos), hl ; Carga en memoria la nueva posición de la pala 2 movePaddle_End: ret

Podemos ahorrar 2 bytes y 2 ciclos de reloj, de la misma forma que en la entrega anterior. Esta vez no daremos la solución al final del tutorial, ya que es similar a la que se verá para la entrega anterior.

Para terminar, vamos a implementar en Main.asm las llamadas a esta rutina, dentro de nuestro bloque infinito, justo encima de la etiqueta loop_continue.

loop_paddle: call ScanKeys call MovePaddle

Primero comprobamos las teclas de control que se han pulsado, CALL ScanKeys, y luego movemos las palas, CALL MovePaddle.

También tenemos que cambiar la etiqueta a la que salta cuando la bola no se mueve, cuatro líneas más arriba.

cp $06 jr nz, loop_paddle ; ¡CAMBIO! call MoveBall

Compilamos y probamos en el emulador.

Ensamblador ZX Spectrum - Palas borrando bordeEnsamblador ZX Spectrum, palas borrando borde

Observamos dos problemas:

  1. Las palas borran el borde.
  2. Las palas se mueven muy rápido y son difíciles de controlar.

Para resolver el primer problema vamos a modificar las constantes que marcan los límites superior e inferior de las palas, que están en Sprite.asm.

PADDLE_BOTTOM: EQU $a6 PADDLE_TOP: EQU $02

Compilamos, cargamos en el emulador y comprobamos que ya no se borra el borde.

Ensamblador ZX Spectrum - Palas no borran bordeEnsamblador ZX Spectrum, palas no borran borde

Velocidad de las palas

Para reducir la velocidad del movimiento de las palas, vamos a usar la misma técnica que usamos con la bola, no vamos a mover las palas en cada iteración del bucle.

Lo primero es declarar la variable que usaremos como contador, lo que hacemos antes de END $8000.

countLoopPaddle: db $00

Ahora, justo debajo de la etiqueta loop_paddle, implementamos la comprobación del contador.

ld a, (countLoopPaddle) inc a ld (countLoopPaddle), a cp $02 jr nz, loop_continue call ScanKeys call MovePaddle

Cargamos el contador en A, LD A, (countLoopPaddle), lo incrementamos, INC A, y lo cargamos en memoria, LD (countLoopPaddle), A. Evaluamos si han pasado las veces que hemos definido para mover las palas, CP $02, y si no es así saltamos, JR NZ, loop_continue.

Si no salta, comprobamos si se ha pulsado alguna tecla de control, CALL ScanKeys, y movemos las palas, CALL MovePaddle, y tal y como hicimos con la bola, hay que poner el contador a cero. Añadimos las líneas siguiente antes de la etiqueta loop_continue.

ld a, ZERO ld (countLoopPaddle), a

Cargamos 0 en A, LD A, ZERO, y lo cargamos en memoria, LD (countLoopPaddle), A, poniendo el contador a 0.

Compilamos y cargamos en el emulador. Ahora el control de las palabras es menos rápido y más preciso.

El código final de Main.asm es el siguiente.

; Pintado de campo, movimiento de palas y bola y temporización org $8000 ; ----------------------------------------------------------------------------- ; Entrada al programa ; ----------------------------------------------------------------------------- Main: ld a, $00 ; A = 0 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Imprime la línea central call PrintBorder ; Imprime el borde del campo Loop: ld a, (countLoopBall) ; Carga el contador de vueltas de la bola inc a ; Lo incrementa ld (countLoopBall), a ; Lo carga en memoria cp $06 ; Comprueba si ha llegado a 6 jr nz, loop_paddle ; Si no ha llegado a 4 salta call MoveBall ; Mueve la bola ld a, ZERO ; Pone el contador a 0 ld (countLoopBall), a ; Lo carga en memoria loop_paddle: ld a, (countLoopPaddle) ; Carga el contador de vueltas de las palas inc a ; Lo incrementa ld (countLoopPaddle), a ; Lo carga en memoria cp $02 ; Comprueba si ha llegado a 2 jr nz, loop_continue ; Si no ha llegado a 2 salta call ScanKeys ; Escanea las teclas pulsadas call MovePaddle ; Mueva las palas ld a, ZERO ; Pone el contador a 0 ld (countLoopPaddle), a ; Lo carga en memoria loop_continue: call PrintBall ; Pinta la bola call ReprintLine ; Repinta la línea ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 call PrintPaddle ; Pinta la pala 1 ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 call PrintPaddle ; Pinta la pala 2 jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador de vueltas de la bola countLoopPaddle: db $00 ; Contador de vueltas de las palas end $8000

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, campo, palas, bola y temporización

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x06 Ensamblador ZX Spectrum Pong – Campo, palas, bola y temporización [Espamatica] [Leer]


Llegamos a una nueva entrega de Ensamblador ZX Spectrum Pong, y con ella inauguramos la segunda parte del tutorial. En esta entrega vamos a dibujar el campo, las palas, la bola, lo vamos a mover todo y a temporizar.

Ensamblador ZX Spectrum Pong – Campo, palas, bola y temporización

Creamos la carpeta Paso06 y copiamos desde la carpeta Paso05 los archivos Game.asm, Sprite.asm y Video.asm, y desde la carpeta Paso03 el archivo Controls.asm. También creamos el archivo Main.asm.

Bucle principal

Empezamos editando el archivo Main.asm, indicando la posición de carga, poniendo el borde en negro, limpiando la pantalla, pintando la línea central y haciendo un bucle infinito para no volver al Basic.

También vamos a incluir el resto de ficheros e indicarle a PASMO dónde llamar al cargar el programa.

org $8000 Main: ld a, $00 out ($fe), a call Cls call PrintLine Loop: jr Loop include "Controls.asm" include "Game.asm" include "Sprite.asm" include "Video.asm" end $8000

Compilamos y vemos el resultado en el emulador.

Ensamblador ZX Spectrum Pong - Bucle principalEnsamblador ZX Spectrum, bucle principal
Borde del campo

El siguiente paso es pintar el borde del campo.

Incluimos al inicio del fichero Sprite.asm una nueva constante.

FILL: EQU $ff

La rutina para pintar el borde la implementamos en el archivo Video.asm, antes de la rutina PrintLine.

PrintBorder: ld hl, $4100 ld de, $56e0 ld b, $20 ld a, FILL

Cargamos en HL la dirección del tercio 0, línea 0, scanline 1, LD HL, $4100, cargamos en DE la dirección del tercio 2, línea 7, scanline 6, LD DE, $56E0, y en B las 32 columnas en las que pintar el borde, LD B, $20. Por último, cargamos el sprite del borde en A, LD A, FILL.

Implementamos el bucle para pintar el borde.

printBorder_loop: ld (hl), a ld (de), a inc l inc e djnz printBorder_loop ret

Pintamos el sprite del borde en la dirección a la que apunta HL, LD (HL), A, y hacemos lo mismo con la dirección a la que apunta DE, LD (DE), A.

Apuntamos HL a la siguiente columna, INC L, y hacemos los mismo con DE, INC E. Repetimos hasta que B valga 0, DJNZ printBorder_loop, tras lo cual salimos de la rutina, RET.

El aspecto final de la rutina PrintBorder es el siguiente.

; ----------------------------------------------------------------------------- ; Pinta el borde del campo. ; Altera el valor de los registros AD, B, DE y HL. ; ----------------------------------------------------------------------------- PrintBorder: ld hl, $4100 ; Carga en HL la dirección del tercio 0, línea 0 y scanline 1 ld de, $56e0 ; Carga en DE la dirección del tercio 2, línea 7 y scanline 6 ld b, $20 ; Carga en B las 32 columnas en las que pintar ld a, FILL ; Carga en A el byte a pintar printBorder_loop: ld (hl), a ; Pinta en la dirección apuntada por HL ld (de), a ; Pinta en la dirección apuntada por DE inc l ; Apunta HL a la siguiente columna inc e ; Apunta DE a la siguiente columna djnz printBorder_loop ; Bucle hasta que B llegue a 0 ret

Para probar esta rutina, volvemos al archivo Main.asm y tras la llamada a PrintLine ponemos la llamada a la nueva rutina.

call PrintBorder

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - Borde del campo.pngEnsamblador ZX Spectrum, borde del campo

Ya tenemos dibujado el campo donde se va a desarrollar la acción.

Introducimos la bola

Vamos a introducir la bola en nuestro campo. Como la vamos a estar moviendo y pintando constantemente, vamos a introducir las llamadas a mover y pintar la bola dentro del bucle, entre Loop y JR Loop.

call MoveBall call PrintBall

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - La bola borra el campoEnsamblador ZX Spectrum, la bola borra el campo

Al ver el resultado, observamos dos problemas: la bola borra la línea central y el borde, y se mueve a una velocidad endiablada.

Velocidad de la bola

Lo primero que vamos a abordar es la velocidad a la que se mueve la bola.

En el paso anterior poníamos un HALT para esperar el refresco de la pantalla, pero esto hace que vaya demasiado lenta. Para reducir la velocidad de la bola, vamos a hacer que no se mueva cada vez que pase por el bucle; se va a mover una de cada N veces.

Seguimos en el archivo Main.asm y, antes de END $8000, declaramos la variable donde vamos a llevar la cuenta de las veces que se ha pasado por el bucle.

countLoopBall: db $00

Y ahora vamos a implementar la parte en la que comprobamos si ha pasado las veces suficientes para que movamos la bola, justo después de la etiqueta Loop.

ld a, (countLoopBall) inc a ld (countLoopBall), a cp $0f jr nz, loop_continue call MoveBall

Cargamos el contador donde guardamos las veces que se ha pasado por el bucle sin mover la bola en A, LD A, (countLoopBall), lo incrementamos, INC A, y lo guardamos en memoria, LD (countLoopBall), A.

Comparamos si el contador ha llegado al número de veces necesarias para mover la bola, CP $0F, y si no ha llegado salta, JR NZ, loop_continue.

Si ya hemos llegado al número de veces necesarias de pasadas por el bucle para mover la bola, la movemos, CALL MoveBall.

La etiqueta loop_continue es nueva y la vamos a poner justo encima de la llamada a PrintBall.

loop_continue: call PrintBall

Tenemos que hacer una última cosa. Si el contador ha llegado al número de veces necesario para mover la bola, después de moverla hay que volver a poner el contador a cero, de lo contrario habría que esperar otras 255 veces, en lugar de las que hemos puesto.

Añadimos las siguientes líneas después de la llamada a MoveBall y antes de la etiqueta loop_continue para poner el contador a 0.

ld a, ZERO ld (countLoopBall), a

La implementación de Main.asm quedaría así.

org $8000 Main: ld a, $00 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Pinta la línea central call PrintBorder ; Pinta el borde Loop: ld a, (countLoopBall) ; Carga en A el contador de la bola inc a ; Incrementa el contador ld (countLoopBall), a ; Carga el valor en memoria cp $0f ; Comprueba si el contador ha llegado a 15 jr nz, loop_continue ; Si no ha llegado, salta call MoveBall ; Mueve la posición de la bola ld a, ZERO ; Pone A = 0 ld (countLoopBall), a ; Carga el valor en memoria loop_continue: call PrintBall ; Pinta la bola jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador para controlar cuando se mueve la bola end $8000

Compilamos y vemos el resultado en el emulador. Esta vez sí vemos como se mueve la bola a una velocidad más aceptable.

No hemos definido una constante para la comparación con el contador de la bola ya que, en un futuro, la velocidad será variable.

Repintamos línea central y borde

Ahora vamos a abordar el problema de las partes que va borrando la bola a su paso, y vamos a empezar por la línea central.

En una primera aproximación, vamos a repintar la parte de la línea que coincide en la coordenada Y con la bola, sin importar si la bola está pasando por encima o no. Parece innecesario, pero nos va a ayudar a temporizar.

Abrimos el archivo Video.asm e implementamos después de la rutina PrintLine.

ReprintLine: ld hl, (ballPos) ld a, l and $e0 or $10 ld l, a

Cargamos la posición de la bola en HL, LD HL, (ballPos), cargamos línea y columna en A, LD A, L, nos quedamos con el valor de la línea, AND $E0, ponemos la columna a 16, que es donde está la línea vertical, OR $10, y cargamos el valor en L, LD L, A.

Vamos a repintar 6 scanlines, que son los mismos que tiene la bola.

ld b, $06 reprintLine_loop: ld a, h

Cargamos en B el número de scanlines que se repintan, LD B, $06, y cargamos tercio y scanline en A, LD A, H.

Para pintar la línea, en los scanlines 0 y 7 pintábamos en blanco, y en el resto la parte visible de la línea.

and $07 cp $01 jr c, reprintLine_00 cp $07 jr z, reprintLine_00

Nos quedamos con la parte del scanline, AND $07, y comprobamos si es 1, CP $01. Si el scanline en menor que 1 saltamos, JR C, reprintLine_00, en el caso contrario comprobamos si es 7, CP $07. Si el scanline es 7 saltamos, JR Z, reprintLine_00.

Si no hemos saltado, el scanline está entre 1 y 6.

ld c, LINE jr reprintLine_loopCont

Cargamos el sprite de la línea en C, LD C, LINE, y saltamos, JR reprintLine_loopCont.

Si anteriormente saltamos, el scanline es 0 o 7.

reprintLine_00: ld c, ZERO

Cargamos el sprite en blanco en C, LD C, ZERO, y pintamos lo que corresponda.

reprintLine_loopCont: ld a, (hl) or c ld (hl), a call NextScan djnz reprintLine_loop ret

Cargamos el valor de la dirección de memoria del byte que vamos a repintar en A, LD A, (HL), le añadimos los píxeles del repintado de la línea, OR C, y lo pintamos en pantalla, LD (HL), A. Calculamos la dirección de memoria del scanline siguiente, CALL NextScan, y repetimos la operación hasta que B valga 0, DJNZ reprintLine_loop. Por último, salimos, RET.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Repinta la línea central. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- ReprintLine: ld hl, (ballPos) ; Carga en HL la posición de la bola ld a, l ; Carga la línea y columna en A and $e0 ; Se queda con la línea or $10 ; Pone la columna a 16 ($10) ld l, a ; Carga el valor en L. HL = Posición inicial repintar ld b, $06 ; Se repintan 6 scanlines reprintLine_loop: ld a, h ; Carga tercio y scanline en A and $07 ; Se queda con el scanline ; Si está en los scanline 0 o 7 pinta ZERO ; Si está en los scanline 1, 2, 3, 4, 5 o 6 pinta LINE cp $01 ; Comprueba si está en scanline 1 o superior jr c, reprintLine_00 ; Si está por debajo, pinta $00 cp $07 ; Comprueba si está en scanline 7 jr z, reprintLine_00 ; Si es así, pinta ZERO ld c, LINE ; Esta en scanline 1 a 6, pinta LINE jr reprintLine_loopCont ; Salta reprintLine_00: ld c, ZERO ; Está en scanline 0 o 7, pinta ZERO reprintLine_loopCont: ld a, (hl) ; Obtiene los pixeles de la posición actual or c ; Los mezcla con C ld (hl), a ; Pinta el resultado en la posición actual call NextScan ; Obtiene el scanline siguiente djnz reprintLine_loop ; Hasta que B = 0 ret

Podemos ahorrar 4 bytes y 19 ciclos de reloj modificando seis líneas de la rutina. Lo dejamos en vuestras manos y veremos la forma de hacerlo al final del tutorial.

Ya solo queda probar lo que hemos implementado, para lo cual abrimos el archivo Main.asm y después de la llamada a PrintBall incluimos la llamada a ReprintLine.

call ReprintLine

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - Repinta la lneaEnsamblador ZX Spectrum, repinta la línea

La línea central ya no se borra, pero podemos apreciar que la velocidad de la bola ha disminuido. Hay que tener en cuenta que ahora realizamos más operaciones que antes. Según avancemos iremos ajustando la velocidad de la bola.

Vamos ahora a evitar que se borre el borde, para lo cual vamos a modificar los límites superior e inferior de la bola, en el fichero Sprite.asm.

BALL_BOTTOM: EQU $b8 BALL_TOP: EQU $02

Compilamos, cargamos en el emulador y comprobamos que ya no se borra el borde.

Incluimos las palas

Ahora vamos a empezar con las palas. Volvemos a Main.asm y añadimos las siguientes líneas entre CALL ReprintLine y JR Loop.

ld hl, (paddle1pos) call PrintPaddle ld hl, (paddle2pos) call PrintPaddle

Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), y la pintamos, CALL PrintPaddle. Hacemos lo mismo con la pala 2.

Como se puede apreciar, las palas se pintan en todas las iteraciones del bucle, al igual que la bola y el repintado de línea.

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - No borra el bordeEnsamblador ZX Spectrum, no borra el borde

Se dibujan las palas y la bola no las borra al pasar. También se aprecia que ahora la bola va mucho más lenta, debido a que hacemos más operaciones en cada iteración del bucle.

Para que la bola vuelva a ir más rápida, vamos a cambiar en Main.asm el valor que tenía que alcanzar el contador para que la bola se moviese.

ld (countLoopBall), a cp $06 ; ¡CAMBIO! jr nz, loop_continue

Compilamos, cargamos en el emulador y comprobamos que la bola vuelve a ir más rápido.

Movemos las palas

Ahora vamos a implementar la rutina para mover las palas; ya vimos como hacerlo en el paso 03. Editamos el archivo Game.asm y vamos al final del mismo.

La rutina que vamos a implementar, recibe en el registro D las pulsaciones de las teclas de control.

MovePaddle: bit $00, d jr z, movePaddle_1Down

Evaluamos si se ha pulsado la tecla arriba del jugador 1, BIT $00, D. Si no se ha pulsado saltamos a comprobar si se ha pulsado la tecla abajo, JR Z, movePaddle_1Down.

Si no salta, se ha pulsado la tecla arriba del jugador 1.

ld hl, (paddle1pos) ld a, PADDLE_TOP call CheckTop jr z, movePaddle_2Up

Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), el límite superior para las palas en A, LD A, PADDLE_TOP, y comprobamos si se ha alcanzado, CALL CheckTop. Si se ha alcanzado el límite, saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.

Si no se ha alcanzado el límite superior, movemos la pala 1.

call PreviousScan ld (paddle1pos), hl jr movePaddle_2Up

Calculamos la nueva posición para la pala 1, CALL PreviousScan, la cargamos en memoria, LD (paddle1pos), HL, y saltamos a comprobar los controles del jugador 2, JR movePaddle_2Up.

Si no se ha pulsado la tecla arriba del jugador 1, comprobamos si se ha pulsado la tecla abajo.

movePaddle_1Down: bit $01, d jr z, movePaddle_2Up

Evaluamos si se ha pulsado la tecla abajo del jugador 1, BIT $01, D. Si no se ha pulsado saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.

Si no salta, se ha pulsado la tecla abajo del jugador 1.

ld hl, (paddle1pos) ld a, PADDLE_BOTTOM call CheckBottom jr z, movePaddle_2Up

Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), el límite inferior para las palas en A, LD A, PADDLE_BOTTOM, y comprobamos si se ha alcanzado, CALL CheckBottom. Si se ha alcanzado el límite saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.

Si no se ha alcanzado el límite inferior, mueve la pala 1.

call NextScan ld (paddle1pos), hl

Calculamos la nueva posición para la pala 1, CALL NextScan, y la cargamos en memoria, LD (paddle1pos), HL.

Hacemos las comprobaciones con los controles del jugador 2. Dada la semejanza, simplemente marcamos los cambios con respecto a la comprobación del jugador 1.

movePaddle_2Up: bit $02, d ; ¡CAMBIO! jr z, movePaddle_2Down ; ¡CAMBIO! ld hl, (paddle2pos) ; ¡CAMBIO! ld a, PADDLE_TOP call CheckTop jr z, movePaddle_End ; ¡CAMBIO! call PreviousScan ld (paddle2pos), hl ; ¡CAMBIO! jr movePaddle_End ; ¡CAMBIO! movePaddle_2Down: ; ¡CAMBIO! bit $03, d ; ¡CAMBIO! jr z, movePaddle_End ; ¡CAMBIO! ld hl, (paddle2pos) ; ¡CAMBIO! ld a, PADDLE_BOTTOM call CheckBottom jr z, movePaddle_End ; ¡CAMBIO! call NextScan ld (paddle2pos), hl ; ¡CAMBIO! movePaddle_End: ; ¡NUEVA! ret ; ¡NUEVA!

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Calcula la posición de las palas para moverlas. ; Entrada: D = Pulsaciones de los controles ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- MovePaddle: bit $00, d ; Evalúa si se ha pulsado la A jr z, movePaddle_1Down ; Si no se ha pulsado salta ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 ld a, PADDLE_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, movePaddle_2Up ; Si se ha alcanzado, salta call PreviousScan ; Obtiene el scanline anterior a la posición de la pala 1 ld (paddle1pos), hl ; Carga en memoria la nueva posición de la pala 1 jr movePaddle_2Up ; Salta movePaddle_1Down: bit $01, d ; Evalúa si se ha pulsado la Z jr z, movePaddle_2Up ; Si no se ha pulsado salta ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 ld a, PADDLE_BOTTOM ; Carga en A el margen inferior call CheckBottom ; Evalúa si se ha alcanzado el margen inferior jr z, movePaddle_2Up ; Si se ha alcanzado, salta call NextScan ; Obtiene el scanline siguiente a la posición de la pala 1 ld (paddle1pos), hl ; Carga en memoria la nueva posición de la pala 1 movePaddle_2Up: bit $02, d ; Evalúa si se ha pulsado el 0 jr z, movePaddle_2Down ; Si no se ha pulsado salta ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 ld a, PADDLE_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, movePaddle_End ; Si se ha alcanzado, salta call PreviousScan ; Obtiene el scanline anterior a la posición de la pala 2 ld (paddle2pos), hl ; Carga en memoria la nueva posición de la pala 2 jr movePaddle_End ; Salta movePaddle_2Down: bit $03, d ; Evalúa si se ha pulsado la O jr z, movePaddle_End ; Si no se ha pulsado salta ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 ld a, PADDLE_BOTTOM ; Carga en A el margen inferior call CheckBottom ; Evalúa si se ha alcanzado el margen inferior jr z, movePaddle_End ; Si se ha alcanzado, salta call NextScan ; Obtiene el scanline siguiente a la posición de la pala 2 ld (paddle2pos), hl ; Carga en memoria la nueva posición de la pala 2 movePaddle_End: ret

Podemos ahorrar 2 bytes y 2 ciclos de reloj, de la misma forma que en la entrega anterior. Esta vez no daremos la solución al final del tutorial, ya que es similar a la que se verá para la entrega anterior.

Para terminar, vamos a implementar en Main.asm las llamadas a esta rutina, dentro de nuestro bloque infinito, justo encima de la etiqueta loop_continue.

loop_paddle: call ScanKeys call MovePaddle

Primero comprobamos las teclas de control que se han pulsado, CALL ScanKeys, y luego movemos las palas, CALL MovePaddle.

También tenemos que cambiar la etiqueta a la que salta cuando la bola no se mueve, cuatro líneas más arriba.

cp $06 jr nz, loop_paddle ; ¡CAMBIO! call MoveBall

Compilamos y probamos en el emulador.

Ensamblador ZX Spectrum - Palas borrando bordeEnsamblador ZX Spectrum, palas borrando borde

Observamos dos problemas:

  1. Las palas borran el borde.
  2. Las palas se mueven muy rápido y son difíciles de controlar.

Para resolver el primer problema vamos a modificar las constantes que marcan los límites superior e inferior de las palas, que están en Sprite.asm.

PADDLE_BOTTOM: EQU $a6 PADDLE_TOP: EQU $02

Compilamos, cargamos en el emulador y comprobamos que ya no se borra el borde.

Ensamblador ZX Spectrum - Palas no borran bordeEnsamblador ZX Spectrum, palas no borran borde

Velocidad de las palas

Para reducir la velocidad del movimiento de las palas, vamos a usar la misma técnica que usamos con la bola, no vamos a mover las palas en cada iteración del bucle.

Lo primero es declarar la variable que usaremos como contador, lo que hacemos antes de END $8000.

countLoopPaddle: db $00

Ahora, justo debajo de la etiqueta loop_paddle, implementamos la comprobación del contador.

ld a, (countLoopPaddle) inc a ld (countLoopPaddle), a cp $02 jr nz, loop_continue call ScanKeys call MovePaddle

Cargamos el contador en A, LD A, (countLoopPaddle), lo incrementamos, INC A, y lo cargamos en memoria, LD (countLoopPaddle), A. Evaluamos si han pasado las veces que hemos definido para mover las palas, CP $02, y si no es así saltamos, JR NZ, loop_continue.

Si no salta, comprobamos si se ha pulsado alguna tecla de control, CALL ScanKeys, y movemos las palas, CALL MovePaddle, y tal y como hicimos con la bola, hay que poner el contador a cero. Añadimos las líneas siguiente antes de la etiqueta loop_continue.

ld a, ZERO ld (countLoopPaddle), a

Cargamos 0 en A, LD A, ZERO, y lo cargamos en memoria, LD (countLoopPaddle), A, poniendo el contador a 0.

Compilamos y cargamos en el emulador. Ahora el control de las palabras es menos rápido y más preciso.

El código final de Main.asm es el siguiente.

; Pintado de campo, movimiento de palas y bola y temporización org $8000 ; ----------------------------------------------------------------------------- ; Entrada al programa ; ----------------------------------------------------------------------------- Main: ld a, $00 ; A = 0 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Imprime la línea central call PrintBorder ; Imprime el borde del campo Loop: ld a, (countLoopBall) ; Carga el contador de vueltas de la bola inc a ; Lo incrementa ld (countLoopBall), a ; Lo carga en memoria cp $06 ; Comprueba si ha llegado a 6 jr nz, loop_paddle ; Si no ha llegado a 4 salta call MoveBall ; Mueve la bola ld a, ZERO ; Pone el contador a 0 ld (countLoopBall), a ; Lo carga en memoria loop_paddle: ld a, (countLoopPaddle) ; Carga el contador de vueltas de las palas inc a ; Lo incrementa ld (countLoopPaddle), a ; Lo carga en memoria cp $02 ; Comprueba si ha llegado a 2 jr nz, loop_continue ; Si no ha llegado a 2 salta call ScanKeys ; Escanea las teclas pulsadas call MovePaddle ; Mueva las palas ld a, ZERO ; Pone el contador a 0 ld (countLoopPaddle), a ; Lo carga en memoria loop_continue: call PrintBall ; Pinta la bola call ReprintLine ; Repinta la línea ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 call PrintPaddle ; Pinta la pala 1 ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 call PrintPaddle ; Pinta la pala 2 jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador de vueltas de la bola countLoopPaddle: db $00 ; Contador de vueltas de las palas end $8000

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, campo, palas, bola y temporización

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

0x05 Ensamblador ZX Spectrum Pong – Movemos la bola por la pantalla [Espamatica] [Leer]


Llegamos a una nueva entrega de Ensamblador ZX Spectrum Pong, y en esta ocasión vamos a mover la bola por toda la pantalla, finalizando la primera parte del tutorial.

Ensamblador ZX Spectrum Pong – Movemos la bola por la pantalla

Creamos la carpeta Paso05, dentro de la misma creamos los archivos Main.asm y Game.asm, y copiamos los archivos Sprite.asm y Video.asm que tenemos en la carpeta Paso04.

Empezamos editando Sprite.asm para añadir dos nuevas constantes que vamos a necesitar para mover la bola por la pantalla.

MARGIN_LEFT: EQU $00 MARGIN_RIGHT: EQU $1e

Igual que tenemos los límites superior e inferior, necesitamos los límites derecho e izquierdo para que la bola se mantenga dentro de los mismos.

El siguiente paso es implementar la lógica del movimiento de la bola, lo que haremos en Game.asm.

MoveBall: ld a, (ballSetting) and $80 jr nz, moveBall_down

Primero cargamos en A la configuración actual de la bola, LD A, (ballSetting), y nos quedamos con el bit 7, AND $80, que indica si la bola se desplaza hacia arriba o hacia abajo. Si el bit no está a 0, la bola se desplaza hacia abajo y salta, JR NZ, moveBall_down.

Si el bit está a 0, la bola se desplaza hacia arriba.

moveBall_up: ld hl, (ballPos) ld a, BALL_TOP call CheckTop jr z, moveBall_upChg call PreviousScan ld (ballPos), hl jr moveBall_x

Cargamos la posición actual de la bola en HL, LD HL, (ballPos), el límite vertical en A, LD A, BALL_TOP, y comprobamos si se ha alcanzado dicho límite, CALL CheckTop. Si se activa el flag Z, se ha alcanzado el límite y saltamos para cambiar la dirección vertical de la bola, JR Z, moveBall_upChg.

Si la bola no ha llegado al límite vertical, calculamos la nueva posición, CALL PreviousScan, la cargamos en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.

En el caso de haber alcanzado el límite superior, hay que cambiar la dirección vertical de la bola.

moveBall_upChg: ld a, (ballSetting) or $80 ld (ballSetting), a call NextScan ld (ballPos), hl jr moveBall_x

Primero cargamos la configuración de la bola en A, LD A, (ballSetting), luego activamos el bit 7, OR $80, para indicar que ahora la bola debe ir hacia abajo, y cargamos el valor en memoria, LD (ballSetting), A. Calculamos la nueva posición vertical de la bola, CALL NextScan, cargamos el valor en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.

Para activar el bit 7 hemos hecho un OR con $80 (10000000). Es conveniente recordar el resultado de la operación OR, dependiendo del valor de los bits.

Bit 1Bit 2Resultado
000
101
011
111
Ensamblador ZX Spectrum, resultado de OR

Según se ve en la tabla, al aplicar OR $80, pone el bit 7 a 1 y el resto los deja como estaban.

Si al iniciar la rutina la bola iba hacia abajo, hay que hacer algo parecido a lo visto anteriormente.

moveBall_down: ld hl, (ballPos) ld a, BALL_BOTTOM call CheckBottom jr z, moveBall_downChg call NextScan ld (ballPos), hl jr moveBall_x

Primero cargamos la posición de la bola en HL, LD HL, (ballPos), el límite inferior en A, LD A, BALL_BOTTOM, y comprobamos si se ha alcanzado, CALL CheckBottom, en cuyo caso saltamos para cambiar la dirección de la bola, JR Z, moveBall_downChg.

Si no se ha alcanzado el límite inferior, calculamos la nueva posición de la bola, CALL NextScan, la cargamos en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.

En el caso de haber alcanzado el límite inferior, hay que cambiar la dirección vertical de la bola.

moveBall_downChg: ld a, (ballSetting) and $7f ld (ballSetting), a call PreviousScan ld (ballPos), hl

Primero cargamos la configuración de la bola en A, LD A, (ballSetting), luego desactivamos el bit 7, AND $7F, para indicar que ahora la bola debe ir hacia arriba, y cargamos el valor en memoria, LD (ballSetting), A. Calculamos la nueva posición vertical de la bola, CALL PreviousScan, y cargamos el valor en memoria, LD (ballPos), HL.

Para desactivar el bit 7 hemos hecho un AND con $7F (01111111). Es conveniente recordar el resultado de la operación AND, dependiendo del valor de los bits.

Bit 1Bit 2Resultado
000
100
010
111
Ensamblador ZX Spectrum, resultado de AND

Según se ve en la tabla, al aplicar AND $7F, pone el bit 7 a 0 y el resto los deja como estaban.

Empezamos a calcular el desplazamiento horizontal.

moveBall_x: ld a, (ballSetting) and $40 jr nz, moveBall_left

Cargamos la configuración de la bola en A, LD A, (ballSetting), comprobamos el estado del bit 6, AND $40, y si no está a 0, la bola va hacia la izquierda y salta, JR NZ, moveBall_left.

Si el bit 6 está a 0, la bola va hacia la derecha.

moveBall_right: ld a, (ballRotation) cp $08 jr z, moveBall_rightLast inc a ld (ballRotation), a jr moveBall_end

Cargamos la rotación en A, LD A, (ballRotation), y comprobamos si está en la última, CP $08, en cuyo caso saltamos, JR Z, moveBall_rightLast.

Si no está en la última rotación, incrementamos la misma, INC A, la cargamos en memoria, LD (ballRotation), A, y saltamos al final de la rutina, JR moveBall_end.

Si, por el contrario, ha llegado a la última rotación y no está en el límite derecho, desplazamos la bola a la siguiente columna.

moveBall_rightLast: ld a, (ballPos) and $1f cp MARGIN_RIGHT jr z, moveBall_rightChg ld hl, ballPos inc (hl) ld a, $01 ld (ballRotation), a jr moveBall_end

Cargamos la línea y columna en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y evaluamos si ha llegado al límite derecho, CP MARGIN_RIGHT, en cuyo caso saltamos para cambiar la dirección de la bola, JR Z, moveBall_rightChg.

Si no se ha llegado al límite derecho, desplazamos la bola a la siguiente columna. Cargamos la dirección donde se encuentra la posición de la bola en HL, LD HL, ballPos, e incrementamos la columna, INC (HL).

Ponemos la rotación de la bola a 1, LD A, $01, lo cargamos en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.

Como se puede ver, para cargar la columna en A, la instrucción usada ha sido LD A, (ballPos), y para incrementar la columna LD HL, ballPos y INC (HL).

Teniendo en cuenta que las posiciones de memoria de la VideRAM se codifican 010TTSS LLLCCCCC, ¿no estaríamos cargando y alterando el scanline? No, y ello se debe a que el Z80 es un micro de tipo Little Endian.

Un micro Little Endian, cuando carga valores de 16 bits en memoria, carga en la primera posición de memoria el byte menos significativo, y en la siguiente el más significativo, de tal manera que si en la posición de memoria $C000 se carga el valor $4000, en la posición $C000 se carga $00 y en la $C001 se carga $40. Es por eso que cuando se carga en A el valor desde (ballPos), lo que carga es el byte menos significativo que es donde están la línea y columna. De igual modo al incrementar (HL), incrementa la columna.

Si la carga se hace sobre un registro de 16 bits, carga el byte menos significativo en la parte baja del registro, y el más significativo en la parte alta. Es por eso que al cargar ballPos en HL, carga en H el byte más significativo de la dirección de memoria y en L el menos significativo.

Seguimos con la rutina…

Si ha llegado al límite derecho, hay que cambiar la dirección de la bola.

moveBall_rightChg: ld a, (ballSetting) or $40 ld (ballSetting), a ld a, $ff ld (ballRotation), a jr moveBall_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), activamos el bit 6 para cambiar la dirección hacia la izquierda, OR $40, y cargamos el valor en memoria, LD (ballSetting), A.

Ponemos la rotación de la bola a -1, LD A, $FF, la cargamos en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.

Si la bola va hacia la izquierda, hay que hacer algo parecido a lo visto anteriormente.

moveBall_left: ld a, (ballRotation) cp $f8 jr z, moveBall_leftLast dec a ld (ballRotation), a jr moveBall_end

Cargamos la rotación en A, LD A, (ballRotation), comprobamos si está en la última, CP $F8, y de ser así saltamos, JR Z, moveBall_leftLast.

Si no está en la última rotación la decrementamos, DEC A, cargamos el valor en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.

Si ha llegado a la última rotación y no ha alcanzado el límite izquierdo, desplazamos la bola a la columna anterior.

moveBall_leftLast: ld a, (ballPos) and $1f cp MARGIN_LEFT jr z, moveBall_leftChg ld hl, ballPos dec (hl) ld a, $ff ld (ballRotation), a jr moveBall_end

Cargamos la línea y columna en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y comprobamos si ha llegado al límite izquierdo, CP MARGIN_LEFT, en cuyo caso saltamos, JR Z, moveBall_leftChg.

Si no ha llegado al límite izquierdo, cargamos la dirección donde está la posición de la bola en HL, LD HL, ballPos, y decrementamos la columna, DEC (HL).

Ponemos la rotación de la bola a -1, LD A, $FF, cargamos el valor en memoria, LD (ballRotation), A, y saltamos al fin de la rutina.

Terminamos la rutina con el cambio de dirección, si se ha alcanzado el límite izquierdo.

moveBall_leftChg: ld a, $01 ld (ballRotation), a ld a, (ballSetting) and $bf ld (ballSetting), a moveBall_end: ret

Ponemos la rotación de la bola a 1, LD A, $01, y la cargamos en memoria, LD (ballRotation), A. Cargamos la configuración de la bola en A, LD A, (ballSetting), desactivamos el bit 6 para que la dirección sea hacia la derecha, AND $BF, y cargamos el valor en memoria, LD (ballSetting), A.

Podemos ahorrar 2 ciclos de reloj y 5 bytes haciendo una pequeña modificación. Lo dejamos en vuestras manos y veremos la forma de hacerlo al final del tutorial.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Calcula la posición, rotación y dirección de la bola para pintarla. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- MoveBall: ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $80 ; Comprueba la dirección vertical jr nz, moveBall_down ; Si el bit 7 está a uno, va hacia abajo moveBall_up: ; La bola va hacia arriba ld hl, (ballPos) ; Carga la posición de la bola en HL ld a, BALL_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, moveBall_upChg ; Si se ha alcanzado salta call PreviousScan ; Obtiene el scanline anterior a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola jr moveBall_x ; Salta moveBall_upChg: ; La bola va hacia arriba, pero ha llegado al tope y cambia de dirección ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola or $80 ; Pone la dirección vertical hacia abajo ld (ballSetting), a ; Carga en memoria la nueva dirección de la bola call NextScan ; Obtiene el scanline siguiente a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola jr moveBall_x ; Salta moveBall_down: ; La bola va hacia abajo ld hl, (ballPos) ; Carga la posición de la bola en HL ld a, BALL_BOTTOM ; Carga en A el margen superior call CheckBottom ; Evalúa si se ha alcanzado el margen superior jr z, moveBall_downChg ; Si se ha alcanzado salta call NextScan ; Obtiene el scanline siguiente a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola jr moveBall_x ; Salta moveBall_downChg: ; La bola va hacia abajo, pero ha llegado al tope y cambia de dirección ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $7f ; Pone la dirección vertical hacia arriba ld (ballSetting), a ; Carga en memoria la nueva dirección de la bola call PreviousScan ; Obtiene el scanline anterior a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola moveBall_x: ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $40 ; Comprueba la dirección horizontal jr nz, moveBall_left ; Si el bit 6 está a uno, va hacia la izquierda moveBall_right: ; La bola va hacia la derecha ld a, (ballRotation) ; Carga la rotación actual de la bola cp $08 ; Comprueba si ya está en la última rotación jr z, moveBall_rightLast ; Si está en la última rotación salta inc a ; Incrementa la rotación ld (ballRotation), a ; La carga en memoria jr moveBall_end ; Fin de la rutina moveBall_rightLast: ; Está en la última rotación ; Si no ha llegado al límite derecho pone la rotación a 1 ; y pone la bola en la siguiente columna ld a, (ballPos) ; Carga la línea y columna de la bola en A and $1f ; Se queda solo con la columna cp MARGIN_RIGHT ; Lo comprara con el límite derecho jr z, moveBall_rightChg ; Si lo ha alcanzado salta ld hl, ballPos ; Carga la dirección de la posición de la bola en HL inc (hl) ; Incrementa la columna ld a, $01 ; Pone la rotación a 1 ld (ballRotation), a ; Carga el valor en memoria jr moveBall_end ; Fin de la rutina moveBall_rightChg: ; Ha llegado al límite derecho ; Pone la rotación a -1 y cambia la dirección horizontal de la bola ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola or $40 ; Pone la dirección horizontal hacia la izquierda ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $ff ; Carga -1 en A ld (ballRotation), a ; Lo carga en memoria Rotación = -1 jr moveBall_end ; Fin de la rutina moveBall_left: ; La bola va hacia la izquierda ld a, (ballRotation) ; Carga la rotación actual de la bola cp $f8 ; Comprueba si ya está en la última rotación jr z, moveBall_leftLast ; Si está en la última rotación salta dec a ; Decrementa la rotación ld (ballRotation), a ; La carga en memoria jr moveBall_end ; Fin de la rutina moveBall_leftLast: ; Esta en la última rotación ; Si no ha llegado al límite izquierdo pone la rotación a -1 ; y pone la bola en la columna anterior ld a, (ballPos) ; Carga la línea y columna en A and $1f ; Se queda solo con la columna cp MARGIN_LEFT ; Lo comprara con el límite izquierdo jr z, moveBall_leftChg ; Si lo ha alcanzado salta ld hl, ballPos ; Carga la dirección de la posición de la bola en HL dec (hl) ; Pasa a la columna anterior ld a, $ff ; Pone la rotación a -1 ld (ballRotation), a ; Carga el valor en memoria jr moveBall_end ; Fin de la rutina moveBall_leftChg: ; Ha llegado al límite izquierdo ; Pone la rotación a 1 y cambia la dirección ld a, $01 ; Carga la posición de la bola en HL ld (ballRotation), a ; Carga el valor en memoria Rotación = 1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $bf ; Pone la dirección horizontal hacia la derecha ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria moveBall_end: ret

Ha llegado el momento de probar todo lo implementado, vamos a editar el archivo Main.asm. En este caso la implementación es muy sencilla.

org $8000 ld a, $02 out ($fe), a call PrintBall

Indicamos la dirección dónde cargar el programa, ponemos el borde en rojo y pintamos la bola en la posición inicial.

Loop: call MoveBall call PrintBall halt jr Loop

Implementamos un bucle infinito en el que movemos la bola, la pintamos, esperamos al refresco de la pantalla y volvemos a realizar estas tres operaciones indefinidamente.

Include "Game.asm" Include "Sprite.asm" Include "Video.asm" end $8000

Por último, incluimos los archivos necesarios e indicamos a PASMO dónde tiene que llamar al cargar el programa.

El aspecto final de Main.asm es el siguiente.

; Mueve la bola por la pantalla trazando diagonales org $8000 ld a, $02 ; A = 2 out ($fe), a ; Pone el borde en rojo call PrintBall ; Imprime la bola Loop: call MoveBall ; Mueve la bola call PrintBall ; Pinta la bola halt ; Espera al refresco de pantalla jr Loop ; Bucle infinito include "Game.asm" include "Sprite.asm" include "Video.asm" end $8000

Llega el gran momento… compilamos y vemos el resultado en el emulador.

Ensamblador ZX Spectrum Pong - Movemos la bola por la pantallaEnsamblador ZX Spectrum, movemos la bola por la pantalla

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, movemos la bola por la pantalla

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.