Interrupciones 2 – scroll de pantalla

Continuando con el tema, hoy vamos a explicar como hacer un scroll de una linea de la pantalla utilizando interrupciones y registros del VIC II. Primeramente vamos a hacer y explicar un ejemplo sencillo, con un scroll de una linea, caracter por caracter. Luego le agregaremos la parte de código para que se mueva de forma suave, y finalmente haremos que el scroll solo afecte a una linea de pantalla. Pero primero…

Un poco de teoría

El VIC II tiene 2 registros para realizar desplazamientos suaves de pantalla, en la dirección $D016 para el movimiento horizontal, y en $D011 para el vertical. Para nuestros ejemplos vamos a utilizar solamente el de movimiento horizontal, que según lo que dice en http://sta.c64.org/cbm64mem.html funciona de la siguiente manera:

Screen control register #2. Bits:

Bits #0-#2: Horizontal raster scroll.
Bit #3: Screen width; 0 = 38 columns; 1 = 40 columns.
Bit #4: 1 = Multicolor mode on.

Default: $C8, %11001000.

Los bits que nos interesan son el 0, 1, 2 para el movimiento fino, y el 3 para activar el modo de 38 columnas (el 4 bit lo dejamos en 0, ya que no nos interesa el modo multicolor, por ahora). Por lo que pude ver es un registro de 4 bits, no encontré para que pueden servir los bits superiores, y probando tampoco vi que hicieran algo, ya que por si bien por defecto están en $c8, si pokeamos y lo seteamos en $10 no veremos ningún cambio.

Los bits de scroll pueden tomar solo valores de 0 a 7… ¿¿¿Como haremos un scroll que mueva mas pixeles que eso???

En realidad no necesitaremos mas, ya que iremos moviendo finamente TODA la pantalla con $d016, y cuando llegamos al valor 7 RÁPIDAMENTE avanzaremos un caracter toda la linea, y ponemos el valor del registro a 0… y es todo lo que tendremos que hacer para obtener un scroll fino…
bueno, no… tendremos que hacer algunas cosas mas.

El registro de scroll fino mueve TODA LA PANTALLA, y si nosotros hacemos lo anteriormente dicho veremos que toda la pantalla va a los saltos, salvo la linea que estamos moviendo, por lo que vamos a tener que setear una interrupción que haga todo lo dicho para el scroll, y otra unas lineas mas abajo que restaure la pantalla (una imagen para aclarar mejor todo):

Ejemplo 1: Scroll por caracter con interrupciones

Para comenzar vamos a hacer una simple rutina para mover una linea, caracter por caracter. Ademas, vamos a hacer que el origen del texto lo obtenga de la primera linea de la pantalla, así podremos jugar con los textos mientras se desplazan.


BasicUpstart2(main)
.const scrollLine = $0400+22*40

* = $1000 "Main Program"

main:
sei // deshabilito interrupciones

lda #$7f // apago las interrupciones
sta $dc0d // de la CIA

lda $d01a // activo la irq
ora #$01 // por raster
sta $d01a

lda $d011 // borro el MSB de raster
and #$7f
sta $d011

lda #100 // especifico una linea de raster
sta $d012 // LSB (aca puede ser cualquiera aca)

lda #intcode // de nuestra rutina
sta 789
cli // habilito las interrupciones
rts // retorno (en este caso BASIC)

intcode:
jsr scrollChar // voy a mi rutina de scroll
inc $d019 // notifico interrupcion

lda #100 // Restauro el punto de interrupción
sta $d012 // para el proximo refresco

jmp $ea31 // salto a las rutinas del sistema

scrollChar:
{
// rutina que scrollea un caracter a la izquierda
ldx #0
loopScroll:
lda scrollLine+1, x
sta scrollLine, x
inx
cpx #39
bne loopScroll

// obtengo un nuevo caracter
textIndex: // index de la cadena q esta en $0400
ldx #0 // #0 se va a ir modificando
lda $0400 , x
sta scrollLine + 39

inx
cpx #39 // si llego al ultimo caracter
beq resetIndex // de la primera linea, reseteo el index
stx textIndex + 1 // si no incremento
rts

resetIndex:
ldx #0
stx textIndex + 1
rts
} // end scroll

