Migración a KickAssembler

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

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

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

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

Main.asm

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

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

/*
    JetPac Wars! Main File
*/

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

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

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

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

Macros, labels, scoping y constantes

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

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

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

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

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

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

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

Instalación y compilación

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

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

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

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

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

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

Funciones desde assembler

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

El modo de direccionamiento (Indirect),y

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

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

Ajahm… y esto de que me sirve???

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

El código!


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

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

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

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

Y como usamos esta “función”?

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

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

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

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

Recargas de combustible – parte 3 (final)

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

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

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

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

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

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

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

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

          
@checkJP2Fuel                   
          ; idem codigo que checkJP1Fuel
          .
          .
          .


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

@checkFireFuel2                  
          ; idem codigo checkFireFuel1
          .
          .
          .

@skipSprDetect
          rts

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

Hasta la próxima!

Recargas de combustible – parte 2

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

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

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

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

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

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

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

@status2
    caida libre hasta que llega al piso

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

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

Status 0

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

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

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

Status 1

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

@status1                
          ldx            statusFuel
          cpx            #1        
          bne            @status2  

          ; Pongo posicion y en 0
          ldy            #0        
          sty            spryfuel  

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

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

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

Status 2

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

@status2                
          ldx            statusFuel
          cpx            #2
          bne            @status3  

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

@incrementFuel_y
          inc            spryfuel  
          ldy            spryfuel  

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

Status 3

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

Status 4

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

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

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

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

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

          ldx            #0        
          stx            statusFuel
          
          ldx            #delayEntreFuels
          stx            fuelCounter

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

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

Recargas de combustible – parte 1

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

Pero antes… un video de lo que quedó:

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

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

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

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

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

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

Sprites – Movimiento en X (bit 8)

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

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

Un poco de teoría

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

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

Y esto como lo trabajamos?

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


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

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

          ; CODIGO ANTERIOR
          ; inc sprx

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

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

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


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

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

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

Y creo que eso es todo.

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

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

Combustible de los JetPacs 1

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

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

    JP1Jet = $c003          ; cantidad de combustible del jet 1 

y luego la inicializamos en initVars.asm:

    ldx            #$ff       
    stx            JP1Jet    

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

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

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

@chkUp
          lda            joy2      
          cmp            #127      
          beq            @jmpNext  

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

@chkLeft  .
          .
          .
@jmpNext  
          rts

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

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

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

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

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

Animaciones – explosiones

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

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


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

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

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

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

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

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


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

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

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

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


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

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

    ldx    #0            ; cambia STATUS a 0 
    stx    statusJP1 

@exit                             
    rts

y finalmente, un video con el resultado final:

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

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

Maquinas de estados

En la entrada de hoy voy a explicar el concepto de ‘maquina de estado’ (no se si se llama asi, pero asi lo aprendi yo). Basicamente  es separar el codigo en diferentes ‘bloques’ que se ejecutaran segun un numero de estado. Para el caso que nos compete, que son los jugadores, vamos a diferenciar el estado de los mismos en 4:

  • STATUS 0: Inicializacion del jugador
  • STATUS 1: Animacion de entrada: una pequeña animacion, durante la cual el jugador es inmune a los disparos (si no otro jugador lo suficientemente habilidoso directamente no lo dejaria jugar)
  • STATUS 2: El codigo de deteccion de movimientos, etc (todo lo que estaba hasta ahora)
  • STATUS 3: Animacion de muerte del jugador

Entonces, si yo tengo una variable statusJP, en base al valor de la misma voy a ejecutar un codigo u otro. Esto en C seria algo asi:


  switch(statusJP1) {
    case 0 : // inicializacion de todas las vars de jugador 1
        statusJP = 1;
        break;
    case 1 : // animacion entrada
             if (condicion_terminacion_animacion_entrada) {
               statusJP = 2;
             }
             break;
    case 2 : // estado normal de juego
             .
             .
             .
             .
             break;
    case 3 : // muerto
             // animacion muerte 
             // y lo que necesitemos
             if (termino_animacion_muerte) {
               statusJP = 0;
             }
             break;
  }
  

En assembler es un un poquito mas complicado, ya que no disponemos de la instruccion switch. Aqui muestro el codigo del player 2 (lo limpie un poco, para que no quede kilometrico – el codigo original esta en el repositorio).


animatePlayer2
                                   ; -------------- STATUS 0 -------
          ldx            statusJP2 
          cpx            #0
          bne            @statusJP2_1
                                   ; INIT SPRITE JP2
          ldx            #233      ; posicionamos jetpac 2
          stx            sprx2
          ldx            #0        ; que salga desde arriba de la pantalla...
          stx            spry2     
          lda            #ptrJPLeft      
          sta            sprpoint2  ; jet pac 2 mirando a la izquierda
                        
          ldx            #1        
          stx            statusJP2 
          
