Efectos de sonido – mini teoría

Para darle un cierre al tutorial, hoy vamos a abordar el tema de los efectos sonoros. Personalmente es un tema que me costó bastante de entender, por lo que decidí simplificar lo mas posible el apartado sonoro.
Primero, vamos a ver que es lo que logramos:

Un poco de teoría

El sonido en la C64 es gestionado por un chip, el famoso SID 6581. Este cuenta con 3 canales independientes, cada uno de ellos posee 4 tipos de ondas diferentes (triangular, diente de sierra, cuadrada/pulsos, ruido blanco), generador de envolvente ADSR, filtros, y unas cuantas cosas mas que no entendí. Para los efectos que vamos a hacer, con esto tenemos mas que suficiente, si queremos profundizar podemos ver los todos los registros en https://www.c64-wiki.com/wiki/SID, o algo mas avanzado en http://codebase64.org/doku.php?id=base:sid_programming.

Los osciladores

Cada canal puede generar 4 ‘tipos’ de sonidos básicos (no se me ocurre como decirlo – si alguien sabe por favor comenten):

  • Triangular, con un sonido suave, como si fuese una flauta
  • Diente de sierra: mas agresivo, mucho mas timbrado… quizá como un violín?
  • Cuadrada/pulsos: Mucho mas agresivo?? no lo sé, no logré hacerlo sonar con esta forma de onda
  • Ruido Blanco: Básicamente ruido, ideal para explosiones, etc

Todos estos sonidos básicos puede luego modificarse en frecuencia, envolvente, filtros…

Envolventes

Los generadores de envolventes modelan la amplitud del sonido desde el momento en que se le da la orden de tocar una nota hasta que le damos la orden de apagarla. ADSR son las iniciales de Attack, Decay, Sustain y Release, y lo que especificamos es el tiempo que tarda la nota en llegar a máximo volumen desde que la disparamos (attack), el tiempo que tarda desde ese máximo hasta el volumen Sustain (Decay), el volumen que queremos que suene (sustain) y el tiempo que queremos que dure desde que damos la orden de apagar la nota hasta el volumen cero. Más claro en el siguiente grafico:

Y… esto como funciona?

Para generar un sonido primero tenemos que configurar algunos registros del SID. Antes que nada no está de más ponerlos todos en 0, desde la posición 54272 hasta 54296 (por las dudas que tengan basura seteada). Luego tenemos que poner el volumen general al máximo, utilizando la posición de memoria 54296, configurar el ADSR (en este caso para el canal 1), con las posiciones de memoria 54277 (Attack/Decay) y 54278 (Release/Sustain), establecer la frecuencia con las posiciones 54272 y 54273, para RECIÉN ahí poder disparar la nota, con la posición 54276… Ufff!, mejor un código de ejemplo en BASIC, que dispara un sonido sencillo:

10 for l = 54272 to 54296 : poke l,0 : next
20 poke 54296, 15 : rem volumen maximo
30 poke 54273, 100 : rem frecuencia MSB
40 poke 54272, 0 : rem frecuencia LSB
50 poke 54277,  8*16 + 10 : rem atack 8 - decay 10
60 poke 54278,  2*16 + 12 : rem sustain 2 -  release 12
70 poke 54276, 32 + 1 : rem onda triangular, comienza
71 print"comienza sonido"
75 for pause = 0 to 2500: next : rem pausa 4 segundos aprox
80 poke 54276, 32 : rem apaga sonido
81 print"finaliza sonido"

si ejecutamos este programita veremos que inicia un sonido desde un volumen 0, sube rápidamente al máximo, luego baja a un volumen mínimo, y se queda en ese volumen por aproximadamente 4 segundos, hasta que comienza a desvanecerse hasta desaparecer (noten que aparece el cartel “FINALIZA SONIDO”, pero es en ese momento en que comienza a desvanecerse, aquí esta el tiempo del parámetro Release).
El ejemplo está comentado para que se comprenda fácilmente, pero quisiera dedicarle un par de lineas a explicar como se establece la frecuencia (el tono), como configuramos el ADSR, y como disparamos/apagamos el sonido.
El Attack/Sustain se especifica en la dirección de memoria 54277, utilizandose los 4 bits superiores para el Attack y los 4 bits inferiores para el Sustain, por eso en el ejemplo lo escribo de esa forma, para que se entienda mejor que es lo que estoy configurando (8*16 seteo los 4 bits superiores, y adicionando un valor de 0 a 15 especifico los 4 inferiores). Todo esto aplica también para Sustain/Release.
La frecuencia se programa con 2 bytes, ya que no nos alcanzaría una sola posición de memoria para cubrir todo el rango audible. El cálculo no lo tengo correctamente aquí, pero para lo que necesitamos en este tutorial vamos a dejar el LSB (54272) en cero, y vamos a toquetear el HSB (54273) para cambiar el tono. Si queremos profundizar en este tema recomiendo ir a http://codebase64.org/doku.php?id=base:how_to_calculate_your_own_sid_frequency_table, si clickean en el enlace comprobarán que es realmente complicado especificar correctamente una frecuencia.
Finalmente queda ver como establecemos la forma de onda y comenzamos el sonido. La posicion 54276 establece con los 4 bits superiores que forma de onda queremos utilizar, y con el bit 0 si queremos que suene o se apague. Por eso comenzamos un sonido con POKE 54276,32 + 1, y lo apagamos con POKE 54276,32.

Por ahora suficiente de teoría, con el material aquí proporcionado podemos generar los sonidos de los lasers cuando disparan, las explosiones de los jetpacs y los Fuels, y un sonido simple para cuando agarramos combustible… pero eso para el próximo post. ̉̉Nos vemos!

