0x0D Ensamblador ZX Spectrum Marciano – Música
En este capítulo de Ensamblador ZX Spectrum Marciano vamos a integrar en nuestro juego todo lo que vimos en el capítulo anterior, habrá alguna pequeña variación, pero es prácticamente lo mismo. Además, ha llegado la hora de dejar todo el código comentado, hay partes que todavía no las tenemos comentadas y si lo dejamos así, con el tiempo, es posible que nos cueste más saber que es lo que hacemos ahí y, más importante aún, ¿por qué?
Creamos la carpeta Paso13 y copiamos desde la carpeta Paso12 los archivos Cargador.tap, Const.asm, Ctrl.asm, Game.asm, Graph.asm, Int.asm, Main.asm, make o make.bat, Print.asm, Var.asm y la carpeta Sound.
Tabla de contenidos
- Constantes
- Variables
- Reproducción
- Control de música por interrupciones
- Efectos de sonido
- Ensamblador ZX Spectrum, conclusión
- Enlaces de interés
Constantes
Lo primero que vamos a hacer es declarar las constantes necesarias en el archivo Const.asm, empezando por la rutina de la ROM que vamos a usar. Localizamos la etiqueta UDG y justo debajo de ella añadimos lo siguiente:
; -----------------------------------------------------------------------------
; Rutina beeper de la ROM.
;
; Entrada: HL -> Nota.
; DE -> Duración.
;
; Altera el valor de los registros AF, BC, DE, HL e IX.
; -----------------------------------------------------------------------------
BEEP: EQU $03b5
Es muy importante que leamos los comentarios, pues según vemos esta rutina de la ROM altera el valor de casi todos los registros, cosa que no hemos tenido en cuenta en el capítulo anterior, pero si que lo vamos a tener en cuenta en este capítulo.
Al final del archivo Const.asm vamos a añadir las constantes de las notas y las frecuencias.
; -----------------------------------------------------------------------------
; Notas a cargar en HL
; -----------------------------------------------------------------------------
C_0: EQU $6868
Cs_0: EQU $628d
D_0: EQU $5d03
Ds_0: EQU $57bf
E_0: EQU $52d7
F_0: EQU $4e2b
Fs_0: EQU $49cc
G_0: EQU $45a3
Gs_0: EQU $41b6
A_0: EQU $3e06
As_0: EQU $3a87
B_0: EQU $373e
C_1: EQU $3425
Cs_1: EQU $3134
D_1: EQU $2e6f
Ds_1: EQU $2bd3
E_1: EQU $295c
F_1: EQU $2708
Fs_1: EQU $24d5
G_1: EQU $22c2
Gs_1: EQU $20cd
A_1: EQU $1ef4
As_1: EQU $1d36
B_1: EQU $1b90
C_2: EQU $1a02
Cs_2: EQU $188b
D_2: EQU $1728
Ds_2: EQU $15da
E_2: EQU $149e
F_2: EQU $1374
Fs_2: EQU $125b
G_2: EQU $1152
Gs_2: EQU $1058
A_2: EQU $0f6b
As_2: EQU $0e9d
B_2: EQU $0db8
C_3: EQU $0cf2
Cs_3: EQU $0c36
D_3: EQU $0b86
Ds_3: EQU $0add
E_3: EQU $0a40
F_3: EQU $09ab
Fs_3: EQU $091e
G_3: EQU $089a
Gs_3: EQU $081c
A_3: EQU $07a6
As_3: EQU $0736
B_3: EQU $06cd
C_4: EQU $066a
Cs_4: EQU $060c
D_4: EQU $05b3
Ds_4: EQU $0560
E_4: EQU $0511
F_4: EQU $04c6
Fs_4: EQU $0480
G_4: EQU $043d
Gs_4: EQU $03ff
A_4: EQU $03c4
As_4: EQU $038c
B_4: EQU $0357
C_5: EQU $0325
Cs_5: EQU $02f7
D_5: EQU $02ca
Ds_5: EQU $02a0
E_5: EQU $0279
F_5: EQU $0254
Fs_5: EQU $0231
G_5: EQU $020f
Gs_5: EQU $01f0
A_5: EQU $01d3
As_5: EQU $01b7
B_5: EQU $019c
C_6: EQU $0183
Cs_6: EQU $016c
D_6: EQU $0156
Ds_6: EQU $0141
E_6: EQU $012d
F_6: EQU $011b
Fs_6: EQU $0109
G_6: EQU $00f8
Gs_6: EQU $00e9
A_6: EQU $00da
As_6: EQU $00cc
B_6: EQU $00bf
C_7: EQU $00b2
Cs_7: EQU $00a7
D_7: EQU $009c
Ds_7: EQU $0091
E_7: EQU $0087
F_7: EQU $007e
Fs_7: EQU $0075
G_7: EQU $006d
Gs_7: EQU $0065
A_7: EQU $005e
As_7: EQU $0057
B_7: EQU $0050
C_8: EQU $004a
Cs_8: EQU $0044
D_8: EQU $003e
Ds_8: EQU $0039
E_8: EQU $0034
F_8: EQU $0030
Fs_8: EQU $002b
G_8: EQU $0027
Gs_8: EQU $0023
A_8: EQU $0020
As_8: EQU $001c
B_8: EQU $0019
; -----------------------------------------------------------------------------
; Frecuencias a cargar en DE, 1 segundo ( / 2 = 0.5 ....)
; -----------------------------------------------------------------------------
C_0_f: EQU $0010 / $20
Cs_0_f: EQU $0011 / $20
D_0_f: EQU $0012 / $20
Ds_0_f: EQU $0013 / $20
E_0_f: EQU $0014 / $20
F_0_f: EQU $0015 / $20
Fs_0_f: EQU $0017 / $20
G_0_f: EQU $0018 / $20
Gs_0_f: EQU $0019 / $20
A_0_f: EQU $001b / $20
As_0_f: EQU $001d / $20
B_0_f: EQU $001e / $20
C_1_f: EQU $0020 / $20
Cs_1_f: EQU $0022 / $20
D_1_f: EQU $0024 / $20
Ds_1_f: EQU $0026 / $20
E_1_f: EQU $0029 / $20
F_1_f: EQU $002b / $20
Fs_1_f: EQU $002e / $20
G_1_f: EQU $0031 / $20
Gs_1_f: EQU $0033 / $20
A_1_f: EQU $0037 / $20
As_1_f: EQU $003a / $20
B_1_f: EQU $003d / $20
C_2_f: EQU $0041 / $20
Cs_2_f: EQU $0045 / $20
D_2_f: EQU $0049 / $20
Ds_2_f: EQU $004d / $20
E_2_f: EQU $0052 / $20
F_2_f: EQU $0057 / $20
Fs_2_f: EQU $005c / $20
G_2_f: EQU $0062 / $20
Gs_2_f: EQU $0067 / $20
A_2_f: EQU $006e / $20
As_2_f: EQU $0074 / $20
B_2_f: EQU $007b / $20
C_3_f: EQU $0082 / $20
Cs_3_f: EQU $008a / $20
D_3_f: EQU $0092 / $20
Ds_3_f: EQU $009b / $20
E_3_f: EQU $00a4 / $20
F_3_f: EQU $00ae / $20
Fs_3_f: EQU $00b9 / $20
G_3_f: EQU $00c4 / $20
Gs_3_f: EQU $00cf / $20
A_3_f: EQU $00dc / $20
As_3_f: EQU $00e9 / $20
B_3_f: EQU $00f6 / $20
C_4_f: EQU $0105 / $20
Cs_4_f: EQU $0115 / $20
D_4_f: EQU $0125 / $20
Ds_4_f: EQU $0137 / $20
E_4_f: EQU $0149 / $20
F_4_f: EQU $015d / $20
Fs_4_f: EQU $0172 / $20
G_4_f: EQU $0188 / $20
Gs_4_f: EQU $019f / $20
A_4_f: EQU $01b8 / $20
As_4_f: EQU $01d2 / $20
B_4_f: EQU $01ed / $20
C_5_f: EQU $020b / $20
Cs_5_f: EQU $022a / $20
D_5_f: EQU $024b / $20
Ds_5_f: EQU $026e / $20
E_5_f: EQU $0293 / $20
F_5_f: EQU $02ba / $20
Fs_5_f: EQU $02e4 / $20
G_5_f: EQU $0310 / $20
Gs_5_f: EQU $033e / $20
A_5_f: EQU $0370 / $20
As_5_f: EQU $03a4 / $20
B_5_f: EQU $03db / $20
C_6_f: EQU $0417 / $20
Cs_6_f: EQU $0455 / $20
D_6_f: EQU $0497 / $20
Ds_6_f: EQU $04dd / $20
E_6_f: EQU $0527 / $20
F_6_f: EQU $0575 / $20
Fs_6_f: EQU $05c8 / $20
G_6_f: EQU $0620 / $20
Gs_6_f: EQU $067d / $20
A_6_f: EQU $06e0 / $20
As_6_f: EQU $0749 / $20
B_6_f: EQU $07b8 / $20
C_7_f: EQU $082d / $20
Cs_7_f: EQU $08a9 / $20
D_7_f: EQU $092d / $20
Ds_7_f: EQU $09b9 / $20
E_7_f: EQU $0a4d / $20
F_7_f: EQU $0aea / $20
Fs_7_f: EQU $0b90 / $20
G_7_f: EQU $0c40 / $20
Gs_7_f: EQU $0cfa / $20
A_7_f: EQU $0dc0 / $20
As_7_f: EQU $0e91 / $20
B_7_f: EQU $0f6f / $20
C_8_f: EQU $105a / $20
Cs_8_f: EQU $1153 / $20
D_8_f: EQU $125b / $20
Ds_8_f: EQU $1372 / $20
E_8_f: EQU $149a / $20
F_8_f: EQU $15d4 / $20
Fs_8_f: EQU $1720 / $20
G_8_f: EQU $1880 / $20
Gs_8_f: EQU $19f5 / $20
A_8_f: EQU $1b80 / $20
As_8_f: EQU $1d23 / $20
B_8_f: EQU $1ede / $20
Variables
El siguiente paso es añadir las variables necesarias: el puntero a la siguiente nota y las canciones. Abrimos el archivo Var.asm y añadimos al final las siguientes líneas:
; -----------------------------------------------------------------------------
; Datos necesarios para la música.
; -----------------------------------------------------------------------------
; -----------------------------------------------------------------------------
; Siguiente nota
; -----------------------------------------------------------------------------
ptrSound:
dw $0000
; -----------------------------------------------------------------------------
; Canciones
; -----------------------------------------------------------------------------
Song_1:
dw $ff0c
dw G_2_f, G_2, G_2_f, G_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2
dw G_2_f, G_2, G_2_f, G_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2
dw D_3_f, D_3, D_3_f, D_3, D_3_f, D_3, Ds_3_f, Ds_3, As_2_f, As_2, Fs_2_f, Fs_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2
dw G_3_f, G_3, G_2_f, G_2, G_2_f, G_2, G_3_f, G_3, Fs_3_f, Fs_3, F_3_f, F_3, E_3_f, E_3, Ds_3_f, Ds_3, E_3_f, E_3
dw Gs_2_f, Gs_2, Cs_3_f, Cs_3, C_3_f, C_3, B_2_f, B_2, As_2_f, As_2, A_2_f, A_2, As_2_f, As_2
dw Ds_2_f, Ds_2, Fs_2_f, Fs_2, Ds_2_f, Ds_2, Fs_2_f, Fs_2, As_2_f, As_2, G_2_f, G_2, As_2_f, As_2, D_3_f, D_3
dw G_3_f, G_3, G_2_f, G_2, G_2_f, G_2, G_3_f, G_3, Fs_3_f, Fs_3, F_3_f, F_3, E_3_f, E_3, Ds_3_f, Ds_3, E_3_f, E_3
dw Gs_2_f, Gs_2, Cs_3_f, Cs_3, C_3_f, C_3, B_2_f, B_2, As_2_f, As_2, A_2_f, A_2, As_2_f, As_2
dw Ds_2_f, Ds_2, Fs_2_f, Fs_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2, A_2_f, A_2, G_2_f, G_2
dw G_2_f, G_2, G_2_f, G_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2
dw G_2_f, G_2, G_2_f, G_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2, Ds_2_f, Ds_2, As_2_f, As_2, G_2_f, G_2
Song_2:
dw $ff05
dw D_4_f, D_4, D_4_f, D_4, D_4_f, D_4, G_4_f, G_4, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_5_f, G_5, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_5_f, G_5, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, C_5_f, C_5, A_4_f, A_4
dw D_4_f, D_4, D_4_f, D_4, D_4_f, D_4, G_4_f, G_4, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_5_f, G_5, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_5_f, G_5, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, C_5_f, C_5, A_4_f, A_4
dw D_4_f, D_4, D_4_f, D_4, E_4_f, E_4, E_4_f, E_4, C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_4_f, G_4, G_4_f, G_4, A_4_f, A_4, B_4_f, B_4, A_4_f, A_4, E_4_f, E_4, Fs_4_f, Fs_4
dw D_4_f, D_4, D_4_f, D_4, E_4_f, E_4, E_4_f, E_4, C_5_f, C_5, C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_4_f, G_4, D_5_f, D_5, D_5_f, D_5, A_4_f, A_4
dw D_4_f, D_4, D_4_f, D_4, E_4_f, E_4, E_4_f, E_4, C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_4_f, G_4, G_4_f, G_4, A_4_f, A_4, B_4_f, B_4, A_4_f, A_4, E_4_f, E_4, Fs_4_f, Fs_4
dw D_5_f, D_5, D_5_f, D_5, G_5_f, G_5, F_5_f, F_5, Ds_5_f, Ds_5, D_5_f, D_5, C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_4_f, G_4, D_5_f, D_5
dw D_4_f, D_4, D_4_f, D_4, D_4_f, D_4, G_4_f, G_4, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_5_f, G_5, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, A_4_f, A_4, G_5_f, G_5, D_5_f, D_5
dw C_5_f, C_5, B_4_f, B_4, C_5_f, C_5, A_4_f, A_4
dw $0000
Como vimos en el capítulo anterior, necesitamos tener una variable de indicadores para la música. Vamos a Main.asm y vemos los indicadores del juego, flags. Justo debajo vamos añadir los indicadores para la música.
; -----------------------------------------------------------------------------
; Indicadores de la música
;
; Bit 0 a 3 -> Ritmo
; Bit 7 -> suena 0 = No, 1 = Sí
; -----------------------------------------------------------------------------
music:
db $00
Recordad que estas etiquetas tienen que estar de inicio a cero, de lo contrario todo podría dejar de funcionar tal y como vimos en el capítulo 5.
Reproducción
Continuamos ahora con la rutina que se va a encargar de reproducir el sonido; vamos al archivo Game.asm.
Antes de nada, es necesario indicar que canción va a sonar y el ritmo. Dado que tenemos dos canciones, en los niveles pares vamos a empezar con la primera canción, mientras que en los impares vamos a empezar con la segunda canción.
Localizamos la etiqueta ChangeLevel, y vemos que la octava y novena línea son:
inc a
cp $1f
Justo entre estas dos líneas vamos a implementar el cambio de canción dependiendo del nivel:
ld hl, Song_1
bit $00, a
jr z, changeLevel_cont
ld hl, Song_2
changeLevel_cont:
ld (ptrSound), hl
ex af, af'
ld a, (hl)
ld (music), a
ex af, af'
Apuntamos HL a la primera canción, LD HL, Song_1, comprobamos el bit cero de A para saber si el siguiente nivel es par o impar, BIT $00, A, y saltamos si es par. Si es impar apuntamos HL a la segunda canción, LD HL, Song_2.
Actualizamos el puntero a la nota siguiente (en realidad el ritmo), LD (ptrSound), HL, preservamos el valor de AF ya que A contiene el siguiente nivel, EX AF, AF’, cargamos en A el ritmo, LD A, (HL), actualizamos los indicadores de la música, LD (music), A, y recuperamos el valor de AF, EX AF, AF’.
El aspecto final de la rutina es el siguiente:
; -----------------------------------------------------------------------------
; Cambia de nivel.
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
ChangeLevel:
ld a, $06 ; Carga el color amarillo en A
ld (enemiesColor), a ; Actualiza el color en memoria
ld a, (levelCounter + 1) ; Carga en A el nivel actual en BCD
inc a ; Incrementa el nivel
daa ; Hace el ajuste decimal
ld b, a ; Carga el valor en B
ld a, (levelCounter) ; Carga el nivel actual en A
inc a ; Carga en A el siguiente nivel
ld hl, Song_1 ; Apunta HL a la canción 1
bit $00, a ; Evalúa el bit cero del nivel para saber si es par
jr z, changeLevel_cont ; Si es par salta
ld hl, Song_2 ; Si es impar apunta HL a la canción 2
changeLevel_cont:
ld (ptrSound), hl ; Actualiza la siguiente nota
ex af, af' ; Preserva el registro AF (A = siguiente nivel)
ld a, (hl) ; Carga en A el byte inferior de la nota (ritmo)
ld (music), a ; Lo carga en los indicadores de la música
ex af, af' ; Recupera el valor de AF
cp $1f ; Compara si el nivel es el 31
jr c, changeLevel_end ; Si no es el 31, salta
ld a, $01 ; Si es el 31, lo pone a 1
ld b, a ; Cargamos el valor en B
changeLevel_end:
ld (levelCounter), a ; Actualiza el nivel en memoria
ld a, b ; Carga en A el nivel en BCD
ld (levelCounter + 1), a ; Lo actualiza en memoria
call LoadUdgsEnemies ; Carga los gráficos de los enemigos
ld a, $20 ; Carga en A el número total de enemigos
ld (enemiesCounter), a ; Lo carga en memoria
ld hl, enemiesConfigIni ; Apunta HL a la configuración inicial
ld de, enemiesConfig ; Apunta HL a la configuración
ld bc, enemiesConfigEnd - enemiesConfigIni ; Carga en BC la longitud
; de la configuración
ldir ; Carga la configuración inicial en la configuración
ld hl, shipPos ; Apunta HL a la posición de la nave
ld (hl), SHIP_INI ; Carga la posición inicial
ret
Y ahora vamos a implementar la rutina que va a hacer sonar las canciones, lo vamos a hacer antes de la rutina RefreshEnemiesFire.
Play:
ld hl, (ptrSound)
ld e, (hl)
inc hl
ld d, (hl)
ld a, d
or e
jr z, play_reset
Cargamos en HL la dirección de la nota actual, LD HL, (ptrSound), cargamos en E el byte inferior de la frecuencia, LD E, (HL), apuntamos HL al byte superior, INC HL, lo cargamos en D, LD D, (HL), lo cargamos en A, LD A, D, y evaluamos si es el fin de las canciones, OR E, en cuyo caso saltamos, JR Z, play_reset.
cp $ff
jr nz, play_cont
ld a, e
ld (music), a
inc hl
ld (ptrSound), hl
ret
Si no hemos saltado, comprobamos si la nota en realidad es un cambio de ritmo, CP $FF, y saltamos si no es así, JR NZ, play_cont.
Si no hemos saltado, cargamos el ritmo en A, LD A, E, actualizamos los indicadores para la música, LD (music), A, apuntamos HL a la siguiente nota (frecuencia), INC HL, actualizamos el puntero, LD (ptrSound), HL, y salimos, RET.
play_reset:
ld hl, Song_1
ld (ptrSound), hl
ret
Si hemos llegado al final de la canciones apuntamos HL a la primera canción, LD HL, Song_1, actualizamos el puntero, LD (ptrSound), HL, y salimos, RET.
play_cont:
inc hl
ld c, (hl)
inc hl
ld b, (hl)
inc hl
ld (ptrSound), hl
ld h, b
ld l, c
Si no hemos llegado al final de las canciones, ni ha habido un cambio de ritmo, apuntamos HL al byte inferior de la nota, INC HL, lo cargamos en C, LD C, (HL), apuntamos HL al byte superior, INC HL, lo cargamos en B, LD B, (HL), apuntamos HL a la siguiente nota, INC HL, actualizamos el puntero, LD (ptrSound), HL, cargamos el byte superior de la nota en H, LD H, B, y el inferior en L, LD L, C.
Al inicio del capítulo declaramos la etiqueta BEEP con el valor de la dirección de memoria donde está alojada la rutina BEEPER de la ROM. Si vemos los comentarios, esta rutina recibe en HL la nota y en DE la frecuencia, por lo que ya tenemos todo listo para llamarla.
He aquí la diferencia fundamental con respecto a la implementación que hicimos en el capítulo anterior dado que vamos a añadir algún tipo de efecto especial, motivo éste para que implementemos en una rutina propia la llamada a la ROM.
Play_beep:
push af
push bc
push de
push hl
call BEEP
pop hl
pop de
pop bc
pop af
ret
Preservamos los valores de los registros, PUSH AF, PUSH BC, PUSH DE, PUSH HL, llamamos a la rutina de la ROM para hacer sonar la nota, CALL BEEP, recuperamos el valor de los registros, POP HL, POP DE, POP BC, POP AF, y salimos, RET.
Ahora podemos llamar a Play para que vaya sonando la música durante la partida, y a Play_beep para hacer sonar notas sueltas, como los efectos de sonido.
Es muy importante no cambiar el orden en el que están implementadas las rutinas, tened en cuenta que Play sale por Play_beep, si cambiamos el orden el resultado puede no ser el deseado.
El aspecto final de la rutina es el siguiente:
; -----------------------------------------------------------------------------
; Hace sonar las canciones.
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
Play:
ld hl, (ptrSound) ; Carga en HL la dirección de la nota actual
ld e, (hl) ; Carga en E el byte inferior de la frecuencia
inc hl ; Apunta HL al byte superior
ld d, (hl) ; Lo carga en D
ld a, d ; Lo carga en A
or e ; Comprueba si es el final de la canción
jr z, play_reset ; Salta si es el final
cp $ff ; Comprueba si escambio de ritmo
jr nz, play_cont ; Si no cambia el ritmo salta
ld a, e ; Carga el nuevo ritmo en A
ld (music), a ; Carga el nuevo ritmo en los indicadores de la música
inc hl ; Apunta HL a la siguiente nota
ld (ptrSound), hl ; Actualiza el puntero
ret
play_reset:
ld hl, Song_1 ; Apunta HL a la primera canción
ld (ptrSound), hl ; Actualiza el puntero
ret
play_cont:
inc hl ; Apunta HL al byte inferior de la nota
ld c, (hl) ; Lo carga en C
inc hl ; Apunta HL al byte superior de la nota
ld b, (hl) ; Lo carga en B
inc hl ; Apunta HL a la frecuencia de la siguiente nota
ld (ptrSound), hl ; Actualiza el puntero
ld h, b
ld l, c ; Carga la nota en HL
; -----------------------------------------------------------------------------
; Hace sonar una nota.
;
; Entrada: HL -> Nota
; DE -> Frecuencia
; -----------------------------------------------------------------------------
Play_beep:
push af
push bc
push de
push hl ; Preserva el valor de los registros
call BEEP ; Llama a la rutina de la ROM
pop hl
pop de
pop bc
pop af ; Recupera el valor de los registros
ret
Tenemos que incluir la llamada a Play desde el bucle principal del juego. Volvemos al archivo Main.asm, localizamos la etiqueta Main_loop y dentro de la misma la línea CALL CheckCrashShip. Justo debajo de esta línea añadimos las siguientes:
ld hl, music
bit $07, (hl)
jr z, main_loopCont
res $07, (hl)
call Play
main_loopCont:
Apuntamos HL a los indicadores para la música, LD HL, music, comprobamos si el bit siete está activo, BIT $07, (HL), y saltamos si no lo está, JR Z, main_loopCont.
Si el bit siete está activo lo desactivamos, RES $07, (HL), y hacemos sonar la siguiente nota de las canciones, CALL Play.
Por último, añadimos la etiqueta a la que saltamos cuando el bit siete no está activo, main_loopCont.
El aspecto de Main.asm una vez comentado es el siguiente:
org $5dad
; -----------------------------------------------------------------------------
; Indicadores
;
; Bit 0 -> se debe mover la nave 0 = No, 1 = Sí
; Bit 1 -> el disparo está activo 0 = No, 1 = Sí
; Bit 2 -> se deben mover los enemigos 0 = No, 1 = Sí
; Bit 3 -> cambia dirección enemigos 0 = No, 1 = Sí
; Bit 4 -> mover disparo enemigo 0 = No, 1 = Sí
; -----------------------------------------------------------------------------
flags:
db $00
; -----------------------------------------------------------------------------
; Indicadores de la música
;
; Bit 0 a 3 -> Ritmo
; Bit 7 -> suena 0 = No, 1 = Sí
; -----------------------------------------------------------------------------
music:
db $00
Main:
ld a, $02
call OPENCHAN ; Abre el canal 2, pantalla superior
ld hl, udgsCommon ; Apunta HL a la dirección de los UDG
ld (UDG), hl ; Cambia la dirección de los UDG
ld hl, ATTR_P ; Apunta HL a la dirección de los atributos permanentes
ld (hl), $07 ; Pone la tinta en blanco y fondo en negro
call CLS ; Limpia la pantalla
xor a ; A = 0
out ($fe), a ; Borde = negro
ld a, (BORDCR) ; Carga el valor de BORDCR en A
and $c0 ; Se queda con brillo y flash
or $05 ; Pone la tinta a 5 y el fondo a 0
ld (BORDCR), a ; Actualiza BORDCR
di ; Desactiva la interrupciones
ld a, $28 ; Carga 40 en A
ld i, a ; Lo carga en el registro I
im 2 ; Pasa a modo 2 de interrupciones
ei ; Activa las interrupciones
ld a, (flags) ; Carga los indicadores en A
Main_start:
ld hl, enemiesCounter ; Apunta HL al contador de enemigos
ld de, enemiesCounter + $01 ; Apunta DE al contador de niveles
ld (hl), $00 ; Pone a cero el contador de enemigos
ld bc, $08 ; Carga en BC el número de bytes a limpiar
ldir ; Limpia los bytes
ld a, $05
ld (livesCounter), a ; Pone el contador de vidas a 5
call ResetEnemiesFire ; Inicializa los disparos enemigos
call ChangeLevel ; Cambia de nivel
call PrintFirstScreen ; Pinta la pantalla de menú y espera
call PrintFrame ; Pinta el marco
call PrintInfoGame ; Pinta los títulos de información de la partida
call PrintShip ; Pinta la nave
call PrintInfoValue ; Pinta la información de la partida
call LoadUdgsEnemies ; Carga los enemigos
call PrintEnemies ; Los pinta
; Retardo
call Sleep ; Produce un retardo antes de empezar el nivel
; Bucle principal
Main_loop:
call CheckCtrl ; Comprueba la pulsación de los controles
call MoveFire ; Muevo el disparo
push de ; Preserva DE
call CheckCrashFire ; Evalúa las colisiones entre enemigos y disparo
pop de ; Recupera DE
ld a, (enemiesCounter) ; Carga el número de enemigos activos en A
or a ; Comprueba si es 0
jr z, Main_restart ; Si es 0 salta
call MoveShip ; Mueve la nave
call ChangeEnemies ; Cambia la dirección de los enemigos si procede
call MoveEnemies ; Mueve los enemigos
call MoveEnemiesFire ; Mueve los disparos de los enemigos
call CheckCrashShip ; Evalúa las colisiones entre la nave
; y los enemigos y sus disparos
ld hl, music ; Apunta HL a los indicadores para la música
bit $07, (hl) ; Evalúa si debe sonar una nota
jr z, main_loopCont ; Si no debe sonar, salta
res $07, (hl) ; Desactiva el bit siete de music
call Play ; Hace sonar la nota
main_loopCont:
ld a, (livesCounter) ; Carga las vidas en A
or a ; Comprueba si están a cero
jr z, GameOver ; Si están a cero salta, ¡GAME OVER!
jr Main_loop ; Bucle principal
Main_restart:
ld a, (levelCounter) ; Carga el número de nivel en A
cp $1e ; Comprueba si es el 31 (tenemos 30)
jr z, Win ; Si es el 31 salta, ¡VICTORIA!
call FadeScreen ; Hace el fundido de la pantalla
call ChangeLevel ; Cambia de nivel
call PrintFrame ; Pinta el marco
call PrintInfoGame ; Pinta los títulos de información
call PrintShip ; Pinta la nave
call PrintInfoValue ; Pinta la información
call PrintEnemies ; Pinta los enemigos
call ResetEnemiesFire ; Reinicia los disparos de los enemigos
; Retardo
call Sleep ; Produce un retardo
jr Main_loop ; Bucle principal
; ¡GAME OVER!
GameOver:
xor a ; Pone A = 0
call PrintEndScreen ; Pinta la pantalla de fin y espera
jp Main_start ; Menú principal
; ¡VICTORIA!
Win:
ld a, $01 ; Pone A = 1
call PrintEndScreen ; Pinta la pantalla de fin y espera
jp Main_start ; Menú principal
include "Const.asm"
include "Var.asm"
include "Graph.asm"
include "Print.asm"
include "Ctrl.asm"
include "Game.asm"
end Main
Control de música por interrupciones
Es la rutina de interrupciones el lugar donde indicamos el momento en el que tiene que sonar una nueva nota, vamos al archivo Int.asm y lo primero que vamos a hacer añadir dos constantes justo por delante de T1: EQU $C8:
FLAGS: EQU $5dad
MUSIC: EQU $5dae
Estás etiquetas hacen referencia a las posiciones de memoria en las que tenemos definidos los indicadores en Main.asm.
Después de los cuatro PUSH de la etiqueta Isr, nos encontramos la línea LD HL, $5DAD que vamos a modificar dejándola como sigue:
ld hl, FLAGS
Al final del archivo vamos a añadir las variables donde guardar el ritmo de la canción, y el contador para saber si hay que activar el bit que indicará que hay que hacer sonar una nota.
countTempo: db $00
tempo: db $00
Localizamos la etiqueta Isr_T1, y en la quinta línea encontramos JR NZ, Isr_end. Vamos a modificar esta línea dejándola como sigue:
jr nz, Isr_sound
Localizamos la etiqueta Isr_end y justo por encima de ella vamos a implementar el control de la música.
Isr_sound:
ld a, (MUSIC)
and $0f
ld hl, tempo
cp (hl)
jr z, Isr_soundCont
ld (hl), a
jr Isr_soundEnd
Cargamos en A los indicadores para la música, LD A, (MUSIC), nos quedamos con el ritmo, AND $0F, apuntamos HL al ritmo actual, LD HL, tempo, y lo comparamos con el ritmo que hay en los indicadores para la música, CP (HL). Si los dos valores son iguales, no hay cambio de ritmo y saltamos, JR Z, Isr_soundCount. Si los valores son distintos, actualizamos el ritmo actual, LD (HL), A, y saltamos, JR Isr_soundEnd.
Isr_soundCont:
ld a, (countTempo)
inc a
ld (countTempo), a
cp (hl)
jr nz, Isr_end
Cargamos en A el contador del ritmo, LD A, (countTempo), lo incrementamos, INC A, lo actualizamos en memoria, LD (countTempo), A, los comparamos, CP (HL), y si no son iguales, no hay que hacer sonar la nota y saltamos, JR NZ, Isr_end.
Isr_soundEnd:
xor a
ld (countTempo), a
ld hl, MUSIC
set $07, (hl)
Si no hemos saltado hay que hacer sonar la nota. Ponemos A a cero, XOR A, actualizamos el contador del ritmo, LD (countTempo), A, apuntamos HL a los indicadores para la música, LD HL, MUSIC, y activamos el bit siete para indicar que debe sonar una nota, SET $07, (HL).
El aspecto final de Int.asm, una vez comentado, es el siguiente:
org $7e5c
FLAGS: EQU $5dad ; Indicadores generales
MUSIC: EQU $5dae ; Indicadores para la música
T1: EQU $c8 ; Interrupciones para activar el cambio de dirección de enemigos
Isr:
push hl
push de
push bc
push af ; Preserva el valor de los registros
ld hl, FLAGS ; Apunta HL a los indicadores
set $00, (hl) ; Activa el bit 0, mover nave
ld a, (countEnemy) ; Carga en A el contador para mover los enemigos
inc a ; Lo incrementa
ld (countEnemy), a ; Lo actualiza
sub $03 ; Le resta 3
jr nz, Isr_T1 ; Si el valor no es cero, salta
ld (countEnemy), a ; Pone el contador a cero
set $02, (hl) ; Activa el bit 2 de los indicadores, mover enemigos
set $04, (hl) ; Activa el bit 4, mover disparo enemigo
; Cambio de dirección de los enemigos
Isr_T1:
ld a, (countT1) ; Carga en A el contador para cambiar la dirección
inc a ; Lo incrementa
ld (countT1), a ; Lo actualiza en memoria
sub T1 ; Le resta las interrupciones que tienen que pasar
jr nz, Isr_sound ; Si el valor no es cero, salta
ld (countT1), a ; Pone el contador a cero
set $03, (hl) ; Activa el bit 3 de los indicadores, cambiar dirección enemigos
; Sonido
Isr_sound:
ld a, (MUSIC) ; Carga en A el valor de los indicadores para la música
and $0f ; Se queda con el ritmo
ld hl, tempo ; Apunta HL al ritmo actual
cp (hl) ; Lo compara con los indicadores para la música
jr z, Isr_soundCont ; Si son iguales, salta
ld (hl), a ; Si son distintos, actualiza el ritmo actual
jr Isr_soundEnd ; Salta para hacer sonar la nota
Isr_soundCont:
ld a, (countTempo) ; Carga en A el valor del contador del ritmo
inc a ; Lo incrementa
ld (countTempo), a ; Lo actualiza en memoria
cp (hl) ; Lo compara con el ritmo actual
jr nz, Isr_end ; Si son distintos, salta
Isr_soundEnd:
xor a ; Pone A = 0
ld (countTempo), a ; Pone el contador de ritmo a 0
ld hl, MUSIC ; Apunta HL a los indicadores para la música
set $07, (hl) ; Activa el bit 7, sonar
Isr_end:
pop af
pop bc
pop de
pop hl ; Recupera los valores de los registros
ei ; Activa las interrupciones
reti ; Sale
; Contador para cambio de dirección de los enemigos
countT1: db $00
; Contador para movimiento de los enemigos
countEnemy: db $00
; Contador para hacer sonar notas
countTempo: db $00
; Ritmo actual
tempo: db $00
Si compilamos y cargamos en el emulador, debemos tener música durante la partida, y cada nivel, ya sea par o impar, debe empezar sonando una u otra canción.
Si la dificultad es demasiada, en el bucle principal comentad la línea CALL CheckCrashShip, para evitar que nos maten.
Efectos de sonido
Además de la música, vamos a implementar efectos de sonido. En concreto vamos a implementar tres efectos distintos:
- Con el movimiento los enemigos.
- Con la explosión la nave.
- Con el disparo.
Estos efectos de sonido los vamos a implementar como rutinas independientes en Game.asm. Como ya hemos visto como hacemos sonar cada nota, vamos a ver el código final de las rutinas sin entrar en detalle, llegados a este punto ya dominamos esta parte.
Localizamos la rutina RefreshEnemiesFire y justo por delante de ella añadimos las líneas siguientes:
; -----------------------------------------------------------------------------
; Emite el sonido del movimiento de los enemigos
;
; Altera el valor de los registros HL y DE
; -----------------------------------------------------------------------------
PlayEnemiesMove:
ld hl, $0a ; Carga la nota en HL
ld de, $00 ; Carga la frecuencia en DE
call Play_beep ; Emite la nota
ld hl, $14 ; Carga la nota en HL
ld de, $20 ; Carga la frecuencia en DE
call Play_beep ; Emite la nota
ld hl, $0a ; Carga la nota en HL
ld de, $10 ; Carga la frecuencia en DE
call Play_beep ; Emite la nota
ld hl, $30 ; Carga la nota en HL
ld de, $1e ; Carga la frecuencia en DE
jr Play_beep ; Emite la nota y sale
; -----------------------------------------------------------------------------
; Emite el sonido de la explosión de la nave
;
; Altera el valor de los registros HL y DE
; -----------------------------------------------------------------------------
PlayExplosion:
ld hl, $27a0 ; Carga la nota en HL
ld de, $2b / $20 ; Carga la frecuencia en DE
call Play_beep ; Emite el sonido
ld hl, $13f4 ; Carga la nota en HL
ld de, $37 / $20 ; Carga la frecuencia en DE
call Play_beep ; Emite el sonido
ld hl, $14b9 ; Carga la nota en HL
ld de, $52 / $20 ; Carga la frecuencia en DE
call Play_beep ; Emite el sonido
ld hl, $1a2c ; Carga la nota en HL
ld de, $41 / $20 ; Carga la frecuencia en DE
jr Play_beep ; Emite el sonido y sale
; -----------------------------------------------------------------------------
; Emite el sonido del disparo de la nave
;
; Altera el valor de los registros HL y DE
; -----------------------------------------------------------------------------
PlayFire:
ld hl, $64 ; Carga la nota en HL
ld de, $01 ; Carga la frecuencia en DE
jr Play_beep ; Emite el sonido y sale
Como podemos ver, en las tres rutinas se van cargando las notas en HL, las frecuencias en DE, y se llama con CALL a la rutina Play_beep, excepto la última nota de cada efecto, en la que usamos JR para salir con el RET de Play_beep.
Ya solo queda llamar a cada rutina. Localizamos la rutina MoveEnemiesFire, y vemos que la última línea es JP RefreshEnemiesFire. Justo por encima de esta línea vamos a añadir la llamada al sonido que vamos a emitir cuando se mueven los enemigos, más en concreto sus disparos.
call PlayEnemiesMove ; Emite el sonido de movimiento de enemigos
La siguiente llamada que vamos a incluir es al sonido que se produce al disparar. Localizamos la rutina MoveFire y tras la sexta línea, SET $01, (HL), añadimos las líneas siguientes:
push hl ; Preserva el valor de HL
call PlayFire ; Emite el sonido del disparo
pop hl ; Recupera el valor de HL
En el caso del sonido de la explosión, no vamos a llamarlo desde Game.asm, aunque parezca incoherente.
Si localizamos la etiqueta checkCrashShip_endLoop, que está dentro de la rutina CheckCreashShip, y nos fijamos en la línea anterior JP PrintExplosion, podemos deducir que cuando la nave es alcanzada saltamos a pintar la explosión, y si vamos a PrintExplosion, observamos que pinta la explosión y salta a pintar la nave, JP PrintShip y sale por allí.
Visto esto, y para no tener que modificar el comportamiento actual, y aunque no sea coherente, la llamada a la emisión del sonido la vamos a hacer desde PrintExplosion. Vamos al archivo Print.asm, localizamos la rutina PrintExplosion y vemos que la última línea es JP PrintShip. Justo por encima de esta línea añadimos la siguiente:
call PlayExplosion ; Emite el sonido de la explosión
Ya tenemos la música y todos los efectos de sonido de nuestro juego. Ahora podemos compilar y ver los resultados.
Ensamblador ZX Spectrum, conclusión
En este capítulo hemos añadido efectos de sonido e integrado la música del capítulo anterior para que suene durante la partida.
En el próximo capítulo implementaremos la selección de distintos niveles de dificultad, la posibilidad de silenciar la música y añadiremos la pantalla de carga.
Todo el código que hemos implementado lo podéis descargar aquí.
Enlaces de interés
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF y EPUB
- Proyecto en itch.io
- Archivo .dsk con los juegos de los tutoriales
- Personalización y depuración con ZEsarUX
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.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.