Hasta la subrutina ‘scrollChar’ el programa es virtualmente idéntico al del post anterior, en el que ciclábamos los colores. Aquí en vez de rotar el color de borde vamos a llamar a una subrutina, que se llamara 60 veces por segundo, y en cada llamada hace un scroll de un caracter hacia la izquierda. La rutina funciona de la siguiente manera:

  • en un loop de 0 a 39, va copiando los caracteres, de n+1 a n (bloque que comienza en la etiqueta ‘loopScroll:’ y finaliza en ‘bne loopScroll’)
  • Toma el siguiente caracter (a partir de $0400) y lo almacena en la ultima columna
    Aquí me gustaría mostrar algo: el índice se va guardando en textIndex + 1, que corresponde al operando de la instrucción LDX, que iremos incrementando en cada llamada a la interrupción, y si llega a la columna 39 lo reseteamos a 0. Me pareció interesante y óptima esta forma de llevar el indice (en vez de utilizar una etiqueta en otra dirección de memoria, cargarla en un registro, incrementar el registro, guardar el registro actualizado en la posición de memoria… bufff). Esta forma de hacerlo se la ‘robé’ a Mike Rivera (https://www.facebook.com/thc64), quien me pidió ayuda con unas rutinas de scroll, y me gusto esta forma de llevar un índice de una cadena de texto.
  • cuando llega a la última posición de la primera linea de la pantalla resetea el índice (pone el operando de LDX en 0)

A continuación, un ejemplo funcionando (pueden descargar el código fuente y el compilado desde Github https://github.com/moonorongo/c64_tutorial_samples)

Ejemplo 2: scroll fino… con detalles

Aquí vamos a agregar una pequeña subrutina que, en cada llamada a la interrupción, desplace la pantalla 1 pixel, y cuando llega a la posición 7 (recordar que el registro solo tiene posiciones de 0 a 7) avanza un caracter y pone el registro de scroll en 0:


intcode:
jsr scrollPixel               // voy a mi rutina de scroll
inc $d019                     // notifico interrupcion

lda #0 // Restauro el punto de interrupción
sta $d012 // para el proximo refresco

jmp $ea31 // salto a las rutinas del sistema

scrollPixel:
{
ldx #7 // 38 columnas, scroll h, index scroll fino
cpx #255
beq resetScrollFino

stx $d016
dec scrollPixel + 1
rts

resetScrollFino:
ldx #7
stx scrollPixel + 1
stx $d016
jsr scrollChar
rts
}

La subrutina scrollPixel: va haciendo un ciclo de 7 a 0 (porque estamos desplazando hacia la izquierda – por eso comparamos con #255), cuando sucede eso es que seteamos $d016 en #7. A continuación, una muestra de como queda (código completo en Github https://github.com/moonorongo/c64_tutorial_samples):

UGGHH!!! Todo muy bien con el scroll, pero nos quedo todo el resto de la pantalla a los saltos… Esto es porque, como dijimos antes, tenemos que restaurar el registro de scroll para el resto de la pantalla fuera de la linea que queremos mover, lo cual haremos en el siguiente ejemplo.

Ejemplo 3 – Scroll final

Como hicimos en el capítulo anterior de interrupciones, vamos a tener un ‘modeflag’, que según su estado vamos a atender una parte u otra de la pantalla.
en ‘intcode:’ vamos a obtener el estado de ‘modeflag’ y segun el mismo vamos a ‘lineaScroll’ o ‘restauroPantalla’. En ‘lineaScroll:’ hacemos lo mismo que en el ejemplo anterior (yo acá agregue un cambio de color de fondo, para ver a partir de que linea sucede la interrupción), y fijamos el siguiente punto de interrupción.
En ‘restauroPantalla:’ pongo el color de pantalla normal, restauro el puntero de scroll a 0, establezco la próxima linea de raster y hago los pla,txa… correspondientes para retornar de la interrupción.


intcode:
    lda modeflag
    beq lineaScroll
    jmp restauroPantalla

lineaScroll:                      // viene si modeflag es 0
{

    lda #$01                      // invertimos el modeflag
    sta modeflag                  // para que la proxima vez vaya a 
                                  // la otra parte del codigo
    lda #COLOUR1                  // ponemos color 
    sta $d020
    sta $d021

    jsr scrollPixel               // voy a mi rutina de scroll

    lda #LINE1                    // seteamos nuevamente la
    sta $d012                     // linea de interrupcion
    inc $d019                     // acusamos recibo de interrupcion

    jmp $ea31                     // salto a las rutinas del sistema
}

restauroPantalla:
{
    lda #$00                      // invertimos el modeflag
    sta modeflag

    lda #COLOUR2                  // ponemos el color
    sta $d020
    sta $d021

    ldx #0                        // dejamos el scroll fijo
    stx $d016   

    lda #LINE2                    // seteamos linea de raster
    sta $d012                     

    inc $d019                     // acusamos recibo
                 
                                  // PEEERO: 
    pla                           // Aqui salimos completamente
    tay                           // esta es la forma de salir de la
    pla                           // interrupción, restaurando
    tax                           // los registros.
    pla                           // lo explico con mas detalle a continuación
    rti
}

A continuación, el resultado final:

Y esto es todo por hoy…

Los 3 ejemplos están subidos y compilados en Github (https://github.com/moonorongo/c64_tutorial_samples), cualquier duda o consulta la pueden hacer en mi Facebook https://www.facebook.com/mscifu

Hasta la próxima!

Anuncios

Interrupciones por Raster – Básico

Hola! Luego de un tiempo prudencial, volvemos con los tutoriales. La idea a partir de ahora es tomar diferentes aspectos de la programación de nuestra amada C64, para que cada uno pueda aprovecharlo para lo que desee, sea desarrollo de demos, juegos, música, gráficos, etc.

Quería iniciar con un tutorial de interrupciones de raster pero, leyendo diversas fuentes, me gustó mucho uno publicado en http://www.antimon.org/dl/c64/code/raster.txt (que lleva unos cuantos añitos) el cual cuenta con un enfoque básico y claro, así que opté por realizar una traducción casi directa, con agregados míos.

Rasters – Que son y como usarlos.

por Bruce Vrieling – (traducción realizada por Mauro Cifuentes)

Cualquiera que haya jugado con las interrupciones en la C64 escucho en algún momento mencionar el concepto de ‘interrupciones por raster’. Pero… ¿Que es un raster? ¿y como podemos utilizar dichas interrupciones?

Una descripción general

Una interrupción, como su nombre lo indica, es una llamada que se le hace al micro, y lo que sucede es que:

. el micro termina de ejecutar la instrucción que estaba haciendo
. guarda todo el estado (registros, PC…)
. SALTA a la dirección asignada donde esta el código que se ejecuta para dicha interrupción
. ejecuta dicho código
. restaura los registros
. retorna al punto siguiente a la instrucción que interrumpió
. y continua la ejecución del programa como si nada hubiese pasado.

Ejemplo de interrupción son las generadas por la CIA para realizar ciertas tareas, como escanear el teclado, hacer titilar el cursor, actualizar el reloj, etc.

Y a todo esto ¿Como cambiamos el curso normal de acción de la interrupción? ¿Como podemos utilizarlo para nuestros fines? Es mas que sencillo.

La C64 tiene un PUNTERO(*1) en las posiciones de memoria 788/9 ($314/5), el cual apunta a la dirección donde esta el código del Kernel que se ejecuta 60 veces por segundo. Si cambiamos este puntero a NUESTRO CÓDIGO, y al finalizar el mismo saltamos a la posición donde esta el código del Kernal (que es $EA31) y tienes el cuidado de no pisar nada, tendrás tu código ejecutándose 60 veces por segundo.

(*1) PUNTERO: posiciones de memoria que guardan la posición de memoria a la que apuntan. En el texto original dice VECTOR, que es lo mismo.

Ejemplo:


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

/*
    Interrupts - LO BASICO
    Este programa cicla los colores del borde, 60 veces 
    por segundo
*/
            * = $1000 "Main Program"
main:
	sei                        // Deshabilita interrupciones

	lda #<intcode              // Seteo en 788/9 
	sta 788                       // el byte bajo y el alto
	lda #>intcode              // de nuestra rutina
	sta 789

	cli                           // habilito las interrupciones
	rts                           // retorno (en este caso BASIC)

// nuestra super rutina	
intcode:
	inc $d020                     // cicla el color de borde
	jmp $ea31                // salto a las rutinas del sistema

Si compilas esto con Kickassembler (si, lo siento…) podrás ver que el borde cicla los colores 60 veces por segundo (en pal: 50).
Fíjense que en el bloque ‘main’, este comienza con SEI y finaliza con CLI, antes del RTS que vuelve al BASIC. No queremos que ocurra una interrupción mientras estamos cambiando el puntero de interrupción, así que las deshabilitamos con SEI  y las restauramos con CLI.
Piensen: podríamos estar modificando 788, pero ocurre una interrupción antes de modificar 789, con lo cual el puntero puede quedar apuntando a cualquier parte.

El ejemplo que mostré es muy simple. Hay muchas cosas útiles que se pueden hacer con interrupciones, como tocar música mientras tipeamos un programa en BASIC. GEOS también usa interrupciones para controlar el mouse y disparar eventos. Las interrupciones son una herramienta poderosa y el concepto concerniente a las del raster específicamente es particularmente útil para muchos casos.

El Raster
¿y al final… que es?

Cuando nos referimos a raster estamos hablando de una interrupción que se dispara cuando el rayo del monitor dibuja una linea especifica en la pantalla de video. Hay diferentes fuentes que pueden producir interrupciones, no estamos limitados a las que el chip CIA puede producir. El Raster depende de interrupciones generadas por el VIC2. Podemos hacer que el color del borde cambie a una altura determinada de la pantalla, por ejemplo: cuando la linea de pantalla que especificamos comienza a re-dibujarse ocurre la interrupción, entonces tu código rápidamente debería cambiar el color del borde.
También podemos poner media pantalla en alta resolución, y el resto en texto, o cambiar el set de caracteres, etc…

Algunos hechos de como se re-dibuja la pantalla de video: Esta se actualiza 60 veces por segundo (50 en las PAL). Contiene 200 lineas en el display de 25×40, numeradas de 50 a 250 (noten que hay mas lineas visibles, por ejemplo el borde superior y el borde inferior). El re-dibujado de la pantalla se sincroniza con la frecuencia de la red eléctrica (60 hz en USA, 50 hz en Europa). Por eso hay diferencia de velocidades en juegos, ejemplo el Giana Sisters va mas rápido en NTSC que en PAL, solamente porque el refresco ocurre mas veces por segundo, y el juego se sincroniza con esa frecuencia de refresco.

Pero entonces… ¿las maquinas NTSC van mas rápido que las europeas?
NO… solamente el procesador es interrumpido mas veces por segundo, lo que realmente ocurre es que son ligeramente mas lentas 😦

¿Por que debemos preocuparnos por las interrupciones de video? Si la pantalla se refresca 60 veces por segundo y las interrupciones estandar ocurren 60 veces por segundo, ¿por que directamente no ponemos algo de código dentro del código de la interrupción estandar que haga lo que queremos con la pantalla?
Porque estas 2 interrupciones no están sincronizadas, ninguna de ellas ocurren exactamente 60 veces por segundo, y las diferencias son mas que suficientes para que sea imposible coordinar una actividad de cualquier tipo en la pantalla. Cuando utilizamos una interrupción de video sabemos exactamente a que linea de raster de la pantalla ocurre.

Entonces, hagamos un resumen

Sabemos que las interrupciones regulares ocurren 60 veces por segundo, y que podemos hacer que se genere una interrupción a una determinada linea de la pantalla (la cual se actualiza 60 veces por segundo). Un pequeño inconveniente es que ambas interrupciones (estandar y raster) las atiende el mismo puntero (por lo que tenemos 120 interrupciones por segundo, 60 por las estandar, otras 60 por el video). Nuestro código tendrá que checkear que fuente de interrupción es y actuar en consecuencia.

el sistema necesita una interrupción que ocurra 60 veces por segundo para que haga la gestión interna, y utiliza el reloj interno de la CIA para generarla. También vamos a querer que cada vez que a cierta linea en la pantalla se dispare una interrupción (lo cual ocurrirá 60 veces por segundo). La interrupción estandar deberá enviarse al código en Rom correspondiente, mientras nuestra interrupción de video deberá ir a nuestro código.

Si ambas ocurren a 60 veces por segundo, ¿Por que no ejecutar el código de la Rom del sistema y el nuestro en la MISMA interrupción? Ya sabemos que la CIA no es buena para esto, ya que está fuera de sincronismo con el video. ¿Por que no apagar la interrupción de CIA, habilitar la interrupción de raster, y hacer ambos trabajos en una interrupción? Entonces tendríamos una señal de interrupción que ocurre 60 veces por segundo, y esta en perfecto sincronismo con el video.

Eso es exactamente lo que vamos a hacer

Alguno se habrá percatado de un ligero fallo en la lógica anteriormente expuesta. Con el fin de simplificar no mencione que necesitaras DOS interrupciones por pantalla para realizar algo útil. ¿Por que dos? porque cualquier cambio que realices a una determinada posición de la pantalla, deberás revertir cuando llegues al tope de la misma. Ejemplo, si quisiéramos que 3/4 de la pantalla fuese en alta resolución, y el cuarto restante texto, entonces necesitaremos una interrupción en 3/4 de la pantalla para cambiar a texto, y otra en el tope (scanline 50) para restaurar hires. También podríamos querer ‘trocear’ la pantalla en N modos de video, por lo cual tendremos N + 1 interrupciones.

Bueno, basta de charla, hagamos un pequeño ejemplo para mostrar lo que hablamos. Vamos a hacer lo mismo que en el primer ejemplo, con la diferencia que en vez de utilizar una interrupción de CIA usaremos la del raster, con lo cual no veras aquí grandes diferencias (visibles) con el primer ejemplo.


BasicUpstart2(main)
/*
    Interrupts - LO BASICO
    Este programa cicla los colores del borde, 60 veces 
    por segundo. En este ejemplo la fuente de interrupción
    es el VIC2
*/

	* = $1000 "Main Program"

main:
	sei                           // deshabilito interrupciones

	lda #$7f                      // apago las interrupciones
	sta $dc0d		      // de la CIA

	lda $d01a                     // activo la irq
	ora #$01		      // por raster
	sta $d01a

	lda $d011                     // borro el MSB de raster
	and #$7f					  
	sta $d011

	lda #100                  // especifico una linea de raster
	sta $d012                 // LSB (puede ser cualquiera aca)

	lda #<intcode              // Seteo en 788/9 
	sta 788                       // el byte bajo y el alto
	lda #>intcode              // de nuestra rutina
	sta 789

	cli                           // habilito las interrupciones
	rts                           // retorno (en este caso BASIC)


intcode:
	inc $d020                     // ciclo el color del borde

	lda #$ff              // (*2) ver explicación a continuación
	sta $d019					  

	//inc $d019		      // (*3) forma optimizada

	lda #100             // Restauro el punto de interrupción
	sta $d012            // para el proximo refresco 

	jmp $ea31            // salto a las rutinas del sistema

El código de ejemplo esta bastante claro en sus comentarios, si se entendió el anterior no deberías tener problemas con este. El único problema que encontré y que tuve que googlear un poco para entenderlo (y lo vi bien explicado en http://codebase64.org/doku.php?id=base:introduction_to_raster_irqs) fue en la parte en que escribimos nuestro código dentro de la interrupción, y que marqué con un (*2).

¿Para que hacemos esto? $d019 es el registro de estado de interrupciones del VIC-II. Cuando ocurre una interrupción si leemos el registro nos dice de que tipo es, y si escribimos el registro le acusamos recibo que atendimos dicha interrupción. Si no escribimos en este registro (concrétamente el bit 0) la condición de interrupción estará presente todo el tiempo, y el código se ejecutará permanentemente independiente de la linea del raster. Pueden probar comentándolo y verán que queda un patrón de colores muy lindo, pero no vuelve jamás al BASIC.

La linea siguiente que dejé comentada en (*3) hace a efectos prácticos lo mismo, solamente que emplea menos ciclos de reloj, por lo cual es una forma más utilizada. En el siguiente ejemplo se nota la optimización de utilizar una u otra.

Vamos con un ejemplo de código mas complejo, para ya ir finalizando el tutorial. Aquí vamos a dividir el borde en 2 por la mitad, poniendo en blanco la parte superior, y en negro la inferior.


BasicUpstart2(main)
/*
    Interrupts - LO BASICO
    Este programa divide el borde en 2 colores
*/

	* = $1000 "Main Program"

.const COLOUR1 = 0
.const COLOUR2 = 1
.const LINE1 = 20
.const LINE2 = 150

main:

	sei                           // Todo esto es lo mismo que 
				      // en ejemplos anteriores

	lda #$7f
	sta $dc0d

	lda $d01a
	ora #$01
	sta $d01a

	lda $d011
	and #$7f
	sta $d011

	lda #LINE1
	sta $d012 

	lda #<intcode
	sta 788      
	lda #>intcode
	sta 789

	cli          
	rts          

intcode:

	lda modeflag                  // vemos si estamos en la parte
	                              // superior o inferior
	beq mode1 		      // de la pantalla
	jmp mode2

mode1:

	lda #$01                  // invertimos el modeflag
	sta modeflag		  // para que la proxima vez vaya a 
				  // la otra parte del codigo
	lda #COLOUR1                  // ponemos color 
	sta $d020

	lda #LINE1                    // seteamos nuevamente la
	sta $d012                     // linea de interrupción

	inc $d019		 // acusamos recibo de interrupción

	jmp $ea31                     // esta parte va a las rutinas 
				      // del kernel en ROM

mode2:
	lda #$00                      // invertimos el modeflag
	sta modeflag

	lda #COLOUR2                  // ponemos el color
	sta $d020

	lda #LINE2                    // seteamos linea de raster
	sta $d012                     


	// lda $#ff		      // interesante en este ejemplo
	// sta $d019                  // la diferencia de usar 
				      // uno u otro metodo.

	inc $d019		      // acusamos recibo


				  // PEEERO: 
	pla                       // Aqui salimos completamente
	tay                       // esta es la forma de salir de la
	pla                       // interrupción, restaurando
	tax                       // los registros.
	pla           // lo explico con mas detalle a continuación
	rti

modeflag: .byte 0

Este ejemplo es similar a los anteriores, con la única diferencia que utilizamos un flag para atender la parte superior o inferior de la pantalla. En la parte inferior de la pantalla (mode1) vamos a hacer lo mismo que antes, seteando el color negro, poniendo la siguiente linea de interrupción en 20 y finalizando la interrupción saltando a $ea31.
Pero en el bloque mode2 vamos a atender la parte superior de la pantalla. Hacemos lo mismo, seteamos el color en blanco, seteamos la linea en 150, pero en vez de saltar a las rutinas del sistema finalizamos la interrupción con el bloque pla, tay…. pla, rti.
¿Por que hacemos esto? porque al poner 2 puntos de interrupción vamos a tener 120 llamadas a nuestro código, y las rutinas del sistema solo debemos llamarlas 60 veces, así que en el bloque en negro llamamos a las rutinas, pero en el bloque en blanco salimos de la interrupción…
¿Y por que todo ese código? Bueno, al principio del artículo habíamos dicho que cuando ocurre una interrupción el micro guarda el estado, básicamente los registros y el Program Counter. Con este bloque de código lo que estamos haciendo es restaurar los registros A, X e Y al estado en que estaban al momento en que ocurrió la interrupción.

Un detalle que ya comenté: cuando acusamos el recibo de la interrupción puse comentado la forma lda-sta. Si prueban una forma u otra van a ver que varia mucho la estabilidad del raster.

Actualización: Como la plataforma WordPress me hizo alguna que otra perrada, decidí subir los archivos de ejemplo a Github en esta dirección: https://github.com/moonorongo/c64_tutorial_samples

Y esto es todo por hoy… con esta base ya podemos encarar la próxima entrega, en la cual voy a explicar como hacer un raster ‘estable’. Nos vemos!

Efectos de sonido – parte 2

Con la teoría aprendida en el post anterior vamos a encarar el tema de los efectos sonoros. Vamos a realizar 2 sonidos: el disparo del jugador y la explosión al recibir el mismo.
Comenzaremos explicando el efecto de disparo, al ser de mayor complejidad de implementar. En primer lugar vamos a agregar las constantes de los registros del SID que vamos a utilizar (en vars.asm):

.const sidPtr    = 54272
.const sid_vol = 54296
.const sid_hfreq1 = 54273
.const sid_ad1 = 54277
.const sid_sr1 = 54278
.const sid_wave1 = 54276

Y luego vamos a inicializar los registros del SID en initvars.asm:

          
          ldy #0
          lda #0
loop:                     // reseteamos los registros
          sta sidPtr,y    
          iny 
          cpy #25
          bne loop

          // configuramos canal 1
          ldx #16          // volumen maximo 
          stx sid_vol
          ldx #0           // attack y decay en 0
          stx sid_ad1
          ldx #100
          stx sid_hfreq1   
          ldx #15*16+5     // volumen de sustain 15
          stx sid_sr1      // relax 5

Disparos

Para el sonido del disparo vamos a hacer un sonido que comenzará en una frecuencia alta, y en cada frame va disminuyendo la frecuencia hasta llegar a 0 (el típico sonido de disparos de naves). Para ello vamos a agregar un nuevo archivo que llamaremos sound_fire1.asm y lo importaremos desde main.asm inmediatamente después de importar animatePlayer1.asm.

Por que es importante esto? Bueno, porque vamos a utilizar la variable fire1 (que se pone en 1 cuando disparamos) y si colocamos el import luego de procesar el disparo nos vamos a encontrar que fire1 siempre esta en 0, ya que la parte que procesa el disparo lo pone en 0…

El código es muy simple: primero chequeamos que se haya producido un disparo, si es así ponemos el MSB de la frecuencia en 100, y comenzamos el sonido. Luego en cada frame vamos a ver si la frecuencia es distinta de 0, si es así decrementamos en 1, y cuando llega a 0 lo apagamos. El código, a continuación:

ldx fire1           
    cpx #1              // si se pulso disparo
    beq turn_on_fire1   // lo enciendo
    jmp dec_freq_1      // o sigo de largo
    
turn_on_fire1:    
    ldx #100             // inicializo la frecuencia
    stx sound_fire1_freq 
    ldx #17              // inicio el sonido
    stx sid_wave1        // con onda triangular
    
dec_freq_1:     
    ldx sound_fire1_freq 
    cpx #0               // comparo frec por 0
    beq turn_off_fire1   // si es igual lo apago
    
    stx sid_hfreq1       // y si no 
    dec sound_fire1_freq 
    dec sound_fire1_freq // lo decremento
    jmp exit
    
turn_off_fire1:    
    ldx #16
    stx sid_wave1
    
exit:

Explosiones

Este sonido es mucho mas simple, ya que solamente implica encenderlo y apagarlo. Vamos a usar el canal 3 del SID, con la siguiente configuración (en initvars.asm):

          // configuramos canal 3
          ldx #0
          stx sid_ad3    // attack/decay rapido
          ldx #3        
          stx sid_hfreq3 // frecuencia muy grave
          ldx #15*16+9   // volumen sustain max
          stx sid_sr3    // relax medio (9)

Poner relajación en 9 hace que cuando prendemos y apagamos el sonido va desvaneciéndose lentamente (mas alto el número, mas duración del sonido).
Luego, para simplificar y embellecer el código vamos a escribir una macro, a la que le pasaremos la frecuencia. De esta manera podremos utilizar diferentes sonidos de explosiones (ejemplo, si quisieramos una explosión diferente para cuando reventamos un tanque de combustible).

.macro playNoise(freq) {
    ldx #freq
    stx sid_hfreq3
    ldx #129
    stx sid_wave3
    ldx #128
    stx sid_wave3
}

Luego utilizaremos la macro donde detectamos que se produce una colisión (en detectCollision.asm) simplemente llamandola con playNoise(2)

Y esto es todo por hoy… y por el curso. Desde que inicié el tutorial a principio del año (con el efecto ciclado de colores) recorrimos un largo camino. Personalmente fue muy gratificante escribir este curso ya que me permitió aprender mucho mas de la C64/6502, además de mejorar como programador, redactor y educador.

A continuación, les dejo índice del curso. Además el enlace al proyecto en Github, mi página en Facebook, y mi Linkedin.

Índice del Curso

Proyecto en Github

Página personal en Facebook

Linked in

Más adelante tengo planeado hacer pequeños tutoriales acerca de programación de efectos, demos, gráficos y lo que se me ocurra sobre esta maravillosa maquina. Además quiero escribir un pequeño curso sobre como programar un motor gráfico para videojuegos en Javascript.

Nos vemos mas adelante!

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!