Menu principal

Para ir cerrando el mini curso, vamos a realizar un menú principal, en el que estará el nombre del juego, una leyenda que nos indique como comenzar a jugar (algo así como “Pulse F1 para comenzar”), y un efecto sencillo, para que no quede tan soso. Para muestra, mejor un vídeo:

Primero vamos a describir el efecto. Es simplemente un ciclado de colores del fondo de pantalla (se acuerdan? los primeros posts… ), pero les vamos a introducir un cutre delay, para que se engrosen las lineas. El efecto es medio azaroso, debido a que no hacemos ningún control del raster, pero así quedó mas o menos bonito ( eso si, pulsas una tecla y se desbanda completamente… esas cosas que tiene la simpleza 😛 ).
Ademas, la pantalla la dibujamos con caracteres invertidos, así tapamos todo, y lo que se muestra es el fondo que pasa a través de los caracteres invertidos…
Pfff, como explicación es pésima, mejor un ejemplo visual:

Caracteres invertidos jetpac_solo
Fondo ciclando anima_texto
Resultado final anima_texto_2

Para el código del ciclado de colores vamos a hacer lo siguiente:

main_menu:
{
          ldx            #0             
          stx            cborde
          ldx            #1             // establecemos colores de 
          stx            cfondo         // borde y fondo

          lda            #0      
          sta            spractive      // desactivamos todos los sprites
  
          ldx            #<menu_screen   // utilizamos la función
          stx            lsbCopyAddress     // que explicamos en el 
          ldx            #>menu_screen   // capitulo anterior y ... 
          stx            msbCopyAddress 
          jsr            copyToScreen       // copiamos pantalla

          ldx            #<menu_color
          stx            lsbCopyAddress
          ldx            #>menu_color
          stx            msbCopyAddress     
          jsr            copyToScreenColor  // idem para colores 

          // INICIO DEL EFECTO DE FONDO.
          ldy #0                  // inicializo indice tabla colores

loop:
          ldx            $CB       // leo ultima tecla pulsada
          cpx            #$04      // si pulso F1
          beq            go_main   // comienzo el juego

          lda            color_ramp,y   // tomamos el color de la 
                                        // tabla de colores que vamos 
                                        // a ciclar

loop_raster:
          ldx            raster    
r_line:   cpx            #$0         // posición de comparación (r_line + 1)
          bne            loop_raster
                                     // esto es mejorable... 
          inc            r_line+1    // incrementa posición de comparación
          inc            r_line+1    // esto lo que hace es engrosar la linea
          inc            r_line+1    // de fondo 
          inc            r_line+1    // es algo así como código automodificable
          inc            r_line+1    // si quitan algunos de estos inc
          inc            r_line+1    // se afina la linea
          inc            r_line+1    // si agregan se engrosa
          inc            r_line+1 
          inc            r_line+1 
          inc            r_line+1 
          inc            r_line+1 
          inc            r_line+1 
          sta            cfondo      // establezco el color de fondo    
          
          iny                        // tomo el próximo color de la rampa
          cpy            #7          
          bne            skip     
          ldy            #0          // y ciclo si llego al final de la rampa
          
skip:          
          jmp            loop     

go_main:
          jmp            start_game
}          
  
// colores que voy a ir ciclando en el fondo
color_ramp:
          .byte 7, 10, 8, 2, 9, 2, 8, 10

Ademas pueden ver que detectamos la pulsación de la tecla F1, eso se hace checkeando la posición de memoria $CB, que guarda la última tecla pulsada.

Quisiera detenerme en lo que me parece la parte mas confusa del código, que son las lineas inc r_line + 1. Lo que estamos haciendo aquí es modificar la posición de memoria ‘r_line + 1’, que corresponde a lo que en el código figura como ‘#$0’. Entonces, lo que hacemos es incrementar hasta el siguiente posición del raster, para luego cambiar al siguiente color. Si quito lineas ‘inc’, entonces la linea se afina, si agrego se engrosa…

Pueden descargar el proyecto desde el repositorio que se encuentra en https://github.com/moonorongo/jp_wars

y esto es todo por hoy, para la próxima entrega voy a tratar de poner algunos efectos de sonido (digo tratar, porque es un tema bastante difícil para mí… 😦 ). Hasta la próxima!

Actualización!!: A sugerencia del amigo @josepzin, del foro de Commodoremania, cambié la acción del menú principal para que, en vez de tener que pulsar F1 para comenzar, simplemente con disparar con cualquiera de los dos joysticks comience la acción. El código a continuación:

loop:
//          ldx            $CB       // CODIGO ANTERIOR:
//          cpx            #$04      // lo comenté a fin de 
//          beq            go_main   // mostrar el cambio realizado
          
          lda            joy1       // si pulso disparo 1
          and            #16           
          beq            go_main    // comienza el juego
          lda            joy2       // o disparo 2
          and            #16           
          beq            go_main    // también

Ahora si… 🙂

Migración a KickAssembler

Luego de un par de semanas inactivo sigo con el curso, pero previamente vamos a tener que introducir un pequeño (o no tanto) cambio. Por cuestiones ajenas a mi voluntad el sistema operativo de mi computadora reventó, así que decidí instalar LinuxMint. Lo que no pensé es que no iba a poder correr el CbmPrg Studio (solo corre en plataformas Windows), por lo que tuve que buscar otra opción, en lo posible multiplataforma.

Después de una larga investigación decidí optar por darle una oportunidad a KickAssembler, el cual es un crossassembler muy potente, escrito en java, por lo que se puede correr tanto en Windows, OSX, o Linux. El mismo posee un lenguaje de scripting muy similar a javascript, que te permite hacer autenticas maravillas, como por ejemplo importar un archivo .JPG, y convertirlo a una secuencia de bytes para mostrarlo en la pantalla de la C64.

