Orange JS – Introducción

Otra librería JS para videojuegos… otra mas, y van…

A ver, la idea acá no es reinventar la rueda, ni plantar cara a los entornos para creación de videojuegos ya establecidos, ni nada que se le parezca.

Hoy día si uno quiere comenzar a PROGRAMAR videojuegos (digo programar, no usar herramientas como GameMaker) decia, si queres empezar a programar un videojuego puedes hacerlo a la antigua, leer un poco de documentación de , aprender a dibujar en él, y ver que te sale.

A través del desarrollo de esta pequeña, extra pequeña, librería estoy buscando construir una estructura mínima que permita el desarrollo de videojuegos 2D sencillos (tipo pacman, space invaders, etc) , que nos permita aprender conceptos básicos como el loop, OOP, eventos, colisiones, layers, sprites, etc.

Acá la idea no es aprender a usar un software para hacer videojuegos SINO a construir uno a partir de una librería sencilla, que cualquiera puede leer el código y entender que hace (menos yo que no me acuerdo que quise hacer)
Para construir un código más claro estoy usando webpack 4, para compilar ES6 a javascript.

Una descripción general

En este estado de desarrollo la librería son solo 5 archivos JS:

Orange: Es nuestro archivo principal, la class Orange, la cual inicializa la librería, establece los FPS, maneja los eventos, inicia el loop, y un par de cositas más, no mucho.

ImageManager: preloader y administrador de los assets que usaremos.

Layer: clase que nos permite agregar capas de imágenes (como las capas de photoshop) estas imágenes pueden ser un background, y los sprites que se agregaran luego en cada layer. También permite definir una imagen como delimitadora de posiciones (útil para definir, por ej, plataformas, laberintos, etc.

ImageMap: basicamente, un spritesheet, con métodos que nos permiten acceder a cada uno de los sprites que contiene.

Sprite: clase que recibe eventos, pinta la imagen correspondiente en el layer, controla el tamaño, posición, si puede o no dibujarse en una posición determinada, unas constantes y no mucho más

Un hola mundo en Orange

y podes verlo funcionando en http://moonorongo.webfactional.com/samples/hello/

Un ejemplo funcional de lo que hace

y podes jugar un rato en http://moonorongo.webfactional.com/samples/space_invaders/

La próxima comenzamos a explicar el código de la librería propiamente.

Raster estable – Técnica de la doble interrupción

En el tutorial de hoy veremos como hacer un raster estable que, si bien no es complicado, requiere tener un poco mas de conocimiento del VIC2 y de la duración de las instrucciones del 6502. Voy a poner 4 ejemplos de código, el primero de los cuales es una interrupción simple (en la que cambiamos el color como lo hicimos en anteriores posts), en el segundo ejemplo muestro ya una primera aproximación a la técnica, en la cual pongo con color blanco la parte sin estabilizar, y en color rojo la parte estabilizada.
En el tercer ejemplo muestro la corrección final, en la que se ve que el cáracter que parpadeaba ya no parpadea más, y finalmente en el cuarto ejemplo esta el código completo y comentado para que se entienda, con las explicaciones y la cantidad de clocks utilizados por cada instrucción.

Técnicas para estabilizar/sincronizar el raster hay varias, yo me decidí por esta debido a que es bastante común y en teoría simple. Digo en teoría porque estuve un par de semanas para entender los detalles a fondo, pero al final pude comprender bastante como funciona.

Cuando intentamos hacer un rasterbar nos encontramos con algo bastante molesto: por más que nosotros seteemos una interrupción y hagamos todos los deberes en el medio de la pantalla nos queda una vibración (a la que llamaremos ‘jitter’ a partir de ahora) bastante molesta, como la que pueden ver a continuación:

Este es el código correspondiente

BasicUpstart2(main)

.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0

.const LINE = 134 	
.const LINE2 = 160

main:
{
	sei                           
	lda #$7f		
	sta $dc0d
	sta $dd0d
	lda #$35
	sta $01	
	lda $d01a		
	ora #$01
	sta $d01a

	lda $d011		
	and #$7f
	sta $d011

	lda #LINE
	sta $d012 

	lda #intcode
	sta $ffff

	asl $d019	
	bit $dc0d   
	bit $dd0d	

	cli          

dummy_loop:	
	ldx #$00
	nop
	inx
	nop
	ldx $d020
	cpx #33
	nop
	ldy #$00
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	ldx $d020
	inx
	nop
	cpx #33
	nop
	ldy #$00
	ldx $d020
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	jmp dummy_loop
}

intcode:
{
	pha
	txa	
	pha
	tya 
	pha

	setColor(RASTERUNSTABLE)
	
	lda #intcode_restore
	sta $ffff

	ldx #LINE2
	stx $d012 

	asl $d019					  

	pla
	tay
	pla
	tax
	pla

	rti
}

intcode_restore: 
{
	pha
	txa	
	pha
	tya 
	pha

	setColor(NORMALCOLOR)

	lda #intcode
	sta $ffff

	ldx #LINE
	stx $d012 

	asl $d019					  

	pla
	tay
	pla
	tax
	pla

	rti
}


.macro setColor(color) {
	lda #color
	sta $d020
	sta $d021
}

¿Y esto por que esta ocurriendo? ¿No se supone que el VIC avisa al procesador cuando llegamos a esa linea?

Pues si… y no. El VIC2 avisa al procesador mediante una interrupción que el raster llegó a dicha linea… y sigue su tarea, que es seguir dibujando la pantalla (y allí es donde se va todo a la m…). En el camino el procesador recibió la interrupción, finalizó la instrucción que estaba ejecutando, guardo el estado en el stack, hizo el salto a nuestra rutina… para este punto el raster ya esta por la mitad de la pantalla, y encima, como la instrucción que estaba ejecutando puede tener una longitud entre 2 y 7 ciclos de clock, nos aparece ese condenado jitter.
El problema es que no podemos saber de antemano que instrucción estaba ejecutando el procesador. Si tuviesemos una forma de saber que se estaba ejecutando o MEJOR AUN podemos especificar una instrucción de antemano, entonces el jitter desaparece (la mayoría, después veremos que queda un chiquito por eliminar).
A alguien muy creativo se le ocurrió lo siguiente:

«Si pongo un punto de interrupción en una linea, inmediatamente cuando sucede la interrupción SETEO LA SIGUIENTE LINEA, y luego completo el resto de los ciclos de clocks que faltan para terminar la misma con una instrucción conocida (8 NOP seguidos), entonces cuando ocurra la próxima interrupción SABRE QUE SE ESTA EJECUTANDO UN NOP»

Efectivamente, como podemos ver en la siguiente imagen:

BasicUpstart2(main)

.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0

.const LINE = 132 	
.const LINE2 = 160

main:
{
	sei                           

	lda #$7f		
	sta $dc0d
	sta $dd0d

	lda #$35
	sta $01	

	lda $d01a		
	ora #$01
	sta $d01a

	lda $d011		
	and #$7f
	sta $d011

	lda #LINE
	sta $d012 

	lda #intcode
	sta $ffff

	asl $d019	
	bit $dc0d   
	bit $dd0d	

	cli          

dummy_loop:	
	ldx #$00
	nop
	inx
	nop
	ldx $d020
	cpx #33
	nop
	ldy #$00
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	ldx $d020
	inx
	nop
	cpx #33
	nop
	ldy #$00
	ldx $d020
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	jmp dummy_loop
}

intcode:
{
	pha
	tya	
	pha
	txa 
	pha


	lda #intcode_stable
	sta $ffff

	setColor(RASTERUNSTABLE)

	inc $d012 
	asl $d019					  

    tsx
    cli

    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
}


intcode_stable:
{
	txs
	waitLine(8)

	noEstabilizaJitter()

	setColor(0)
	nop
	nop
	nop
	
	setColor(RASTERCOLOR1) 

	lda #intcode_restore
	sta $ffff

	ldx #LINE2
	stx $d012 

	asl $d019					  

	pla                           
	tax                           
	pla                           
	tay                           
	pla                           

	rti
}

intcode_restore: 
{
	pha
	tya	
	pha
	txa 
	pha

	setColor(NORMALCOLOR)

	lda #intcode
	sta $ffff

	ldx #LINE
	stx $d012 

	asl $d019					  

	pla                           
	tax                           
	pla                           
	tay                           
	pla                           

	rti
}


.macro waitLine(cant_loops) {
    ldx #cant_loops	
    dex      		
    bne *-1  		
    bit $00  		
}


.macro estabilizaJitter() {
	lda $d012 
	cmp $d012 
	beq *+2   
}


.macro noEstabilizaJitter() {
	nop 
	nop 
	nop
	bit $00 
	nop 
}

.macro setColor(color) {
	lda #color
	sta $d020
	sta $d021
}

Peeero… está CASI estable. Si bien el jitter disminuye considerablemente, todavía queda una linea de un cáracter que esta parpadeando. Esto ocurre porque, si bien nuestra interrupción ocurrió durante una instrucción NOP, esta puede haber estado ejecutándose o haber finalizado, lo cual nos da esa diferencia de un clock.

Para ello tenemos que introducir una pausa (puede ser un bucle) MUY precisa, para que nos lleve CASI al final de la linea (una linea completa lleva 63 ciclos de procesador, en una c64 pal, y en cada ciclo el VIC2 dibuja una linea de un cáracter de ancho). Allí cargamos en A el valor de $d012, inmediatamente lo comparamos con el valor de $d012 (que loco no?) y si es igual saltamos… a la siguiente linea de código.
Esto que estoy explicando parece un sinsentido total, es mas, les muestro el código para que vean:

lda $d012 // 4 clocks
cmp $d012 // 5 clocks
beq siguiente // 2 clocks si distinto o 3 clocks si igual
// si es igual va a siguiente y si no tambien… pfff
siguiente:
// el resto
// del codigo
// aqui…

O sea… si es distinto va a siguiente y si es igual tambien… CUAL ES LA LOGICA?
A lo mejor ustedes ya lo entendieron, esta fue la parte que mas me costó comprender. En primer lugar, este bloque de código tiene sentido cuando el raster ESTA CERCA DE FINALIZAR LA LINEA. El truco pasa por comparar el valor de $D012 (la linea de raster) con el valor que toma unos pocos clocks después. Si es el mismo valor quiere decir que la interrupción ocurrió justo cuando finalizó la ejecución del NOP, entonces SALTA a la siguiente instrucción, lo cual le insume 3 ciclos de clock.
Ahora bien, si el valor da diferente es porque hubo un clock de más (producido porque se estaba ejecutando todavía el NOP). El salto no se ejecuta,, lo cual le insume 2 ciclos de clock… a la siguiente instrucción (a la misma que hubiera hecho el salto) ¿Ven la magia? si hay un clock de mas suma 2 clocks, si no hay clock suma 3… y se nos estabiliza el raster.

BasicUpstart2(main)

.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0

.const LINE = 52
.const LINE2 = 132

main:
{
	sei                           

	lda #$7f		
	sta $dc0d
	sta $dd0d

	lda #$35
	sta $01	

	lda $d01a
	ora #$01
	sta $d01a

	lda $d011
	and #$7f
	sta $d011

	lda #LINE
	sta $d012 

	lda #intcode
	sta $ffff

	asl $d019
	bit $dc0d
	bit $dd0d

	cli          

dummy_loop:	
	ldx #$00
	nop
	inx
	nop
	ldx $d020
	cpx #33
	nop
	ldy #$00
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	ldx $d020
	inx
	nop
	cpx #33
	nop
	ldy #$00
	ldx $d020
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	jmp dummy_loop
}

intcode:
{
	pha
	tya	
	pha
	txa 
	pha


	lda #intcode_stable
	sta $ffff

	setColor(RASTERUNSTABLE)

	inc $d012 
	asl $d019					  

    tsx
    cli

    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
}


intcode_stable:
{
	txs

	waitLine(8)

	estabilizaJitter()

	setColor(0)
	nop
	nop
	nop

	setColor(RASTERCOLOR1) 

	lda #intcode_restore
	sta $ffff

	ldx #LINE2
	stx $d012 

	asl $d019					  

	pla                           
	tax                           
	pla                           
	tay                           
	pla                           

	rti
}

intcode_restore: 
{
	pha
	tya	
	pha
	txa 
	pha

	setColor(NORMALCOLOR)
	dec $d020

	lda #intcode
	sta $ffff

	ldx #LINE
	stx $d012 

	asl $d019					  

	pla                           
	tax                           
	pla                           
	tay                           
	pla                           

	rti
}


.macro waitLine(cant_loops) {
    ldx #cant_loops	
    dex      		
    bne *-1  		
    bit $00  		
}


.macro estabilizaJitter() {
	lda $d012 
	cmp $d012 
	beq *+2   
}


.macro noEstabilizaJitter() {
	nop 
	nop 
	nop
	bit $00 
	nop 
}

.macro setColor(color) {
	lda #color
	sta $d020
	sta $d021
}

En estos ejemplos previos puse una linea blanca, que indica el punto de la primera interrupción y la parte sin estabilizar, luego en negro para mostrar el resto de jitter, y finalmente en rojo la parte estabilizada. Pueden ver que la linea final de la rasterbar sigue con jitter, esto es porque no me tomé el trabajo de estabilizarla.
El código final está a continuación, en el mismo estan cada uno de los pasos a realizar comentados y ademas los ciclos de clock que lleva cada instrucción o macro utilizada.

BasicUpstart2(main)

.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0

// no podemos poner LINE = 131, 139, 147, 155... porque son badlines y rompen
// hay que recalcular waitLine() para las bad lines
.const LINE =  132 	
.const LINE2 = 160

main:
{
	sei                           
					
	// disable CIA
	lda #$7f		
	sta $dc0d
	sta $dd0d

	// Bank out kernal and basic
	// ponemos disponibles 
	// $A000-$BFFF (BASIC) 
	// y $E000-$FFFF (KERNAL)
	lda #$35
	sta $01	
/*
	Basicamente: apagamos las roms del basic y el kernel 
	(realmente, nos interesa apagar el kernel porque 
		necesitamos escribir un valor alli en $fffe y $ffff 
		y el sistema pueda leer lo que escribimos) 
	mas info leer: https://dustlayer.com/c64-architecture/2013/4/13/ram-under-rom
*/
	// aca: todo lo de antes... 
	lda $d01a		// enable VIC IRQ
	ora #$01
	sta $d01a

	lda $d011		// clear MSB raster
	and #$7f
	sta $d011

	lda #LINE
	sta $d012 

	lda #intcode
	sta $ffff

	// esto lo hacemos para que no haga 
	// cosas raras cuando arranca... 
	asl $d019	// Ack any previous raster interrupt
	bit $dc0d   // reading the interrupt control registers 
	bit $dd0d	// clears them

	cli          


// este dummy loop es para que la interrupcion ocurra 
// en cualquier instruccion, asi tenemos un caso lo mas 
// real posible... en los ejemplos que encontre 
// normalmente hacian un jmp *, lo cual no era real, ya que 
// la interrupcion ocurria en una instruccion que conocemos (JMP)
dummy_loop:	
	ldx #$00
	nop
	inx
	nop
	ldx $d020
	cpx #33
	nop
	ldy #$00
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	ldx $d020
	inx
	nop
	cpx #33
	nop
	ldy #$00
	ldx $d020
	iny
	bit $00
	nop 
	ldx $d020
	nop
	ldx #$00
	nop
	inx
	ldx $d020
	nop
	cpx #33
	nop
	ldy #$00
	iny
	ldx $d020
	bit $00
	nop 
	ldx $d020
	nop
	jmp dummy_loop
}

intcode:
{
	// GUARDAMOS REGISTROS *1
	/* 
	   En los ejemplos que vi lo hace mejor
	   escribiendo codigo automodificable
	   el cual ocupa menos clocks del procesador
	   pero a fines didacticos esto es mas claro
	*/
	pha
	tya	
	pha
	txa 
	pha


	lda #intcode_stable
	sta $ffff

	// pongo la interrupcion en la proxima linea
	inc $d012 
	asl $d019					  

    // Almacena el actual puntero del stack 
    // porque no queremos volver aca cuando se produzca el RTI
    // sino que queremos que vaya al dummy_loop 
    // (o la parte de nuestro codigo)
    tsx
    cli

    // en algun punto en los siguientes nop's 
    // se ejecutara la siguiente interrupcion
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
}


intcode_stable:
{
// en este punto, se ejecuto la interrupcion en un comando NOP,
// por lo tanto, ya tenemos 1 o 2 clocks corridos a lo sumo
// (dependiendo si estaba ejecutando la instruccion cuando llamó 
//  la interrupcion o si la había finalizado)

	// nop 1 o 2 clocks
	// salto interrupcion 7 clocks
	// 8 o 9 clocks
	
	// Restaura el puntero del stack al punto de retorno 
	// donde se llamo por primera vez (y que guardamos en X)
	// nos interesa que esté en el punto donde guardamos (*1)
	// si no el RTI va a volver acá, y no es la idea
	txs
	// 10 u 11 clocks

	// espero que pase CASI toda la linea
	// el calculo: 
	// 	2 + (7 * (2 + 3)) + 2  + 3 = 42
	waitLine(8) // (43 + (10 u 11)) = 52/53)

	// corrige el jitter de 1 clock del nop
	// para q esto funcione tenemos q estar casi al final
	// de la linea: cargamos en A el valor de $d012
	// luego lo comparamos
	// si es igual hay un ciclo de menos, entonces salta (3 clocks)
	// si es distinto pasa, hay un ciclo de mas, no salta (2 clocks)
	// y con eso se estabiliza 

	estabilizaJitter()

	setColor(RASTERCOLOR1) 

	// restauro interrupcion a 2da linea
	lda #intcode_restore
	sta $ffff

	ldx #LINE2
	stx $d012 

	asl $d019					  

	pla                           
	tax                           
	pla                           
	tay                           
	pla                           

	rti
}

intcode_restore: 
{
	pha
	tya	
	pha
	txa 
	pha
	// en este punto esta sin estabilizar
	// necesitamos hacer nuevamente todo lo que hicimos
	// anteriormente... 
	setColor(NORMALCOLOR)

	// restauro interrupcion
	lda #intcode
	sta $ffff

	ldx #LINE
	stx $d012 

	asl $d019					  

	pla                           
	tax                           
	pla                           
	tay                           
	pla                           

	rti
}


.macro waitLine(cant_loops) {
    ldx #cant_loops	//	2  
    dex      		//  2
    bne *-1  		//	3
    bit $00  		//  3
}


.macro estabilizaJitter() {
	lda $d012 // 4 		(56/57)
	cmp $d012 // 5 		(62/63)
	beq *+2   // 2 distinto o 3 igual
}


.macro noEstabilizaJitter() {
	nop 
	nop 
	nop
	bit $00 
	nop 
	// 11 clocks
}

.macro setColor(color) {
	lda #color	//	2
	sta $d020	//	4
	sta $d021   //	4
}

Para el final me he dejado los detalles tediosos del VIC.

Este código funciona en tanto y cuando no usemos las lineas 51, 59, 67 … y asi de 8 en 8, las cuales se conocen como «bad lines». ¿Que esta pasando? Bueno, en dichas lineas (que coinciden con la primera linea de una fila de caracteres) el VIC2 interrumpe durante 40 CICLOS al procesador para obtener los códigos de caracteres de una linea desde la matriz de vídeo, haciendo que se nos de-sincronice el raster y se vaya todo al demonio. Lamentablemente no encontré todavía información para evitar esto, en cuanto encuentre algo actualizo el artículo.

Por último, dejo una lista de enlaces para profundizar mas en el tema:

Double IRQ explained: http://codebase64.org/doku.php?id=base:double_irq_explained

STABLE RASTER ROUTINE: http://codebase64.org/doku.php?id=base:stable_raster_routine

VIC-II FOR BEGINNERS PART 3 – BEYOND THE SCREEN: RASTERS AND CYCLES https://dustlayer.com/vic-ii/2013/4/25/vic-ii-for-beginners-beyond-the-screen-rasters-cycle

The MOS 6567/6569 video controller (VIC-II) http://www.zimmers.net/cbmpics/cbm/c64/vic-ii.txt

Y esto es todo por hoy. Pueden descargar el código desde Github, visitar mi página en Facebook y comentar cualquier duda que tengan.
Hasta la próxima!

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!

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!