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.
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
- Ensamblador ZX Spectrum Pong
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF y EPUB
- Proyecto en itch.io
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.