Tuve que realizar algunas modificaciones al código (afortunadamente no fueron tantas), pero al final quedo mucho mejor. No tenemos la facilidades del IDE como por ejemplo hacer un click en un botón para ejecutar, pero si nos armamos un pequeño script que nos compile los archivos se nos facilita mucho la tarea.

Para que no se vuelvan locos, aquí voy a hacer una pequeña reseña de las modificaciones que realicé:

Main.asm

Como no tenemos un IDE que nos organice el proyecto todos los archivos que utilicemos los tenemos que importar en nuestro main.asm, lo cual es mas cómodo y claro (podemos ver como está con un simple editor de texto, sin depender de un programa especifico para una plataforma).

// BasicUpstart2: esta macro genera codigo en 
//  BASIC para iniciar el programa
BasicUpstart2(main)

/*
    JetPac Wars! Main File
*/

// Includes
#import "sprites.asm"
#import "vars.asm"
#import "macros.asm"

          *         = $1000 "Main Program"
main:
            jmp            main_menu

Aquí tenemos las primeras lineas de nuestro nuevo main.asm. La primera de todas muestra una funcionalidad de KickAsm, la cual genera el código BASIC para arrancar el juego, y toma como parámetro la etiqueta de inicio del código (noten que la etiqueta está inmediatamente luego * = $1000, o sea generará un SYS 4096).

Quienes tengan unos conocimientos de algún lenguaje ‘tipo C’ podrán apreciar que los comentarios se realizan de idéntica manera. Los archivos extras se incluyen con #import “archivo_a_importar.asm” en el orden en que los vamos importando (en el CbmPrg Studio teniamos que ir a las propiedades del projecto y cambiar el orden en una lista… aqui los tenemos todos a la vista).

Macros, labels, scoping y constantes

Las macros las invocamos como si fueran una función, ejemplo miSuperMacro(param1, param2, param3), y las definimos de la siguiente manera (lo podemos hacer en cualquier parte, no es necesario que sea antes de usarlas):

.macro unsetB8(spriteNumber) {
          lda            sprxBit8 
          and            #255 - spriteNumber
          sta            sprxBit8
}

Esta macro la invocamos simplemente con (por ejemplo) unsetB8(1).

Las etiquetas (labels) se definen con ‘nombre_etiqueta:’, y si le precedemos un signo ‘!’ podemos utilizar el mismo nombre dentro del ámbito en que se definen (Multi labels). Eso es util cuando tenemos una etiqueta tipo ‘next:’, en vez de andar poniendo ‘next1:’, ‘next2’, ‘next…’ simplemente las nombramos a todas con ‘!next:’ y las utilizamos como (por ej) ‘jmp !next+’ si quiero ir al siguiente ‘!next:’ o ‘jmp !next+’ si quiero ir al anterior . A continuación un ejemplo de código que muestra mas claramente lo que estoy explicando:

tickGenerator:
{
          ldx tick4
          dex
          stx tick4
          cpx #$ff
          bne !next+   // Salto al siguiente !next
          
          ldx #3
          stx tick4 
!next:
          ldx tick64
          dex
          stx tick64
          cpx #$ff
          bne !next+  // salto al !next siguiente 
          jmp !next-  // salto al !next previo
          ldx #64
          stx tick64 
!next:
          rts
}

Los lectores más observadores se habrán percatado de las llaves que encierran el código, como si fuera una función de c/java/php/etc … Estas llaves se utilizan para definir un ámbito (scope) de las etiquetas, así puedo darles el nombre que yo quiera, que no van a entrar en conflicto con nombres definidos en otros ámbitos.

Finalmente, el último cambio que tuve que introducir es la forma en que defino las constantes/punteros en ‘vars.asm’. Anteriormente se definían como ‘nombre = valor’, pero en KickAsm hay que hacerlo como ‘.const nombre = valor’.

Instalación y compilación

La instalación es muy sencilla, simplemente es un archivo comprimido que podemos descargar desde http://www.theweb.dk/KickAssembler/KickAssembler.zip, y lo descomprimimos en una carpeta que luego podamos recordar (ejemplo: c:/Archivos de Programa/KickAssembler).

Para compilar, simplemente escribiendo java -jar ‘c:/Archivos de Programa/KickAssembler/KickAss.jar’ main.asm hace toda la magia (lo mejor es armarse un archivo tipo ‘build.cmd’ que ejecutaremos cada vez que tengamos que compilar).

Nota: KickAssembler esta hecho en Java, por lo que necesitamos el runtime de Java instalado. Para descargarlo: https://java.com/en/download/.

Pueden descargar todas las modificaciones realizadas desde <a href=”https://github.com/moonorongo/jp_wars”>https://github.com/moonorongo/jp_wars</a&gt;

Ademas! Pueden descargar un release que armo la gente de Genesis Project, al que le hicieron un trainer y una intro muy linda: <a href=”http://csdb.dk/release/?id=157158″>Jetpac Wars Preview V3 +</a>

Y por hoy es todo, ahora si, en la próxima entrega voy a explicar como hice la pantalla de portada, y explicar el código de finalización de la partida. Hasta la proxima!

Funciones desde assembler

La entrada de hoy sera teórica, y surge de la necesidad de mejorar el código. Ya estamos entrando en las fases finales del juego, acabo de terminar la pantalla de presentación (que en próximas entregas mostraré), así como también definí la duración de la partida a 10 muertes y ademas agregué inmunidad durante la entrada de los jugadores.
Al querer armar el menú principal me encontré que tenía que reescribir/duplicar la rutina que copiaba la pantalla a la memoria de video (que mostraba el campo de juego), lo que me pareció una tremenda abominación.
En cualquier otro lenguaje de medio nivel lo habria resuelto con una función, lamentablemente aquí no disponemos de dichas herramientas… o si?

El modo de direccionamiento (Indirect),y

En las primeras entregas (6502 Basico – Registros) comenté muy por arriba los modos de direccionamiento del 6502, y claramente no explique los dos últimos debido a que en ese momento no comprendía su funcionamiento, ni tampoco encontraba su utilidad.
Pero “la necesidad tiene cara de hereje“, así que al momento de querer implementar algo parecido a una función me puse a investigar el modo (Indirect),y.  En este modo le pasamos una dirección de memoria de la Zero Page, y lo que hace es tomar esa posición como el byte menos significativo de un puntero de memoria, y la posición siguiente como el byte mas significativo, y finalmente le adiciona el valor de Y.

Ejemplo: vamos a suponer que en la posición de memoria $20 (Zero page) tenemos el valor $00, y en la posición $21 (la siguiente) tenemos el valor $04. Ademas, en el registro Y vamos a poner el valor $00.
Si introducimos el operando LDA ($20),Y el registro A va a tomar el valor la posición de memoria $0400 (el primer caracter de la pantalla), ya que $0400 se forma de con el valor contenido en las posiciones $20 y $21, y le adicionamos Y, que tiene el valor ‘0’.

Ajahm… y esto de que me sirve???

Bueno, bien no se, pero yo le encontré una utilidad para una función que copia una pantalla a la memoria de video. Basicamente le indicamos en 2 direcciones previamente definidas la dirección de origen de la pantalla, y la copiará a la memoria de video.

El código!


; punteros utilizados por copyScreen
;   CopyAddress 
;       lsbCopyAddress = $20
;       msbCopyAddress = $21           
; scrPtr    = $0400

copyToScreen
; posiciones de pantalla 0 - 255 
          ldy            #$00               ; Inicializo el indice
@loop
          lda            (lsbCopyAddress),y ; cargo en A CopyAddress + Y
          sta            scrPtr,y           ; y lo guardo en pantalla + y
          
          iny                               ; incremento indice
          cpy            #$00               
          bne            @loop              ; loopea si no completo 255 loops
          
; posiciones de pantalla 256 - 512
          ldy            #$00               ; inicializo Y en 0 (quiza innecesario...)
          inc            msbCopyAddress     ; incremento MSB de CopyAddress
                                            ; ya que voy a copiar los siguientes 255 bytes
                                            ; de la pantalla
@loop2
          lda            (lsbCopyAddress),y ; idem el loop anterior
          sta            scrPtr + $100,y    ; salvo que esta vez lo guardamos 
                                            ; en $0500 (el siguiente bloque de 255 bytes)
          iny
          cpy            #$00       
          bne            @loop2

; posiciones de pantalla 513 - 768 (idem loops anteriores)
          ldy            #$00
          inc            msbCopyAddress
@loop3
          lda            (lsbCopyAddress),y
          sta            scrPtr + $200,y   ; $0600
          
          iny
          cpy            #$00       
          bne            @loop3

; posiciones de pantalla 768 - 1000  (idem loops anteriores)
          ldy            #$00
          inc            msbCopyAddress
@loop4
          lda            (lsbCopyAddress),y
          sta            scrPtr + $300,y   ; $0700
          
          iny
          cpy            #232       
          bne            @loop4 ; pero este comparamos antes de 255, 
                                ; ya que solo queremos copiar 1000 bytes, no 1024
          
          rts

Y como usamos esta “función”?

Bien, supongamos que generamos una pantalla con el editor, y la guardamos con una etiqueta de nombre screen, entonces la llamaríamos de la siguiente manera:

    ldx #<screen
     stx lsbCopyAddress
     ldx #>screen
     stx msbCopyAddress
     jsr copyToScreen

Nótese que utilizamos ‘<‘ para indicar que queremos el LSB de screen, y ‘>’ para indicar que queremos el MSB de screen (una característica muy común de los ensambladores).

y esto es todo por hoy… en la próxima entrega vamos a ir armando el menu de entrada, y en las siguientes vamos a ir finalizando el tutorial. Nos vemos!

Recargas de combustible – parte 3 (final)

Y para terminar con los tanques de combustible, en esta entrada vamos a hacer la deteccion de colisiones. Aqui la idea es que cualquiera de los dos jugadores que agarre el tanque le incremente en 10 unidades el fuel. Y si el fuel lo alcanza un disparo, entonces lo destruye. Basicamente modificamos el archivo detectCollision.asm para que detecte las colisiones jugador1-fuel, jugador2-fuel, disparo1-fuel, disparo2-fuel.

Primero, las variables que necesitaremos (en vars.asm):

tempCollision = $12 ; guardo el estado de colision $d01e
FUEL = $10 ; 16, constante sprite
cantFUEL = 10 ; cantidad de combustible de cada fuel

Luego el código, una simplificación de lo que ya estaba, y luego el agregado de las partes que comprueban las colisiones (detectCollision.asm):

detectCollision
          lda            $d019     
          and            #$04      
          cmp            #$04      
          bne            @jmpSkipSprDetect0
          
          lda            $d01e
          sta            tempCollision  ; guarda los sprites que colisionaron

           ; detecto que ocurrio una colision de sprites
           ; colision disparo1 con jugador 2
           .
           .
           .

@checkJP1
                                   ; check JP1 & fire JP2
           ; colision disparo2 con jugador 1
           .
           .

@checkJP1Fuel                   
                                        ; detecta si jugador1 agarro fuel
          lda            tempCollision  ; recupero el estado de colision
          and            #JP1 + FUEL
          cmp            #JP1 + FUEL
          bne            @checkJP2Fuel  ; si no colisionaron compruebo 
                                        ; la siguiente
                                        ; si colisionaron:
          ldx            #4             ; paso el statusFuel a 4 
          stx            statusFuel
                                        
          lda            JP1Jet         ; y le sumo combustible al jugador 1
          adc            #cantFUEL 
          sta            JP1Jet    
          bcs            @setMaxFuel1   ; si ocurre un desbordamiento 
                                        ; seteo el maximo (255)
          
          jmp            @checkJP2Fuel  ; chequeo el siguiente
          
@setMaxFuel1
          lda            #$ff      
          sta            JP1Jet    

          
@checkJP2Fuel                   
          ; idem codigo que checkJP1Fuel
          .
          .
          .


@checkFireFuel1                  
                                          ; detecta si se destruyo con tiros 
          lda            tempCollision    ; lo recupero del temporal
          and            #FUEL + FJP1
          cmp            #FUEL + FJP1
          bne            @checkFireFuel2  ; si no chequea el siguiente caso
          
          jsr            turnOffFire1     ; apaga el disparo 1
          ldx            #4
          stx            statusFuel       ; y pasa el statusFuel a 4

@checkFireFuel2                  
          ; idem codigo checkFireFuel1
          .
          .
          .

@skipSprDetect
          rts

Como siempre, el código completo lo pueden bajar desde el repositorio:
https://github.com/moonorongo/jp_wars.git

Hasta la próxima!

Recargas de combustible – parte 2

Seguimos con la segunda parte de las “recargas de combustible”. En la primer parte vimos como implementar un generador de números aleatorios, algo que es trivial en cualquier lenguaje de programación, pero en ensamblador es un pequeño dolor de cabeza.
En esta segunda parte vamos a ver el código para que cada cierto intervalo de tiempo caiga un tanque de combustible desde el cielo, que puede ser tomado por cualquiera de los jugadores o destruido a tiros, para que no la agarre nadie.

Vamos a hacer, como en anteriores ocasiones, una máquina de estados, la cual tendrá 5 ‘status’ (status0 – status4). Esta máquina utilizará las siguientes variables/posiciones de memoria (que definiremos convenientemente en vars.asm):

; direcciones de fuel, sprite 4
sprcolorfuel = $d02b
sprpointfuel = $07fc
sprxfuel = $d008          
spryfuel = $d009
FUEL      = $10       ; 16, constante sprite 4 (00010000) 
cantFUEL  = 10        ; cantidad de combustible de cada fuel 
statusFuel = $c0a0
fuelCounter = $c0a1

;255 mas grande el valor, mas tiempo tarda en aparecer
delayEntreFuels = 32    

Para que haya mas claridad en la explicación primero voy a hacer un “resumen” de la maquina de estados, y luego voy a desglosar cada parte.

@status0
    decrementa el contador 'fuelCounter', cuando llega a 0 pasa a status1

@status1
    inicializa el 'fuel' (obtiene posicion x aleatoria, posiciona en y, etc

@status2
    caida libre hasta que llega al piso

@status3
    espera en el piso, hasta que alguien lo agarre o lo destruyan

@status4
    apaga el sprite, reinicia contadores, cambia a status0

Status 0

Aqui solamente esperamos un cierto tiempo, para ello decrementamos un contador ‘fuelCounter‘, con la particularidad que lo haremos cada 64 frames (para ello usaremos un ‘generador de ticks’ que previamente usamos en otra parte). Cuando llegue a 0 pasamos a status1

@status0                
          ; si statusFuel == 0 sigue, si no pasa al siguiente
          ldx            statusFuel
          cpx            #0        
          bne            @status1  

          ; Si hay un tick64 entonces decremento en 1 el fuelCounter
          lda            tick64 
          cmp            #0        
          bne            @decrementFuelCounter
          jmp            @exitFuel 
          
@decrementFuelCounter          
          dec            fuelCounter
          ; Si el fuelCounter llego a 0 cambio a status1
          beq            @setStatus1
          jmp            @exitFuel 
          
@setStatus1
          ldx            #1        
          stx            statusFuel

Status 1

Cuando el fuelCounter llega  a 0 cambiamos de status,  e inicializamos el tanque de combustible. Para ello  activo el sprite 4, pongo en 0 la posición y, borro el bit8 (ya que puede haber quedado seteado de un tanque anterior), obtengo un numero aleatorio y le sumo 48, para centrarlo un poco. Esto es porque nuestro generador tira números de 0 a 255, pero nuestra pantalla tiene 320 px de ancho.
Para no complicar demasiado todo, y considerando que de 0 a 24  y de 320 en adelante el tanque no se mostraría en pantalla, decidí simplemente restringirlo a un área centrada de la pantalla, de  255 pixels, que abarca la posición entre 48 y 303.
Luego que inicializé todo paso a status2.

@status1                
          ldx            statusFuel
          cpx            #1        
          bne            @status2  

          ; Pongo posicion y en 0
          ldy            #0        
          sty            spryfuel  

          ; activo sprite 4
          lda            spractive 
          ora            #FUEL
          sta            spractive 
          
          ; posicion x fuel (random entre 48 y 303)
          ; borro el bit8 del sprxfuel (por si quedo seteado anteriormente)
          lda            sprxBit8   
          and            #%11101111
          sta            sprxBit8  
          
          ; obtengo un numero del generador random
          jsr            randomGenerator
          clc
          lda            random    

          ; lo centro un poco
          adc            #48       
          sta            sprxfuel  
          ; si tengo carry, entonces voy a tener que setear Bit8
          bcs            @setBit8  
          
          jmp            @setStatus2  
@setBit8                           
          lda            sprxBit8  
          ora            #%00010000
          sta            sprxBit8  

@setStatus2
          ; finalmente paso a status2
          ldx            #2
          stx            statusFuel

Status 2

Aqui comienza la caída libre, incrementando la posición y del sprite cada cuatro frames (usamos el mismo tick4 que previamente definimos para la gravedad de los jetpacks). Una vez que llega al piso pasamos a status3

@status2                
          ldx            statusFuel
          cpx            #2
          bne            @status3  

          ; incremento cada 4 frames
          lda            tick4
          cmp            #0        
          bne            @incrementFuel_y
          jmp            @exitFuel 

@incrementFuel_y
          inc            spryfuel  
          ldy            spryfuel  

          ; si llego al piso (un poquito mas, ya que es mas corto el sprite)
          cpy            #floorPosition + 3
          beq            @setStatus3
          jmp            @exitFuel 
          
@setStatus3          
          ldx            #3
          stx            statusFuel

Status 3

Este status no hace nada, ni me molesto en poner codigo, el sprite se queda en el piso hasta que, por detección de colisiones, lo agarre alguien o lo destruyan de un tiro.

Status 4

A este status se llega desde la detección de colisiones (que lo explicaré en la próxima entrega). Básicamente reinicializa el contador fuelCounter, apaga el sprite y pone el status en 0, para que comience nuevamente todo.

@status4                
          ;reinicializa statusFuel a 0
          ldx            #0        
          stx            statusFuel

          ; inicializa fuelCounter con el valor de delayEntreFuels
          ldx            #delayEntreFuels 
          stx            fuelCounter

          ; apago sprite 4
          lda            spractive 
          and            #255 - FUEL
          sta            spractive 

Y no me tengo que olvidar de inicializar algunas de las variables previamente definidas, en initVars.asm

          ldx            #0        
          stx            statusFuel
          
          ldx            #delayEntreFuels
          stx            fuelCounter

Y con esto ya estamos…
En la próxima entrega vamos a finalizar esta parte de las recargas, vamos a editar el detectCollision.asm para que detecte cuando un jugador agarra el combustible, o cuando lo destruyen.

Como siempre, el codigo completo lo pueden bajar desde el repositorio:
https://github.com/moonorongo/jp_wars.git
Hasta la próxima!

Recargas de combustible – parte 1

Vamos a incluir los tanques de combustible que nos permitirán recargar los jetpacks. Como todo esto tiene una complejidad extra voy a dividir la entrada en varias partes. La idea es que cada cierto tiempo caigan desde el cielo tanques de combustible (bien espaciados, cosa que sean escasos, y obligue a los jugadores a luchar por ellos).
Ademas, dichos tanques pueden ser destruidos con los disparos… esto tiene un doble filo, ya que permite destruir un tanque que el otro jugador esté a punto de tomar, o también puede ocurrir que lo destruyamos por disparar a lo loco.

Pero antes… un video de lo que quedó:

Los tanques caerán aleatoriamente desde diferentes posiciones X de la pantalla. Para esto necesitamos un generador de números aleatorios, en nuestro caso algo muy sencillo, que tire números de un byte de longitud, es decir, entre 0 y 255 valores diferentes. Si bien la pantalla de la C64 tiene 320 pixels, para simplificar un poco la cuestión vamos a hacer que los tanques caigan entre la posición 48 y la posición 303.
El código lo tome de codebase64.org, concretamente aquí. Es una pequeña subrutina, que llamamos en cada loop, y lo que hace es guardar un numero aleatorio de 1 byte de longitud en la posición de memoria random.

randomGenerator
          lda            random      ; cargo en A el contenido de random
          beq            @doEor      ; si es 0 hace un EOR con $1d, y lo guarda en random nuevamente
          asl                        ; si no, hago un shift left 
          beq            @noEor      ; si la op es 0, guarda en random
          bcc            @noEor      ; si hay carry, guarda en random
@doEor    eor            #$1d        ; y si llego aca, hace un eor 00011101 (para 'randomizarlo' un poco)
@noEor    sta            random      ; finalmente guarda el valor generado
          rts

El código es muy básico, y no demasiado aleatorio… genera los números aleatorios multiplicando por 2 (asl) y/o aplicando un eor con un numero (en este caso $1d). Viendo la rutina en acción veo que hay una tendencia a que genere números altos, los tanques tienden a caer del lado derecho de la pantalla, pero a los efectos que necesitamos en el juego esta… mas que bien.

Para que la rutina funcione correctamente es necesario inicializar random. Para ello vamos a utilizar una dirección de memoria de la C64, concretamente la posición de memoria $a2, que contiene el byte menos significativo de la variable TI del sistema, concretamente es un timer interno de la computadora, lo que nos dará una inicialización mas o menos aleatoria. El código de inicialización lo ponemos en initVars.asm:

          ldx            $a2         ; inicializamos el generador de numeros aleatorios
          stx            random      ; con un valor de la variable TI (que esta en $a2)

Como siempre, el codigo completo lo pueden bajar desde el repositorio:
https://github.com/moonorongo/jp_wars.git
En la próxima entrega vamos a implementar la maquina de estados para que caiga un tanque de combustible cada cierto intervalo de tiempo.
Hasta la proxima!

Sprites – Movimiento en X (bit 8)

En el post de hoy tenia pensado ir implementando un generador de números aleatorios, para que a intervalos de tiempo vayan cayendo tanques de combustible con los que recargar los Jets. Pero decidi introducir unos cambios gracias a que MI amigo Pablo Borobia se bajo el codigo e introdujo unos cambios para que los jugadores se muevan por toda la pantalla.

Originalmente no puse esa caracteristica porque implica un poco mas de programación, y conocimientos que al principio del tutorial no tenía. Hay que  chequear si ocurre un desbordamiento de carro y setear un bit extra, para posicionar el sprite en una parte de la pantalla u otra.

Un poco de teoría

La posicion X de los sprites se guardan en las direcciones $d000, $d002,  …. $d00e. Ahora bien, una posicion de memoria tiene una longitud de 8 bits, con lo que nos permite posicionar el sprite en las posiciones de 0 a 255 en la pantalla… y nuestra pantalla tiene 320 pixels de ancho (la parte central, no contemos los bordes, que se pueden abrir y mostrar los sprites alli… pero eso es una historia mas avanzada).

Para ello necesitamos utilizar el registro del VIC II que esta en la direccion $d010. Este tiene el bit 8 que nos falta de cada uno de los sprites, correspondiendo el bit 0 al bit 8 del sprite 0, bit 1 al sprite 1… bit 7 al sprite 7. Por ejemplo, la posicion X del sprite 0 nos queda determinada por la direccion $d000 y el bit 0 de la posicion $d010.

Y esto como lo trabajamos?

Buena pregunta, hay montones de forma, una que me parecio bastante correcta es la implementacion que a continuación voy a explicar (para el jugador 1, luego tenemos que ampliar todo al jugador 2, los disparos, etc.)


@chkLeft  lda            joy2      
          and            #4        ; left
          bne            @chkRight  
          
          jsr            checkFloorP1 ; solo se mueve si esta volando
          cpx            #1      
          beq            @chkRight  

          lda            #ptrJPLeft   ; pone sprite mirando a la izquierda
          sta            sprpoint  

          ; CODIGO ANTERIOR
          ; inc sprx

          ; NUEVO CODIGO
          sec                              ; seteo el carry, ya que voy a decrementar A
          lda            sprx              ; cargo en A posicion X sprite jugador 1
          sbc            #1                ; y resto 1
          bcc            @chkOvrL          ; si ocurre acarreo, llamo a checkOverflowP1
          sta            sprx              ; si no, estamos en la de antes, actualizo posicion X
          jmp            @chkRight         ; sigo checkeando el resto ...

@chkOvrL  jsr            checkOverflowP1   ; subrutina para controlar Bit 8 del jugador 1
          sta            sprx              ; actualizo posicion, con el bit 8 actualizado

Y mas adelante (al final de la maquina de estados):


; ------------- end of main animatePlayer1 ---------------------------                

; verifica posicion X, setea bit 8 spr 0
checkOverflowP1
          tax                           ; guardo A en X (lo necesito despues)
          lda            sprxBit8       l cargo en A $d010
          eor            #1             ; invierto el bit 1 (jugador)
                                        ; es simple: si ocurre un acarreo, es bit se invierte
                                        ; es todo lo que hay que hacer :) 
                                        ; a lo sumo me quedará inicializarlo en 0
                                        ; en initVars.asm
          sta            sprxBit8       
          txa                           ; restauro A, ya que al retornar tengo un STA... 
          rts

Y eso es todo, tenemos que repetir este codigo (con las variables/etiquetas que correspondan) para animatePlayer2, y ver como lo agregamos en la parte de codigo de los disparos… no demasiado complicado.

Y creo que eso es todo.

No: me faltó agregar la variable sprxBit8 = $d010, en vars.asm, si no este codigo no anda ni para atras…

Como siempre, el codigo completo lo pueden bajar desde el repositorio:
https://github.com/moonorongo/jp_wars.git
Ahora si,  en la próxima entrega vamos a implementar un generador simple de numeros pseudoaleatorios, el que utilizaremos para que aparezcan cayendo tanques de combustible.
Hasta la proxima!

Combustible de los JetPacs 1

En esta entrada vamos a implementar el indicador que figura debajo de HITS, concretamente el combustible de los jets. La idea detras de esto es agregar un poco de dificultad haciendo que los jets tengan una cantidad limitada de combustible, y que haya una posibilidad de recuperarlo con tanques que caerán aleatoriamente. Y para darle mas emoción, que se puedan destruir con los tiros (para, por ejemplo, evitar que el otro jugador agarre combustible).

Para comenzar, vamos a agregar una dirección de memoria que guarde la cantidad de combustible, para lo cual agregaremos la siguiente etiqueta en vars.asm:

    JP1Jet = $c003          ; cantidad de combustible del jet 1 

y luego la inicializamos en initVars.asm:

    ldx            #$ff       
    stx            JP1Jet    

También necesitaremos un ‘tick’ extra. Como hicimos previamente en otro post, vamos a agregar un poco de código para generar una señal, en este caso cada 64 loops, la que usaremos dentro de animatePlayer1.asm para decrementar el combustible cuando empujamos el joystick hacia arriba. La razón de usar un tick cada 64 loops es porque si no lo incluimos el combustible baja rapidamente, y consideré que cada 64 ciclos esta mas que bien (este código va en tickGenerator.asm).

; tick64 ------------------------------------
@tick64
          lda            internalCounter
          and            #64
          cmp            tick64        ; comparo con nuevo tick64
          bne            @setTick64_1   
          ldx            #0
          stx            tick64
          jmp            @skip     
@setTick64_1
          ldx            #1  
          stx            tick64

Ahora, con las variables definidas y el tick64 disponible, vamos a agregar el codigo para que decremente el combustible cada vez que ‘volamos’ (en animatePlayer1.asm):

@chkUp
          lda            joy2      
          cmp            #127      
          beq            @jmpNext  

          ldx            JP1Jet    ; check si hay combustible
          cpx            #$0       
          beq            @chkLeft  ; si se acaba... adiooos
          .
          .
          .
          beq            @chkLeft
          dec            spry      
          
          lda            tick64    
          cmp            #0        ; si no hay tick64 paso a revisar otra direccion
          beq            @chkLeft  
          dec            JP1Jet    ; y si no gasta combustible (c/64 frames)

@chkLeft  .
          .
          .
@jmpNext  
          rts

Y para actualizar en pantalla la cantidad de combustible tenemos que agregar 2 cositas mas: una llamada, al final de la maquina de estados en animatePlayer1.asm.

@exit          
          .
          .
          .
          jsr            updateJP1Jet
          rts
; ------------- end of main animatePlayer1 ---------------------------

y la subrutina ‘updateJP1Jet’ que se encargará de mostrar el valor en pantalla (la podemos agregar luego de la que actualiza los hits:

; actualiza el medidor de JET de JP1
updateJP1Jet
          lda            JP1Jet   
          jsr            convert2ascii
          sty            $0565
          stx            $0566
          sta            $0567
          rts

Y creo que eso es todo.
Como siempre, el codigo completo lo pueden bajar desde el repositorio:
https://github.com/moonorongo/jp_wars.git
La idea en la próxima entrega vamos a implementar un generador simple de numeros pseudoaleatorios, el que utilizaremos para que aparezcan cayendo tanques de combustible.
Hasta la proxima!

Animaciones – explosiones

En esta entada voy a explicar como agregar una animacion para cuando el jugador es impactado por un laser.
Inicialmente haciamos algo tan sencillo como incrementar o decrementar la posicion del jugador impactado por 3 pixeles, luego lo mejoramos con una caida ‘catastrofica’ cuando implementamos la maquina de estados, y ahora le pondremos una animacion.
Para que la animacion no pase tan rapido agregue un bloque de codigo que genera un ‘tick’ cada 4 frames, que lo usaremos para avanzar un frame de la animacion.

Primero, un vistazo y explicacion al codigo del ‘Tick Generator’ (seguramente, cuando mas adelante necesitemos otros intervalos los agregaremos aqui)


tickGenerator          
    lda     internalCounter   ; direccion contador interno programa
                              ; se incrementa en cada ciclo
    and     #4                ; si el bit 2 esta activado 
    sta     tick4             ; guardo tick4 anterior 
                              ; usando la direccion tick4 como temporal          
    inc     internalCounter

    lda     internalCounter   
    and     #4         
    cmp     tick4             ; comparo con nuevo tick4
                              ; si es distinto hay un cambio de estado
    bne     @setTick4_1       ; entonces genero un 'tick'          

    ldx     #0
    stx     tick4             ; si no pongo tick4 en 0

    jmp     @skip     
    
@setTick4_1
    ldx     #1  
    stx     tick4 
    
@skip
    rts

El funcionamiento del generador de ticks es muy sencillo, simplemente tenemos una direccion de memoria llamada internalCounter, que la incrementamos en cada loop, y que haciendo diferentes AND’s (1,2,4,8,16,32,64,128) podemos generar una suerte de mensajes cada 1, 2, 4… 128 frames (cada loop es un frame).

Con el generador de ticks podemos explicar el siguiente codigo, que corresponde al STATUS_3:


@statusJP_3                    ; --------------- STATUS 3 -----
    ldx    gravityCounter      ; Bloque controlador de gravedad  
    dex                        ; si no pongo esto
    stx    gravityCounter      ; la animacion pasa muy rapido
    cpx    #$0                 ; quiza a la brevedad 
    bne    @exit               ; lo meta en una subrutina
    ldx    #gravity
    stx    gravityCounter
                               ; fin bloque controlador gravedad
                               
    lda    tick4               ; me fijo si hay un tick4
    cmp    #0                  ; si no hay tick
    beq    @skipIncFallCounter ; sigo de largo
                               ; si hay tick4 (c/4 frames)
    inc    fallCounter         ; incremento fallCounter 
         
@skipIncFallCounter          
    lda    spry                ; en A cargo posicion Y de JP
    adc    fallCounter         ; sumo fallCounter 
    sta    spry                ; y actualizo

    ldx    fallCounter    ; tambien voy a usar fallCounter
    dex                   ; como contador de frames (0 a 4)
    cpx    #4             ; hasta que llegue a 4 
    beq    @explodeEnded  ; si es igual, que no asigne ningun frame mas

    txa                   ; transfiero x -> a
    adc    #ptrJPExplode  ; lo sumo a primer ptr explosion
    sta    sprpoint       ; lo asigno al ptr de JP
         
@explodeEnded          
                        ; controlo si decremento pos X de JP
                        ; segun para donde esta mirando el otro
    ldx    sprpoint2    ; (si recibe el tiro de frente o detras)
    cpx    #ptrJPRight  ; si esta mirando a la derecha
    bne    @decxspr2    ; va a decrementa sprx

    ldx    sprx       
    inx              ; si mira a la izq incrementa
    stx    sprx      
    jmp    @chkFloor  
         
@decxspr2
    ldx    sprx      
    dex              ; decrementa si mira a la derecha
    stx    sprx      


@chkFloor
    jsr    checkFloorP1  ; compara si llego al piso
    cpx    #1            
    bne    @exit         ; si no, sigue

    ldx    #0            ; si llego al piso 
    stx    fallCounter   ; inicializa fallCounter

    ldx    #0            ; cambia STATUS a 0 
    stx    statusJP1 

@exit                             
    rts

y finalmente, un video con el resultado final:

Como siempre, el codigo completo lo pueden bajar desde el repositorio:
https://github.com/moonorongo/jp_wars.git

Ademas: el compilado para descargar: link
Hasta la proxima!