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!

Anuncios

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

Animacion Sprites Player 2

La entrada de hoy es corta, ya que a pesar que vamos a agregar un segundo jugador, lo unico que vamos a hacer es duplicar las variables, duplicar un par de archivos, agregar unas llamadas en el main.asm y… voila!!! tenemos el segundo jugador funcionando!!! magia… no, en realidad, son las ventajas de separar el codigo en archivos. A continuacion, un video de lo que me quedo:

Para lograr ello, lo primero que tenemos que hacer es duplicar las etiquetas que utilizamos para el jugador 1. por ejemplo:

agregados en vars.asm

; direcciones jetpac 2 -------------------------------------------------------
sprcolor2  = $d029
sprpoint2  = $07fa
sprx2      = $d004
spry2      = $d005
fire2     = $2a                   ; zero page direccion sin usar
gravityCounter2 = $04

; direcciones disparo
sprcolorFire2  = $d02a
sprpointFire2  = $07fb
sprxFire2      = $d006
spryFire2  = $d007
; dirFire2 esta en la direccion de memoria que me sobra del jet pac right

Tambien tenemos que inicializar estas variables, para ello agregamos en el archivo macros.asm (en la macro initVars) lo siguiente:

          ldx            #150      ; posicionamos jetpac 2
          stx            sprx2
          ldx            #139       
          stx            spry2

          ldx            #196      ; posicionamos disparo 2
          stx            sprxFire2
          ldx            #139       
          stx            spryFire2

          lda            #$05      
          sta            spractive ;activamos el sprite 0 y 2

          lda            #$03
          sta            sprcolor2  ; jetpac 2 color cyan

          lda            #$0a
          sta            sprcolorFire2  ; fire color red

          lda            #$0a      
          sta            sprxpandX ; expandimos X el disparo 1 y 2.

          lda            #ptrJPLeft
          sta            sprpoint2  ; jet pac 1 mirando a la izquierda

          lda            #ptrJPFire      
          sta            sprpointFire2  ; punteros de disparo

          lda            #$0       ; fire flag 
          sta            fire2     

          lda            #gravity
          sta            gravityCounter2

y tenemos que duplicar animatePlayer2.asm y animatePlayer2Fire.asm, vamos a Project properties y en el build order los ubicamos a continuacion de los del Player 1.
Ademas tenemos que modificar todas las referencias a las variables del player 1, a las nuevas variables.

Un agregado mas

Por ultimo, un detalle que me viene rompiendo las bolas desde que comence a programar… siempre que cargamos un prg en la c64 tenemos que ejecutarlo con SYS 4096… para ahorrarnos ese paso agregamos una llamada sys, que la generamos con una herramienta de CBM PRG Studio.
Agregamos un nuevo archivo asm, que lo nombraremos como syscall.asm. Luego vamos a Tools -> Generate SYS() Call, se nos abrira un popup, en Enter start addres ponemos $0801, hacemos click en ok, y nos generará algo como esto:

; 10 SYS4096

*=$0801

        BYTE    $0B, $08, $0A, $00, $9E, $34, $30, $39, $36, $00, $00, $00

Nota: El popup nos da 2 opciones para seleccionar, basicamente si queremos que la linea aparezca de una forma u otra… pero no se como sabe el programa que la direccion es esa (4096)… supongo que la saca del main.asm… pero creo que en ningun lado estoy poniendo que ese es el archivo principal, o lo que sea… en fin…

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

Animacion Sprites

Retomamos el codigo para esta vez comenzar a animar nuestro personaje… Hoy empiezo mostrando todo lo que hice en un video, y despues vamos con el codigo y las complicaciones, de paso vemos un poco de teoria que tuve que aprender.

Como podemos ver, ahora nuestro personaje hace bastante mas cosas. En principio el grafico ‘gira’ para el lado que se mueve, lo que obligó a modificar la rutina de disparo para que, segun para donde este mirando nuestro personaje, salga el laser.
Ademas hay gravedad, o algo parecido… ahora si pulsamos para arriba nos ‘impulsamos’, y si soltamos la gravedad nos tira para abajo hasta tocar el piso.
Como nuestro personaje no puede caminar… bah, no tengo ganas de hacer tooooda la animacion de las patitas… lo resolvi muuuy simple: si estas tocando el piso NO TE PODES MOVER.
La idea detras de esto es la siguiente:

  • Solo te podes mover si volas con el jetpac
  • Solo podes volar si tenes combustible
  • Si te quedas sin combustible quedas anclado en el piso (y seras masacrado horriblemente por tu competidor)
  • Podes recargar mas combustible con celdas de combustible que cae del cielo (y podes dispararle a las celdas para evitar que tu competidor agarre combustible)
  • Hablando de disparos: cada tiro que disparas te quita una unidad de energia (para no ponerte a disparar como loco)
  • Cada tiro que te ponen te resta 30 unidades de energia
  • Cada vez que volas perdes energia (no tanto como si te pusieran un tiro, pero perdes)… todavia a definir

O sea: perdes energia por cualquier cosa, y tenes que pelear por recuperarla.
De todo eso solo tengo implementado el primer punto, los restantes los ire implementando mas adelante.

Ahora ¡El codigo!

