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.

Ensamblador ZX Spectrum, música
Ensamblador ZX Spectrum, música

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

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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies
A %d blogueros les gusta esto: