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...............

 

Sprites

Recuerdo una de las primeras cosas que aprendi a programar fueron graficos, concretamente a redefinir los ‘UDG’ en la ZX Spectrum, máquina con la hice mis primeras armas en programación. Luego me compre una C64, la cual tenia la ventaja de ser una maquina mas poderosa, pero mas inaccesible al momento de programar.
En realidad lo mas importante que hay que saber para dibujar sprites (y toquetear todas las cosas que tiene el VIC II) es el siguiente número mágico: $d000 (53248 en decimal). Esta es la direccion base de los 47 registros que tiene la C64 para hacer maldades con el video (y mira que le sobra para hacer cosas).
De paso vamos a trabajar con bloques de datos en el macro assembler. Estos nos permiten facilmente definir cadenas (por ejemplo, en el ‘Hello World’ que hicimos previamente usamos una cadena de texto), o bloques de data, de forma muy similar a como utilizabamos DATA en BASIC. Esto es medio largo, asi que comencemos:

Para definir datos lo unico que debemos hacer es hacer una etiqueta, luego escribimos el tipo de dato (text, byte, word, bits), y a continuación el valor. En el siguiente ejemplo vamos a definir una etiqueta pacman, que contendra la definición de un sprite, como el siguiente:

 

pacman      bits ..........BBBBB.........
            bits .......BBBBBBBBBBB......
            bits ......BBBBBBBBBBBBB.....
            bits .....BBBBBBB..BBBBBB....
            bits ....BBBBBBBB.BBBBBBBB...
            bits ...BBBBBBBBBBBBBBBBBBB..
            bits ...BBBBBBBBBBBBBBBBB....
            bits ...BBBBBBBBBBBBBB.......
            bits ..BBBBBBBBBBBBB.........
            bits ..BBBBBBBBBBB...........
            bits ..BBBBBBBB..............
            bits ..BBBBBBBBBBB...........
            bits ..BBBBBBBBBBBBB.........
            bits ...BBBBBBBBBBBBBB.......
            bits ...BBBBBBBBBBBBBBBBB....
            bits ...BBBBBBBBBBBBBBBBBBB..
            bits ....BBBBBBBBBBBBBBBBB...
            bits .....BBBBBBBBBBBBBBB....
            bits ......BBBBBBBBBBBBB.....
            bits .......BBBBBBBBBBB......
            bits ..........BBBBB.........

Con esto ya tenemos los valores en memoria para el sprite. Nótese que no tuvimos que convertir de binario a decimal ni toda la parafernalia que lleva hacer eso, simplemente definimos una etiqueta que tendra los 63 bytes necesarios para la imagen, nada mas que codificados de una manera mas piola… Tambien podriamos haber puesto lo siguiente:

pacman  byte    0, 62, 0 
        byte    1, 255, 192
        .
        .
        .

Pero tenemos que tomarnos el trabajo de convertir los bits de la imagen a binario, y luego pasarlos a decimal.
Con esto ya tenemos definido el sprite, ahora tendremos que copiar estos datos a una parte de la memoria que pueda ser accesible por el VIC…

Punteros de Sprites

Los punteros de sprites son las direcciones de memoria, para cada sprite, que contienen la direccion en que estan los datos del mismo. Estas van de $07f8 (2040) para el sprite 1 a $07ff(2047) para el sprite 8. Peeero si para acceder a una direccion de memoria necesitamos 2 bytes… ¿Como podemos especificar donde estan los datos de nuestro sprite con solamente un byte?.
Los punteros de sprite se manejan de una forma muy particular. Primero de todo, solamente podemos apuntar a datos que esten en el mismo banco de memoria en el que tengamos la memoria de pantalla. Tenemos 4 bancos, el primero de ellos ($0000-$3fff) es el que se usa por defecto. Un sprite toma 63 bytes de datos, pero tienen que estar en intervalos de 64 bytes, asi que se pueden poner los datos en las direcciones $0000, $0040, $0080, $0c00, $0100 …
En cada banco del VIC podemos tener hasta 256 sprites… voila, eso es lo que podemos representar con un byte… ¿se entiende como funciona el cachivache? Si pongo el valor $0 en $07f8 los datos del sprite deberemos tenerlos en la direccion $0000, si ponemos $1 entonces en $0040… y asi hasta completar el banco… y si quisieramos hacer una animacion solamente tenemos que cambiar el puntero y tendremos la animacion con los diferentes frames.

Ahora vamos a copiar los datos del sprite a la direccion $$0400, que corresponde al puntero $10. Esta pequeña rutina a continuacion toma los datos de la etiqueta pacman y los copia a la direccion $0400

movedata    ldx #$00
loop        lda pacman, x
            sta $$0400,x
            inx
            cpx #$3f
            bne loop
            rts

Direcciones importantes del VIC II

Para este ejemplo vamos a utilizar el sprite 1, por lo que necesitaremos activarlo, posicionarlo en pantalla, darle color y especificar donde estan los datos que definen su forma. Para ello utilizaremos los siguientes registros:

$00 (0) $d000 (53248) Coordenada x sprite 0
$01 (1) $d001 (53249) Coordenada y sprite 0
$15 (21) $d015 (53269) Sprite enable. aca controlamos cual de todos activamos
$27 (39) $d027 (53287) Color sprite 0
(*) $07f8 (2040) Puntero de memoria del sprite 0

(*) no es un registro del VIC, pero necesitamos esta direccion para especificar donde estan los datos de nuestro sprite.

Para mas detalle de los registros, o si queremos utilizar otro sprite en el ejemplo: registros del VIC II.

Ejemplo con sprite 0

* = $c000
          lda            #$00      
          sta            $d020     
          sta            $d021     ; ponemos borde y fondo negro

          lda            #$01      
          sta            $$d015    ;activamos el sprite 0

          lda            #172      
          sta            $d000     
          lda            #139
          sta            $d001     ; lo centramos en la pantalla (172,139)

          lda            #$07      
          sta            $d027     ; lo ponemos color amarillo

          lda            #$10      
          sta            $07f8     ; ponemos el puntero del sprite 0 en $0100

          jsr            movedata  
          rts

movedata  ldx            #$0
loop      lda            pacman,x  
          sta            $0400,x   
          inx
          cpx            #$3f      
          bne            loop
          rts

pacman    bits           ..........BBBBB.........
          bits           .......BBBBBBBBBBB......
          bits           ......BBBBBBBBBBBBB.....
          bits           .....BBBBBBB..BBBBBB....
          bits           ....BBBBBBBB.BBBBBBBB...
          bits           ...BBBBBBBBBBBBBBBBBBB..
          bits           ...BBBBBBBBBBBBBBBBB....
          bits           ...BBBBBBBBBBBBBB.......
          bits           ..BBBBBBBBBBBBB.........
          bits           ..BBBBBBBBBBB...........
          bits           ..BBBBBBBB..............
          bits           ..BBBBBBBBBBB...........
          bits           ..BBBBBBBBBBBBB.........
          bits           ...BBBBBBBBBBBBBB.......
          bits           ...BBBBBBBBBBBBBBBBB....
          bits           ...BBBBBBBBBBBBBBBBBBB..
          bits           ....BBBBBBBBBBBBBBBBB...
          bits           .....BBBBBBBBBBBBBBB....
          bits           ......BBBBBBBBBBBBB.....
          bits           .......BBBBBBBBBBB......
          bits           ..........BBBBB.........

y el resultado final:

<–

Consideraciones a tener en cuenta

  • algo interesante que hice adrede es poner la el puntero del sprite en el inicio de la memoria de video. Toda esa basura que se puede ver en la parte superior de la pantalla es la definicion del sprite. Nótese ademas que llevé el cursor al primer caracter de la pantalla, lo que hizo aparecer un puntito en el sprite. Es interesante que podemos borrar los caracteres, y ver como se borra el sprite. Por ejemplo, puedo borrar de a 3 caracteres, lo que hara que quede como interlineado (en realidad, ponemos un caracter @ que en binario es 00000000):
  • para centrar en la pantalla en teoria deberia poner 160×100, ya que la resolucion de la C64 es de 320×200. Pero los sprites pueden estar posicionados en CUALQUIER lugar de la pantalla, haciendo algo que se conoce como apertura de bordes, asi que la posicion 0 en realidad no es al lado del borde, sino al lado del borde de la pantalla.
  • Estamos definiendo en un area de memoria el sprite, y luego lo copiamos a otra, dado que si no el VIC no podria acceder a los datos… en realidad es ineficiente, los datos del sprite habria que tenerlos en disco, aparte, y cargarlos donde correspondan, que sean accesibles por el VIC.
  • Definimos el sprite con bits ........BBBBBBBB........, pero solo para este ejemplo… en realidad es mejor utilizar alguna herramienta que nos genere esos datos y cargarlos de disco.
  • pueden toquetear el codigo, definir otro sprite, usar otros sprites (1,2,3,…7), cambiar los colores, o directamente desde BASIC, haciendo POKE’s a las direcciones correspondientes para ver como cambia… O poner una direccion mas adecuada para el puntero de datos del sprite… no se, jueguen …