@statusJP2_1                       ; -------------- STATUS 1 -----------------
                                   
          ldx            statusJP2 
          cpx            #1        
          bne            @statusJP2_2
          
          inc            spry2      
          ldy            spry2      
          cpy            #50       
          beq            @set_statusJP2_2
          
          jmp            @exit     
          
@set_statusJP2_2
          ldx            #2
          stx            statusJP2
          
          
@statusJP2_2                       ; -------------- STATUS 2 -----------------------
                                   ; todo el codigo que estaba antes va aqui...
          
          
@statusJP2_3                       ; --------------- STATUS 3 -----------------
             
          ldx            gravityCounter2
          dex
          stx            gravityCounter2
          cpx            #$0       
          bne            @exit
          ldx            #gravity
          stx            gravityCounter2

          inc            fallCounter2
          lda            spry2
          adc            fallCounter2
          sta            spry2
          
          ldx            sprpoint
          cpx            #ptrJPRight  ; si esta mirando a la derecha
          bne            @decxspr2    ; decrementa sprx
         
          lda            sprx2
          adc            fallCounter2  ; si mira a la izq incrementa
          sta            sprx2      
          jmp            @chkFloor  
          
@decxspr2 
          lda            sprx2     
          sbc            fallCounter2
          sta            sprx2      
          
          
@chkFloor
          jsr            checkFloorP2
          cpx            #1        
          bne            @exit
          
          ldx            #0      
          stx            fallCounter2

          ldx            #0        
          stx            statusJP2
          

@exit                             
          rts
; ------------- end of main animatePlayer2 ---------------------------                

Para muestra, aqui tengo un videos, observen que cuando recibe un tiro sale disparado hacia atras, a diferencia de antes, que se corria 3 mugrosos pixeles. Mi idea ademas es hacer una pequeña animacion con una explosion, que lo voy a dejar para el proximo post.

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

Colisiones

Hoy vamos a explicar como se detectan las colisiones utilizando el VIC II. Estuve un rato para entender como era, pero finalmente lo pude hacer.
Basicamente hay 2 formas de detectar colisiones: una es viendo si los sprites que nos interesan estan colisionando (haciendo un polling), y la otra es utilizando interrupciones. Como la segunda opcion queda fuera de mi conocimiento (de momento) voy a explicar la primera.

El código (simplificado)

detectCollision
        ; detecto que ocurrio una colision de sprites
          lda            $d019    ; Direccion que indica en bit 2 que ocurrio una colision
          and            #$04     ; MASK bit 2
          cmp            #$04     ; testeo si bit 2 esta encendido
          bne            @skipSprDetect ; si NO sigo de largo

        ; SI OCURRIO UNA COLISION
          lda            $d01e    ; Direccion que indica cuales colisionaron 
          and            #JP2 + FJP1 ; mascara con los que quiero testear (2 + 4 = 6)
          cmp            #JP2 + FJP1 ; testeo si estan encendidos los 2
          bne            @skipSprDetect  ; si no, que siga de largo

        ; SI LLEGA ACA = COLISIONARON DISPARO jugador 1  CONTRA jugador 2
          ; aca hago todo lo que quiera: incremento puntaje, 
          ; resto energia... 
          ; animo el sprite impactado... 

          jmp            @skipSprDetect

@skipSprDetect
          rts

Vamos a trabajar con 2 registros: $d019, que indica que ocurrio una colision sprite-sprite, y $d01e, que indica cuales colisionaron.
Aca el tema es que, cuando quise fijarme directamente en la direccion $d01e (registro de colision de sprites-sprites), no funcionaba. Eso ocurria porque ese registro se llena cuando ocurre una colision, y eso hay que detectarlo con el registro $d019, concretamente el bit 2. Si tenemos colision, entonces RECIEN podemos ver cuales colisionaron.
Un detalle que lei en la documentacion es que el registro se limpia cuando se lee… o sea, que si queremos chequear varios sprites tenemos que cargar el valor del registro y guardalo de alguna forma (ya sea en memoria, en el stack… donde se nos ocurra).

El codigo simplificado esta comentado, se entiende muy facil, NO ES EL CODIGO final que subi al repositorio, ya que ese codigo hace varias cositas mas:

  • detecta colisiones fire1 y spr2
  • fire2 y spr1
  • registra el impacto en un contador
  • muestra la cantidad de hits de ambos players
  • hace una pequeña animacion…

Pero como tengo idea de implementar maquinas de estado en ambos players (mas adelante explico que es), decidi explicar mas teoricamente esto de las colisiones, y luego de hacer las maquinas de estado poner todo mas completo.

A partir de ahora no voy a poner tanto codigo ya que ademas de codigo nuevo voy corrigiendo el codigo que escribi previamente, o hago alguna modificacion de ultimo momento, asi que prefiero explicar lo que hice, tratando de entenderlo uds y yo, y pueden ir bajando desde Github lo que tengo realizado hasta el momento:
https://github.com/moonorongo/jp_wars.git