Lo primero que explicaré es como hice la animacion del jetpac (que mire para donde se está moviendo). Es super sencillo, abrí el archivo sprites.asm y definí un grafico identico al que habia realizado previamente, pero lo puse en espejo. Lo puse un label con nombre jetpacLeft, a continuacion de la definicion del disparo. Codigo a continuación:

jetpacLeft
          bits           ..........BBBB..........
          bits           ............BBB.........
          bits           ........BBBB.BB.B.......
          bits           ........BBBB.BB.B.......
          bits           ........BBBB.BB.B.......
          bits           .........BBBBB..BB......    
          bits           ................BB......
          bits           ..........BBBB.B.B......
          bits           .......B.BB..B.BBB......
          bits           .......B.BB..B.B.B......
          bits           ....BBBBB....B.BBB......
          bits           .........BBBB.BB.B......
          bits           ..........BBB.BBBB......
          bits           ......BBBBBB.BBBB.......
          bits           ......BBBBBB..B.B.......
          bits           ......BB.....B.B.B......
          bits           .............BBBBB......
          bits           .....BBB................
          bits           ....BBBB.....B.BB.......
          bits           .............B..B.......
          bits           ...............B..B.....
          byte           0

Luego agregue unas variables en vars.asm (en realidad las uso como constantes, pero bue… todo lo que definimos aca son punteros o constantes):

; constantes
gravity   = $05   ; usado para setear la 'fuerza' de la gravedad, 1 = super fuerte, mayor = mas debil
floorPosition = 223
topPosition = 52

ptrJPLeft = $23
ptrJPRight = $21
ptrJPFire = $22
; dirFire esta en la direccion de memoria que me sobra del sprite de disparo... mhmm          

ademas de una de nombre dirFire, que es el ultimo byte del sprite ptrJPRight, asi lo aprovechamos para algo (puse ese comentario con las demas variables porque me paso que no me acordaba donde la habia definido… si… es medio una cochinada, pero solo tengo 65535 espacios libres de memoria… y ni siquiera eso).
Las variables ptrJPLeft y ptrJPRight guardan el puntero de la ubicacion de los sprites. Podria haber utilizado los numeros, pero el codigo queda mucho mas claro asi. Estas vars/const (llamenlas como quieran) las utilizo en animatePlayer1.asm. A continuacion un codigo de ejemplo (no es el codigo exactamente, borre un pedacito para clarificar):

chkLeft   lda            joy2      
          and            #4         ; checkeo si es left
          bne            chkRight   ; si no voy a checkear right
                                    ; y si es left, sigo por aca
          lda            #ptrJPLeft ; cargo en a el valor de ptrJPLeft ($23)     
          sta            sprpoint   ; y lo establezco en el puntero del jetpac
          dec            sprx       ; y todo lo demas... .

Lo mismo para right, o sea, en cada cambio derecha/izquierda lo unico que hago es cambiar un numerito en el puntero del sprite del jetpac1. De la misma forma, si quisieramos hacer que camine, o que expulse fuego por el jetpac cuando vuela lo que deberemos hacer es armar un contador, que se incremente cada n frames, que segun el numero que establezca el puntero del frame correspondiente… es laborioso, pero no imposible.

Un detalle que tuve que cambiar fue el tema de los disparos. Antes siempre miraba hacia la derecha, por lo que la rutina de disparos era tan elemental como que cuando detectaba el boton de disparo procedia a:

  • tomar la posicion x e y del jugador, establecia la posicion x e y del disparo, y pasaba el status a disparando.
  • mientras estaba disparando en cada frame avanzaba 3 pixeles, y chequeaba que no haya llegado al final de la pantalla (si x es MAYOR o IGUAL que una determinada posicion – esto tiene profundas implicancias mas adelante).
  • si llegaba al final de la pantalla procedia a apagar el sprite de disparo, y pasaba a estado esperando.

Ahora tengo que tener en cuenta ademas para adonde esta mirando, e inicializar una variable dirFire para mas adelante, cuando este en status disparando, decremente o incremente la posicion del disparo.

Anteriormente dije que chequeaba si el disparo habia finalizado cuando llegaba al final de la pantalla, o sea a una posicion x MAYOR o IGUAL a un punto arbitrario. En assembler no es tan trivial como en cualquier otro lenguaje hacer una comparacion por mayor o igual (hay que hacer una operacion, y ver si ademas tenemos acarreo). Eso se hace cargando en A la posicion x del sprite, luego hacemos un ADC, que es una operacion para sumar, pero con acarreo, ponemos el resultado de la suma en la posicion x del sprite, y con BCS (Branch on Carry Set) saltamos a status0 si se produjo un acarreo (o sea la suma excedio 255). Codigo de ejemplo donde hago eso:

        lda            sprxFire  
        clc             ; seteamos el flag de acarreo en 0
        adc            #$02 ; cantidad de pixels que aumenta
        sta            sprxFire  
        bcs            @setStatus0        

Diferente es cuando en vez de sumar tenemos que restar. Hacemos exactamente lo mismo, pero con unas salvedades:

  • en vez de ADC usamos SBC, que a diferencia de ADC, BORRA el flag de acarreo cuando se excede el valor de una resta (o sea, da menos que cero). Eso nos lleva a …
  • en vez de CLC usamos SEC, que ESTABLECE el flag de acarreo, para finalmente chequear con
  • BCC (Branch on Carry Clear), lo mismo que el otro, pero cuando borra el Carry.

esta boludez me tuvo una semana, todo por no leer la puta documentacion. Quedais avisaos…

Otro detalle que agregue fue la gravedad y los limites superiores e inferiores. Para la gravedad agregue dos constantes gravity, que es el valor inicial del contador, y gravityCounter, que es el contador de gravedad. Al final de cada frame decremento gravityCounter y me fijo llego a cero. Si no llego a cero decremento el contador en 1, y si es igual a cero decremento la posicion Y del sprite e inicializo el gravityCounter con el valor de gravity.

next
          ldx            gravityCounter
          dex
          stx            gravityCounter
          cpx            #$0       
          bne            @exit

          jsr            checkFloorP1

          cpx            #1      ; check floor (si result checkFloorP1 es 1 es el piso)
          beq            @skipGravity

          inc            spry      ; gravity :P 

@skipGravity
          ldx            #gravity
          stx            gravityCounter

Pueden ver ademas que hago una llamada a una subrutina de nombre checkFloorP1. Esta rutina checkea que si el jugador llego al piso, y establece el registro x con 1 o 0, comparamos x con 1 o 0 (true o false, seria) y procedemos… a continuacion la rutina para checkear si llego al piso (y de paso la rutina para chequear el techo, de regalo).

; verifica posicion Y de P1, y retorna 1 en X si es el piso 
checkFloorP1
          ldx            spry      
          cpx            #floorPosition
          beq            @returnTrue    ; is equal
          bcs            @returnTrue    ; or greater ;) 
          ldx            #0        
          rts
@returnTrue
          ldx            #1        
          rts

; verifica posicion Y de P1, y retorna 1 en X si es el top
checkTopP1
          ldx            spry      
          cpx            #topPosition
          beq            @returnTrue    ; is equal
          ;bcs            @returnTrue    ; or greater ;) 
          ldx            #0        
          rts
@returnTrue
          ldx            #1        
          rts

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

Exportacion del fondo del juego

Previamente diseñamos la pantalla de fondo de nuestro juego, ahora vamos a incorporarla. Para ello necesitaremos exportar la imagen como .asm, y luego una pequeña rutina que copie estos datos al area de memoria de la pantalla. El post de hoy sera corto, ya que es realmente sencillo lo que hay que hacer.

Exportación de datos

Primero, carguemos el fondo que diseñamos en el tuto anterior en el ‘Screen editor’, luego vamos a File, y seleccionamos Export Assembler, tras lo que aparecera la siguiente pantalla:

dejamos las opciones como estan, ya que queremos la pantalla, asi como tambien los colores, y clickeamos en ok. Se abrirá una ventana con los datos exportados, los seleccionamos y copiamos, y los pegamos en un nuevo .asm que crearemos en nuestro proyecto. Para el fondo que yo hice me quedo algo asi:

screen.asm

*         = $2000
screen  BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$0A,$05,$14,$10,$01,$03,$20
        BYTE    $20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$17,$01,$12,$13,$21,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$51,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$74,$0A,$10,$20,$31,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$08,$09,$14,$20,$20,$31,$30
        BYTE    $20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$06,$15,$05,$0C,$20,$39,$39
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$0A,$10,$20,$32,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$08,$09,$14,$20,$20,$31,$35
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$74,$06,$15,$05,$0C,$20,$32,$35
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$E9,$DF,$20,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$2E,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$E9,$A0,$A0,$DF,$20,$20,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$E9,$A0,$A0,$A0,$A0,$DF,$20,$20,$20,$20,$20,$2E,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$E9,$A0,$A0,$A0,$A0,$A0,$A0,$DF,$20,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$20,$2E,$20,$20,$20,$20,$20,$20,$E9,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$DF,$20,$20,$20,$20,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$68,$E9,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$DF,$68,$68,$68,$74,$02,$19,$20,$20,$20,$20,$20
        BYTE    $A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$74,$20,$20,$20,$20,$20,$20,$20
        BYTE    $A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$74,$0D,$13,$03,$09,$06,$15,$20
        BYTE    $A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$A0,$74,$20,$20,$20,$20,$20,$20,$20

screen_colour.asm

*         = $2400
colour  BYTE    $03,$01,$01,$01,$01,$01,$01,$01,$01,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$01,$01,$01,$01,$01,$01,$01,$01,$03,$03,$03,$07,$0C,$0C,$0C,$0C,$0C,$0C,$0C
        BYTE    $03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$01,$03,$03,$03,$03,$03,$03,$03,$03,$03,$03,$07,$05,$05,$05,$03,$03,$03,$0C
        BYTE    $03,$03,$01,$03,$03,$03,$03,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$0C,$0A,$0A,$0A,$0A,$0A,$0C
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0B,$0B,$0B,$0B,$0B,$01,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$01,$0B,$0B,$07,$0C,$0C,$01,$0F,$0F,$01,$0C
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$03,$01,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$01,$0B,$0B,$0B,$0B,$0B,$07,$05,$05,$05,$05,$05,$05,$05
        BYTE    $05,$05,$03,$03,$03,$03,$03,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$05,$05,$05,$01,$05,$05,$05
        BYTE    $03,$0B,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$05,$05,$05,$05,$01,$05,$05
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$05,$05,$05,$05,$05,$05,$05
        BYTE    $03,$03,$03,$03,$03,$01,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$01,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$03,$03,$03,$03,$03,$03,$03
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$01,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$01,$0C,$0C,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0C,$07,$03,$03,$03,$03,$03,$03,$03
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0C,$07,$03,$03,$03,$03,$03,$03,$03
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0C,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$0B,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0C,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$0B,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0B,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0C,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $03,$03,$03,$03,$03,$03,$03,$0B,$0C,$0C,$0C,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0C,$07,$01,$01,$01,$01,$01,$01,$01
        BYTE    $0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0C,$0C,$0C,$0C,$0C,$0C,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$07,$07,$0C,$0C,$0C,$0C,$0C
        BYTE    $0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$0B,$07,$0C,$0C,$0C,$0C,$0C,$0C,$0C
        BYTE    $0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$0C,$07,$07,$07,$07,$07,$07,$07,$0C
        BYTE    $0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$07,$0C,$0C,$0C,$0C,$0C,$0C,$0C

En Project properties, en la parte de build order ubiquemoslos al final de todos, primero screen y luego screen_colour.

Solo queda agregar una pequeña macro que copie estos dos bloques de codigo a sus correspondientes direcciones (pantalla y color), como la siguiente (agregar al archivo macros.asm):

defm      copyScreen
          ldx            #$00      
@loop          
          lda            screen,x  
          sta            scrPtr,x  

          lda            screen + $100,x  
          sta            scrPtr + $100,x  

          lda            screen + $200,x  
          sta            scrPtr + $200,x  

          lda            screen + $300,x  
          sta            scrPtr + $300,x  

          lda            colour,x  
          sta            scrColPtr,x

          lda            colour + $100,x  
          sta            scrColPtr + $100,x

          lda            colour + $200,x  
          sta            scrColPtr + $200,x

          lda            colour + $300,x  
          sta            scrColPtr + $300,x

          inx
          cpx            #$00       
          bne            @loop     
          endm

e invocamos copyScreen en main.asm

*         = $1000

; no cambiar el orden de estas llamadas!
; (copyScreen pisa la inicializacion de los punteros de sprites)
          copyScreen
          initVars

main_loop
          jsr            animatePlayer1
          jsr            animatePlayer1Fire

          waitrt
          jmp            main_loop 

exit
          rts                      ; BASIC

Como ya saben a esta altura el codigo del proyecto con lo que tenemos desarrollado hasta el momento lo pueden bajar desde Github:
https://github.com/moonorongo/jp_wars.git

Fondo del juego

Vamos a diseñar un fondo sencillo para nuestro juego, simulando algunas estrellas con los caracteres PETASCII, asi le vamos dando un marco al juego. Para ello utilizaremos una de las herramientas que nos provee el CBM PRG Studio para diseñar pantallas. Para ello vamos al menú Tools, y seleccionamos Screen editor:

y se nos abrirá una ventana para editar una pantalla de C64.

El Editor

Aquí tenemos 4 áreas bien separadas:

Área de trabajo: nuestra pantalla, una matriz de caracteres, de 40×25.
Character set (Caracteres): El set que utilizaremos para trabajar. La C64 tiene 2 sets de 128 caracteres, el primero solo con mayusculas y caracteres PETASCII, y el segundo incluyendo las minusculas, con un set reducido de caracteres graficos. Del caracter 128 en adelante se repite el set, pero invertido (esto es clickeando en Reversed). El caracter que tengamos seleccionado aqui es el que se utilizará con la opcion Draw, subopcion Chars, del siguiente área.
Mode: Aqui especificamos como queremos dibujar en la matriz de pantalla. Los modos son los siguientes:

  • Draw: Modo dibujo, segun las opciones debajo que tengamos seleccionadas podremos dibujar con el caracter que tengamos seleccionado de Character set, o Lineas, o Cajas. Si seleccionamos cajas se nos habilita un boton para configurar la forma en que se crearan (ejemplo: si se hacen con un caracter seleccionado, o rellenas, o utilizando unos determinados PETASCII…). Si hacemos click con el boton izquierdo pinta, y si hacemos click con el derecho borra.
  • Select: Al seleccionar un area se nos habilitará el menu Selection, tras lo cual podemos cortar, copiar, mover la seleccion, colorear, etc.
  • Re-colour: Igual que Draw, pero aqui solo cambia el color de los caracteres sobre los que dibujemos.
  • Text: Aqui podemos ingresar texto.
  • Path: Podemos generar un path, y cuando queremos que lo ‘pase a chars’, vamos al menu ‘Path’ y seleccionamos ‘convert to characters’. No le encontre demasiada utilidad a esta opcion… Tal vez ustedes…

Colours: Aqui especificamos el color con el que pintaremos. A fin de mantener la simpleza del tutorial nosotros diseñaremos una pantalla en modo ‘Single colour’ (los modos multicolores o extendido los dejare para bastante mas adelante… si llego…). Seleccionamos el color con que queremos pintar de la opcion [11] Character colour (las otras se utilizan en los demas modos, y la de background no tiene sentido especificarla.)

Mi pantalla de fondo

Finalmente solo queda guardarla en un archivo, vamos a File, y seleccionamos ‘Save’. Se nos guardara en el ‘Project Explorer->Screen Designs’. En el próximo tutorial vamos a ver como exportar esta pantalla con sus colores, y como utilizarla en nuestro proyecto.

Como ya saben a esta altura el codigo del proyecto con lo que tenemos desarrollado hasta el momento lo pueden bajar desde Github:
https://github.com/moonorongo/jp_wars.git

Orden en el código

En este post vamos a organizar un poco el proyecto. Hasta ahora estabamos trabajando todo en un solo archivo, lo cual si bien es comodo al principio, en cuanto metemos un par de cositas se vuelve inmanejable.
Para ello vamos a separar el archivo en varios modulos. CBM Prg Studio nos permite fraccionar el codigo en distintos archivos, y luego especificar como estos archivos tienen que estar ordenados (al momento de compilar). Esto es necesario porque, por ejemplo, las definiciones de macros deben estar ANTES de ser invocadas.

Vamos a separar todo nuestro codigo en 6 archivos diferentes: main.asm, macros.asm, vars.asm, sprites.asm, animatePlayer1.asm, animatePlayer1Fire.asm. A medida que el proyecto crezca se iran agregando mas archivos. Los archivos vamos a ordenarlos de la siguiente manera (esta ventana es la de Project Properties):

 

En el frame ‘Project Files (shown in build order)’ tenemos que seleccionar ‘use specific build order’, asi de esa manera le decimos al compilador el orden correcto de los archivos.
Nótese que el primer archivo no es el main.asm, sino sprites.asm, y si observamos mas en detalle podemos ver que la columna ‘Start’ de dicho archivo dice ‘$840’. Ahi está especificando que el contenido de dicho archivo lo cargue en la posicion $840 de memoria. De esta manera nos evitamos las macros movedata y la duplicacion de datos (en las definiciones del sprite que luego copiabamos a una direccion accesible por el VIC II). Asi queda mas eficiente, mas bonito y nos ahorramos unos cuantos bytes.

Archivos

sprites.asm

*         = $0840
; dejamos 2 kbytes para la definicion de los sprites, ya quedan mapeados en un area de memoria que
; puede acceder el VIC II

jetpac    
          bits           ..........BBBB..........
          bits           .........BBB............
          bits           .......B.BB.BBBB........
          bits           .......B.BBB.BBB........
          bits           .......B.BBB.BBB........
          bits           ......BB..BBBBB.........
          bits           ......BB................
          bits           ......B.B.BBBB..........
          bits           ......BBB.B..BB.B.......
          bits           ......B.B.B..BB.B.......
          bits           ......BBB.B....BBBBB....
          bits           ......B.BB.BBBB.........
          bits           ......BBBB.BBB..........
          bits           .......BBBB.BBBBBB......
          bits           .......B.B..BBBBBB......
          bits           ......B.B.B.....BB......
          bits           ......BBBBB.............
          bits           ................BBB.....
          bits           .......BB.B.....BBBB....
          bits           .......B..B.............
          bits           .....B..B...............
          byte           0                          ; byte que se desperdicia

jetpacFire
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           B..B..BBBBBBBBBBBBBBBB.B
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................

Notese que al final de la definicion jetpac hay un byte, con valor 0, que no estaba antes. Esto es asi ya que el puntero de sprites del VIC accede de a 64 bytes, y el sprite necesita 63 bytes, por lo que necesariamente se va a desperdiciar 1 byte en cada sprite. Mas adelante veré si le puedo dar alguna utilidad (guardar algun estado, que se yo…)

vars.asm

raster    = $d012
cborde    = $d020
cfondo    = $d021
spractive = $d015
sprxpandX = $d01d          

; direcciones jetpac 
sprcolor  = $d027
sprpoint  = $07f8
sprx      = $d000
spry      = $d001
joy2      = $dc00
fire1     = $02         ; zero page direccion sin usar

; direcciones disparo
sprcolorFire  = $d028
sprpointFire  = $07f9
sprxFire      = $d002
spryFire      = $d003

skiprt    = $03         ; para pausar la ejecucion

Notese: agregamos una variable skiprt, con un valor arbitrario de $03. Esta variable se utiliza en una nueva rutina para controlar la velocidad de ejecucion. Anteriormente frenabamos la ejecucion en cada refresco de pantalla, lo cual era excesivamente lento. Ahora lo que hacemos es frenar cada 3 refrescos, con lo que aceleramos un poco el juego. Seguramente mas adelante quitaremos la macro waitrt, y la sustituiremos por una interrupcion, lo que seguramente es lo correcto de hacer. De momento es lo que hay…

macros.asm

defm      waitrt
          ldx            skiprt    
          cpx            #$00
          bne            @next     
          ldx            #$03      
          stx            skiprt    

@loop     ldx            raster
          cpx            #$0       
          bne            @loop     
@next     
          dec            skiprt    
          endm

defm      initVars
          lda            #$00      ; borde y fondo negro
          sta            cborde     
          sta            cfondo

          lda            #$03      ; inicializo skiprt (conteo hasta hacer un wait retrace)
          sta            skiprt

          ldx            #172      ; posicionamos jetpac
          stx            sprx
          ldx            #139       
          stx            spry

          ldx            #196      ; posicionamos disparo
          stx            sprxFire
          ldx            #139       
          stx            spryFire

          lda            #$01      
          sta            spractive ;activamos el sprite 0 

          lda            #$01      
          sta            sprcolor  ; jetpac color blanco

          lda            #$07      
          sta            sprcolorFire; fire color amarillo

          lda            #$02      
          sta            sprxpandX ; expandimos X el disparo.

          lda            #$21      
          sta            sprpoint  ; sprite 0 en $0840

          lda            #$22 
          sta            sprpointFire  ; sprite 0 en $0880

          lda            #$0       ; fire flag 0      
          sta            fire1     
          endm

main.asm

*         = $1000

          initVars

main_loop
          jsr            animatePlayer1
          jsr            animatePlayer1Fire

          waitrt
          jmp            main_loop 

exit
          rts                      ; BASIC

animatePlayer1.asm

animatePlayer1
          lda            joy2      
          cmp            #127      
          beq            next      

          lda            joy2      
          and            #1        ; up
          bne            chkDown   
          dec            spry      

chkDown   lda            joy2      
          and            #2        ; down
          bne            chkLeft   
          inc            spry      

chkLeft   lda            joy2      
          and            #4        ; left
          bne            chkRight  
          dec            sprx      

chkRight  lda            joy2      
          and            #8        ; right
          bne            chkFire   
          inc            sprx      

chkFire   lda            joy2      
          and            #16       ; fire
          bne            next      

          ldx            fire1     
          cpx            #0        
          bne            next      ; checkeo si ya disparo

          lda            #1        ; setea status fire1 
          sta            fire1     
          lda            #$03      
          sta            spractive ;activamos el sprite 1 

;         end check joystick          

next
          rts

animatePlayer1Fire.asm

animatePlayer1Fire
          ldx            fire1     
          cpx            #$0       
          bne            @initFire
          rts                      ; vuelve si status fire es 0

@initFire
          ldx            fire1     
          cpx            #$1       
          bne            @loop     
                                   ; init fire (status 1)          
          ldx            spry
          stx            spryFire  

          lda            sprx      
          adc            #12
          sta            sprxFire
          inc            fire1     ; paso a status 2 (disparando)

@loop
          lda            #1        
          adc            sprxFire  ; incremento 3 x disparo
          sta            sprxFire  

          bcs            @setStatus0
          jmp            @next     

@setStatus0          
          ldx            #$0       ; si no seteo flag en 0
          stx            fire1     
          lda            #$01      
          sta            spractive ; desactivamos el sprite 1 

@next          
          rts

Como es un bodrio poner todos estos archivos, a partir de ahora tengo todo subido a github, pueden clonarse el repo, y actualizar a medida que vamos avanzando con el juego:
https://github.com/moonorongo/jp_wars.git
En los próximos posts trataré de centrarme en el bloque de codigo que trabaje, y el resto se lo bajan de github.

Disparos

Con el jetpac volando, ahora vamos a agregar una funcion para que dispare un laser. La teoria es sencilla: en el loop incluimos un llamado a una subrutina, que si tiene seteado un flag predefinido va a animar un disparo. Cuando el jugador pulse el disparo ponemos un flag en 1, asi la rutina procede a hacer lo suyo. El disparo sera un sprite sencillo, con solamente una linea horizontal, pero con la particularidad que estará ensanchada en el eje X (para ello utilizaremos uno de los registros del VIC)

Algunos detalles

Basicamente agregamos un sprite jetpacFire que es solamente una linea horizontal (simulando un laser) al que expandimos al doble. Agregamos las variables de memoria que utilizaremos para manejar dicho sprite, ademas de una variable de estado que denominaremos fire1 la que puede tener los siguientes estados:

  • 0 = No pasa nada
  • 1 = el usuario pulso disparo, entonces activo el sprite, lo posiciono en la pantalla y paso fire1 a 2
  • 2 = incremento la posicion X hasta que llegue al final de la pantalla (en realidad, hago un ADC, que es ADD con carry, si tengo desborde de acarreo entonces desactivo el sprite, y pongo fire1 a 0)

Optimizé el codigo un poco, al principio van a ver que estan todas las direcciones de memoria que ire utilizando, luego las definiciones de macro, agregue una que es para mover los datos a la posicion de memoria deseada… no me gusta del todo, espero en proximas revisiones hacer algo mejor.
También agregue la subrutina animateFire que la llamo dentro del loop, que hace lo que explique previamente. Miren el codigo, esta bien comentado.

Codigo

*         = $c000

raster    = $d012
cborde    = $d020
cfondo    = $d021
spractive = $d015
sprxpandX = $d01d          

; direcciones jetpac 
sprcolor  = $d027
sprpoint  = $07f8
sprx      = $d000
spry      = $d001
joy2      = $dc00
fire1     = $02         ; zero page direccion sin usar

; direcciones disparo
sprcolorFire  = $d028
sprpointFire  = $07f9
sprxFire      = $d002
spryFire      = $d003

; -----------------------------------------------------------
; MACROS ----------------------------------------------------
; -----------------------------------------------------------
defm      waitrt
@loop     ldx            raster
          cpx            #$0       
          bne            @loop     
          endm

; movedata origen, destino 
defm      movedata 
          ldx            #$0       
@loop     lda            /1,x  
          sta            /2,x   
          inx
          cpx            #$3f      
          bne            @loop
          endm

; -----------------------------------------------------------
; MAIN CODE -------------------------------------------------
; -----------------------------------------------------------
main      lda            #$00      
          sta            cborde     
          sta            cfondo

; posicionamos jetpac
          ldx            #172       
          stx            sprx
          ldx            #139       
          stx            spry

; posicionamos disparo
          ldx            #196       
          stx            sprxFire
          ldx            #139       
          stx            spryFire

          lda            #$01      
          sta            spractive ;activamos el sprite 0 

          lda            #$01      
          sta            sprcolor  ; jetpac color blanco

          lda            #$07      
          sta            sprcolorFire; fire color amarillo

          lda            #$02      
          sta            sprxpandX ; expandimos X el disparo.

          lda            #$0d      
          sta            sprpoint  ; sprite 0 en $0340

          lda            #$0e 
          sta            sprpointFire  ; sprite 0 en $0380

          lda            #$0       ; fire flag 0      
          sta            fire1     

          movedata       jetpac,$0340
          movedata       jetpacFire,$0380

main_loop
          jsr            chkJoystick
          jsr            animateFire

          waitrt
          jmp            main_loop 

exit
          rts                      ; BASIC

; -----------------------------------------------------------
; SUBRUTINAS ------------------------------------------------
; -----------------------------------------------------------
chkJoystick
          lda            joy2      
          cmp            #127      
          beq            next      

          lda            joy2      
          and            #1        ; up
          bne            chkDown   
          dec            spry      

chkDown   lda            joy2      
          and            #2        ; down
          bne            chkLeft   
          inc            spry      

chkLeft   lda            joy2      
          and            #4        ; left
          bne            chkRight  
          dec            sprx      

chkRight  lda            joy2      
          and            #8        ; right
          bne            chkFire   
          inc            sprx      

chkFire   lda            joy2      
          and            #16       ; fire
          bne            next      

          ldx            fire1     
          cpx            #0        
          bne            next      ; checkeo si ya disparo

          lda            #1        ; setea status fire1 
          sta            fire1     
          lda            #$03      
          sta            spractive ;activamos el sprite 1 

;         end check joystick          

next
          rts
; -------------------------------------------

animateFire
          ldx            fire1     
          cpx            #$0       
          bne            @initFire
          rts                      ; vuelve si status fire es 0

@initFire
          ldx            fire1     
          cpx            #$1       
          bne            @loop     
; init fire (status 1)          
          ldx            spry
          stx            spryFire  

          lda            sprx      
          adc            #12
          sta            sprxFire
          inc            fire1     ; paso a status 2 (disparando)

@loop
          lda            #3        
          adc            sprxFire  ; incremento 3 x disparo
          sta            sprxFire  

          bcs            @setStatus0
          jmp            @next     

@setStatus0          
          ldx            #$0       ; si no seteo flag en 0
          stx            fire1     
          lda            #$01      
          sta            spractive ; desactivamos el sprite 1 

@next          
          rts

; -----------------------------------------------------------
; DATA  -----------------------------------------------------
; -----------------------------------------------------------
jetpac    bits           ..........BBBB..........
          bits           .........BBB............
          bits           .......B.BB.BBBB........
          bits           .......B.BBB.BBB........
          bits           .......B.BBB.BBB........
          bits           ......BB..BBBBB.........
          bits           ......BB................
          bits           ......B.B.BBBB..........
          bits           ......BBB.B..BB.B.......
          bits           ......B.B.B..BB.B.......
          bits           ......BBB.B....BBBBB....
          bits           ......B.BB.BBBB.........
          bits           ......BBBB.BBB..........
          bits           .......BBBB.BBBBBB......
          bits           .......B.B..BBBBBB......
          bits           ......B.B.B.....BB......
          bits           ......BBBBB.............
          bits           ................BBB.....
          bits           .......BB.B.....BBBB....
          bits           .......B..B.............
          bits           .....B..B...............

jetpacFire
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           B..B..BBBBBBBBBBBBBBBB.B
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................
          bits           ........................

 

Sprites en movimiento

Hoy vamos a mover el sprite que hicimos por la pantalla, utilizando el joystick en el port 2.

Los joysticks son manejados una de las CIA que tiene la C64, para ello vamos a leer la direccion de memoria $dc00 que corresponde al joystick 2. Los bits 0,1,2,3 corresponden a las direcciones (0 up, 1 down, 3 right, 4 left) y el bit 5 correspondiente al disparo. Si leemos la direccion vamos a notar que nos devuelve el valor 127, y cuando movemos en alguna direccion o disparamos se pone el bit correspondiente en 0. Hay muchas formas de leer esto, pero me parecio la mas simple la siguiente:

Cargamos en el registro A el contenido de $dc00, le hacemos un AND (1, 2, 4, 8, 16), y si da 0 es porque esta pulsado el joy en la direccion que estamos chequeando, entonces incrementamos o decrementamos el registro de posicion X o Y del sprite correspondiente, y proseguimos a chequear otra direccion de movimiento.

El codigo es mas claro:

joy2 = $dc00

main_loop
          lda            joy2      ; cargo en A el contenido de $DC00
          cmp            #127      
          beq            next      ; si es 127 (11111111) es que no pulse nada, salteo el checkeo restante

; aca entra si se pulso algo en el joystick
          lda            joy2      
          and            #1        ; up (11111110 AND 00000001 = 0 si pulso arriba, si no es 1)
          bne            chkDown   ; si el resultado de la ultima operacion es 1 pasa a chequear otra direccion de movimiento (*)
          dec            spry      ; y si no decrementa posicion Y del sprite 1 

chkDown   lda            joy2      
          and            #2        ; down
          bne            chkLeft   
          inc            spry      

chkLeft   lda            joy2      
          and            #8        ; left
          bne            chkRight  
          dec            sprx      

chkRight  lda            joy2      
          and            #4        ; right
          bne            chkFire   
          inc            sprx      

chkFire   lda            joy2      
          and            #16       ; fire
          bne            next      
          rts                      ; vuelve al BASIC
next
          jmp            main_loop 

La parte mas ‘rara’ de entender para mi fue donde señalé con (1). BNE es una instruccion que hace un salto si la comparacion previa da distinto:
ejemplo: tengo en X el valor #5, hago CPX #6 y la siguiente instruccion BNE va a saltar, porque el resultado de la comparacion da que son diferentes.
Ahora ¿como hace la comparacion el procesador? Simple, lo que hace es una resta, y si da 0 es que son iguales, y si da distinto de 0 son diferentes.
En este caso estamos haciendo una operacion logica AND, y si el resultado de la operacion es 1 hace un salto (porque BNE hace un salto si el resultado de la ultima operacion es distinto de 0). Si da 0 es porque el bit de la direccion que queremos testear se pone en 0, BNE no hace el salto, pasa a la siguiente instruccion que (o casualidad) decrementa o incrementa la posicion X/Y del sprite.
¿Se entiende?

Aca el codigo completito

Cambiamos el sprite del pacman por el del juego Jetpac, de la ZX Spectrum, lo podemos mover con el joystick conectado al port 2, y volvemos al BASIC con el disparo. La idea con este codigo es ir incorporando bloques para armar un pequeño juego. En las siguientes entregas vamos a agregar disparos, colisiones, 2 jugadores… lo que salga…

*         = $c000

raster    = $d012
cborde    = $d020
cfondo    = $d021
spractive = $d015
sprcolor  = $d027
sprpoint  = $07f8
sprx      = $d000
spry      = $d001
joy2      = $dc00

defm      putsprite
          ldx            #/1       
          stx            sprx
          ldx            #/2       
          stx            spry
          endm

defm      waitrt
@loop     ldx            raster
          cpx            #$0       
          bne            @loop     
          endm

main      lda            #$00      
          sta            cborde     
          sta            cfondo

          putsprite      172,139     

          lda            #$01      
          sta            spractive ;activamos el sprite 0

          lda            #$01      
          sta            sprcolor  ; lo ponemos color blanco

          lda            #$0d      
          sta            sprpoint  ; ponemos el puntero del sprite 0 en $0340

          jsr            movedata  

main_loop
;         check joystick 
          lda            joy2      
          cmp            #127      
          beq            next      

          lda            joy2      
          and            #1        ; up
          bne            chkDown   
          dec            spry      

chkDown   lda            joy2      
          and            #2        ; down
          bne            chkLeft   
          inc            spry      

chkLeft   lda            joy2      
          and            #8        ; left
          bne            chkRight  
          dec            sprx      

chkRight  lda            joy2      
          and            #4        ; right
          bne            chkFire   
          inc            sprx      

chkFire   lda            joy2      
          and            #16       ; fire
          bne            next      
          rts
;         end check joystick          

next
          waitrt
          jmp            main_loop 

exit
          rts                      ; return to BASIC

movedata  ldx            #$0
@loop     lda            jetpac,x  
          sta            $0340,x   
          inx
          cpx            #$3f      
          bne            @loop
          rts

jetpac    bits           ..........BBBB..........
          bits           .........BBB............
          bits           .......B.BB.BBBB........
          bits           .......B.BBB.BBB........
          bits           .......B.BBB.BBB........
          bits           ......BB..BBBBB.........
          bits           ......BB................
          bits           ......B.B.BBBB..........
          bits           ......BBB.B..BB.B.......
          bits           ......B.B.B..BB.B.......
          bits           ......BBB.B....BBBBB....
          bits           ......B.BB.BBBB.........
          bits           ......BBBB.BBB..........
          bits           .......BBBB.BBBBBB......
          bits           .......B.B..BBBBBB......
          bits           ......B.B.B.....BB......
          bits           ......BBBBB.............
          bits           ................BBB.....
          bits           .......BB.B.....BBBB....
          bits           .......B..B.............
          bits           .....B..B...............