icono de compudanzas

tutorial uxn: día 6, hacia el pong

esta es la sexta sección del tutorial de uxn! aquí hablamos de cómo podemos integrar todo lo que hemos cubierto para crear subrutinas y programas más complejos para el ordenador varvara.

basamos nuestra discusión en una recreación del clásico juego pong.

además de utilizar estrategias y fragmentos de código anteriores, cubrimos estrategias para dibujar y controlar sprites de varios tiles, y para comprobar las colisiones.

lógica general

aunque pong pueda parecer sencillo y fácil de programar, una vez que lo analicemos nos daremos cuenta de que hay varios aspectos a tener en cuenta. afortunadamente, la mayoría de ellos se pueden dividir como diferentes subrutinas que podemos discutir por separado.

abordaremos los siguientes elementos en orden:

dibujando el fondo: repitiendo un tile

en el tutorial de uxn día 5 hablamos de una forma de crear un bucle para repetir un tile de 1bpp varias veces seguidas.

aquí ampliaremos ese procedimiento para que también se repita verticalmente en toda la pantalla.

configurando

empecemos con el siguiente programa como plantilla. incluye los datos para un sprite de 1bpp que consiste en líneas diagonales.

( hola-pong.tal )

( dispositivos )
|00 @Sistema [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Pantalla [ &vector $2 &ancho $2 &alto $2 &auto $1 &pad $1 &x $2 &y $2 &direc $2 &pixel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &boton $1 &tecla $1 ]

( macros )
%RTN { JMP2r }

( programa principal )
|0100
@configuracion
      ( establecer los colores del sistema )
      #2ce9 .Sistema/r DEO2
      #01c0 .Sistema/g DEO2
      #2ce5 .Sistema/b DEO2
      
BRK

@tile-fondo 1122 4488 1122 4488

repetir un tile en una fila

¿qué procedimiento podríamos seguir para repetir el dibujo de un tile empezando por x, y terminando en un límite correspondiente a x+ancho?

una forma sería algo así como:

una primera versión

digamos que nuestra x inicial es 0000, nuestro ancho es el ancho de la pantalla, y el tile que estamos dibujando es tile-fondo.

el límite del bucle, x+ancho, sería también el ancho de la pantalla.

el primer paso, dibujar el tile en x sería:

;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
#0000 .Pantalla/x DEO2 ( establecer la x inicial )

#03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )

el segundo paso, añadir 8 a x, ya lo sabemos:

.Pantalla/x DEI2 #0008 ADD2 ( añadir 8 a x )
.Pantalla/x DEO2 ( guardar la nueva x )

comprobar si x es menor que el límite, saltando si lo es, sería algo así:

.Pantalla/x DEI2 ( obtener x )
.Pantalla/ancho DEI2 ( obtener el límite )
LTH2 ,&bucle JCN ( saltar si x es menor que el límite )

integrando todo ello, podríamos obtener:

;tile-fondo .Pantalla/direc DEO2 ( fijar la dirección del tile )
#0000 .Pantalla/x DEO2 ( establecer la x inicial )

 &bucle-x
    #03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
    .Pantalla/x DEI2 #0008 ADD2 ( añadir 8 a la x )
    DUP2 ( duplicar la nueva x )
    .Pantalla/x DEO2 ( guardar la nueva x )
    .Pantalla/ancho DEI2 ( obtener el límite )
    LTH2 ,&bucle-x JCN ( saltar si x es menor que el límite )

nótese el uso de DUP2 para evitar releer el valor de x.

¡esto debería funcionar ahora! pero vamos a discutir una forma más agradable de hacerlo :)

una segunda versión, usando la pila

en lugar de leer el ancho de pantalla y la coordenada x cada vez, podríamos usar la pila para almacenar y manipular esos valores.

después de establecer la dirección del tile, podemos empujar nuestro límite (el ancho de la pantalla) y el valor inicial hacia abajo en la pila:

.Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )

usaremos ese valor en la parte superior de la pila como coordenada x.

dentro del bucle, podemos duplicarlo para establecerlo como la x de la pantalla, e incrementarlo.

entremedio, podemos enviar nuestro byte de sprite para dibujar el tile.

 &bucle-x
    DUP2 .Pantalla/x DEO2 ( establecer coordenada x )
    #03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
    #0008 ADD2 ( incrementar x )

en este punto, la pila tiene la nueva x en la parte superior de la pila, y el ancho de la pantalla debajo.

podemos compararlos en modo mantener para conservar esos valores en la pila, y hacer nuestro salto como antes:

GTH2k ( ¿es ese ancho mayor que x?, o también, ¿es x menor que el ancho? )
,&bucle-x JCN ( salta si x es menor que el límite )

cuando terminamos el bucle, tenemos que hacer POP a ambos valores.

usando esta estrategia, obtendríamos el siguiente bucle:

;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )

.Pantalla/ancho DEI2 #0000 ( empujar límite y `x` inicial )
 &bucle-x
    DUP2 .Pantalla/x DEO2 ( establecer coordenada x )

    #03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
    
    #0008 ADD2 ( incrementar x )
    GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
    ,&bucle-x JCN ( salta si x es menor que el límite )
POP2 POP2 ( eliminar x y el límite )

no sólo es un código más corto, sino que también es más rápido porque realiza menos operaciones dentro del bucle.

¡es bueno tenerlo en cuenta!

programa completo

lo siguiente muestra nuestro programa en contexto, llenando completamente la primera fila de nuestra pantalla con nuestro tile:

captura de pantalla mostrando la primera fila de la pantalla varvara rellenada con líneas diagonales
( hola-pong.tal )

( dispositivos )
|00 @Sistema [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Pantalla [ &vector $2 &ancho $2 &alto $2 &auto $1 &pad $1 &x $2 &y $2 &direc $2 &pixel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &boton $1 &tecla $1 ]

( macros )
%RTN { JMP2r }

( programa principal )
|0100
@configuracion
    ( establecer los colores del sistema )
    #2ce9 .Sistema/r DEO2
    #01c0 .Sistema/g DEO2
    #2ce5 .Sistema/b DEO2
    
    ( dibujar fondo )
    ;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )

    .Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
     &bucle-x
        DUP2 .Pantalla/x DEO2 ( fijar coordenada x )
        #03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
        #0008 ADD2 ( incrementar x )
        GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
        ,&bucle-x JCN ( salta si x es menor que el límite )
    POP2 POP2 ( eliminar x y el límite )
BRK

@tile-fondo 1122 4488 1122 4488

repitiendo una fila

similar a lo que acabamos de hacer: ¿cuál es el procedimiento que podríamos seguir para repetir verticalmente una fila empezando por `y`, y terminando en un límite correspondiente a y+altura?

siguiendo la misma estrategia, podríamos hacer:

el bucle vertical

para ilustrar un pequeño cambio, supongamos que queremos tener un margen en la parte superior e inferior de la pantalla. podemos definir este margen como una macro:

%MARGEN-PARED { #0010 } ( margen en la parte superior e inferior )

nuestra `y` inicial sería MARGEN-PARED, y nuestro límite sería la altura de la pantalla menos MARGEN-PARED.

podemos usar la misma estructura que antes, pero usando `y`:

;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )

.Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
MARGEN-PARED ( establecer `y` inicial )
 &bucle-y
    DUP2 .Pantalla/y DEO2 ( establecer coordenada y )

    ( - dibujar fila aquí - )


    #0008 ADD2 ( incrementa `y` )
    GTH2k ( ¿es ese límite mayor que `y`? o también, ¿es `y` menor que el límite? )
    ,&bucle-y JCN ( salta si `y` es menor que el límite )
POP2 POP2 ( eliminar `y` y el límite )

bucles anidados

ahora que tenemos esta estructura, podemos sustituir el comentario "dibujar fila aquí" por nuestro bucle horizontal anterior:

;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )

.Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
MARGEN-PARED ( establecer `y` inicial )
 &bucle-y
    DUP2 .Pantalla/y DEO2 ( establecer coordenada y )

    ( dibujar fila )
    .Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
     &bucle-x
        DUP2 .Pantalla/x DEO2 ( fijar coordenada x )

        ( dibujar sprite de 1bpp con color 3 y 0 )
        #03 .Pantalla/sprite DEO 
        
        #0008 ADD2 ( incrementar x )
        GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
        ,&bucle-x JCN ( salta si x es menor que el límite )
    POP2 POP2 ( eliminar x y el límite )

    #0008 ADD2 ( incrementar y )
    GTH2k ( ¿es ese límite mayor que y? o también, ¿es `y` menor que el límite? )
    ,&bucle-y JCN ( salta si `y` es menor que el límite )
POP2 POP2 ( eliminar `y` y el límite )

observa cómo, al asegurarnos de que nuestro bucle interno deja limpia la pila al terminar, podemos ponerlo allí sin problemas: los valores correspondientes al bucle externo permanecen intactos en la pila.

subrutina dibuja-fondo

ahora podemos envolver estos bucles anidados dentro de una subrutina:

@dibuja-fondo ( -- )
    ;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
    
    .Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
    MARGEN-PARED ( establecer `y` inicial )
     &bucle-y
        DUP2 .Pantalla/y DEO2 ( establecer coordenada y )
    
        ( dibujar fila )
        .Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
         &bucle-x
            DUP2 .Pantalla/x DEO2 ( establecer coordenada x )

            ( dibujar sprite de 1bpp con color 3 y 0 )
            #03 .Pantalla/sprite DEO 
            
            #0008 ADD2 ( incrementar x )
            GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
            ,&bucle-x JCN ( salta si x es menor que el límite )
        POP2 POP2 ( eliminar x y el límite )
    
        #0008 ADD2 ( incrementar y )
        GTH2k ( ¿es ese límite mayor que `y`? o también, ¿es `y` menor que el límite? )
        ,&bucle-y JCN ( salta si `y` es menor que el límite )
    POP2 POP2 ( eliminar `y` y el límite )
RTN

que podemos llamar simplemente desde nuestra subrutina de iniciación:

;dibuja-fondo JSR2
captura de pantalla que muestra la pantalla varvara cubierta de líneas diagonales excepto por un margen en la parte superior e inferior.

¡lindo!

en el tutorial de uxn apéndice a puedes encontrar una discusión detallada de cómo generalizar un procedimiento como éste en una subrutina dibuja-tiles que dibuje un rectángulo arbitrario rellenado con un tile dado.

se habla de varias posibilidades para usar uxntal de esa manera abstracta: yo diría que es muy interesante, pero está definitivamente fuera del alcance para hacer el juego :)

las palas

podemos pensar en las dos palas del juego como dos rectángulos, cada uno con sus propias coordenadas x e `y`, y ambos con la misma anchura y altura.

la coordenada x de cada pala puede ser constante, y la coordenada `y` debe ser de seguro una variable.

en esta parte veremos cómo dibujar las palas en base a estos parámetros, y también recapitularemos cómo cambiar sus coordenadas `y` con el controlador.

dibujar las palas multi-tile

¡quiero usar las palas como ejemplo de cómo dibujar un sprite compuesto por múltiples tiles!

los datos

he utilizado nasu para dibujar una pala compuesta por tiles de 2x3 en modo 2bpp. las numeraré de izquierda a derecha y de arriba a abajo de la siguiente manera

0 1
2 3
4 5

100R - nasu

los datos resultantes son los siguientes:

@pala
 &tile0 [ 3f 7f e7 c3 c3 c3 c3   00 00 18 3c 3c 3c 3c 3c ]
 &tile1 [ fc fe ff ff ff ff ff   00 00 00 00 00 00 06 06 ]
 &tile2 [ c3 c3 c3 e7 ff ff ff   3c 3c 3c 3c 18 00 00 00 ]
 &tile3 [ ff ff ff ff ff ff 06   06 06 06 06 06 06 06 06 ]
 &tile4 [ ff ff ff ff ff 7f 3f   00 00 00 00 00 00 00 00 ]
 &tile5 [ ff ff ff ff ff fe fc   06 06 06 06 06 1e 3c 00 ]

se pueden obtener estos números leyendo la notación hexadecimal en nasu en la parte superior derecha, primero la columna de la izquierda y luego la de la derecha, o utilizando una herramienta como hexdump con el archivo chr correspondiente:

$ hexdump -C pong.chr

he dibujado el sprite usando el modo de mezcla 85 como indica nasu, pero lo cambiaré a c5 para dibujarlo en el primer plano.

subrutina de dibujo de la pala

construyamos una subrutina que dibuje las 6 fichas de la pala en el orden correspondiente.

podríamos escribir la subrutina recibiendo como argumentos la posición x e `y` de su esquina superior izquierda:

@dibuja-pala ( x^ y^ -- )

pero añadamos también un byte de color para el byte del sprite:

@dibuja-pala ( x^ y^ color -- )

recordemos que estamos utilizando la convención de añadir un signo de intercalación (^) después del nombre de un valor para indicar que es un corto, y un asterisco (*) para indicar que es un corto que funciona como un puntero (es decir, una dirección en la memoria del programa)

por un lado esta segunda versión nos permitiría cambiar de color cuando, por ejemplo, le demos a la pelota, pero lo más importante es que esto nos permitirá borrar la pala antes de moverla, como hemos hecho en días anteriores.

en principio la subrutina debería ser directa: tenemos que establecer las coordenadas x e `y` de cada una de las fichas, relativas a las coordenadas x e `y` dadas, y dibujarlas con el color dado.

hay muchas maneras de hacerlo, dependiendo del gusto.

podríamos por ejemplo dibujar los tiles en el siguiente orden, con las siguientes operaciones:

o podríamos hacerlo de forma más tradicional:

en lugar de restar podríamos recuperar x de la pila de retorno, o de una variable relativa.

una posible ventaja de ir en orden es que podemos incrementar la dirección del sprite por 10 (16 en decimal) para llegar a la dirección del siguiente tile. para esto, y/o para los cambios de coordenadas, podemos aprovechar el byte auto de la pantalla.

sin embargo, en este caso voy a ir por la primera opción, y voy a establecer manualmente la dirección para cada tile.

adicionalmente, guardaré el color en la pila de retorno:

@dibuja-pala ( x^ y^ color -- )
    ( guardar color )
    STH

    ( establecer `y` y x iniciales )
    .Pantalla/y DEO2 
    .Pantalla/x DEO2 

    ( dibujar tile 0 )
    ;pala/tile0 .Pantalla/direc DEO2
    ( copiar color de la pila de retorno: )
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a x: )
    .Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2

    ( dibujar tile 1 )
    ;pala/tile1 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a y: )
    .Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2

    ( dibujar tile 3 )
    ;pala/tile3 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO

    ( sub 8 a x: )
    .Pantalla/x DEI2 #0008 SUB2 .Pantalla/x DEO2

    ( dibujar tile 2 )
    ;pala/tile2 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a y: )
    .Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2

    ( dibujar tile 4 )
    ;pala/tile4 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a x: )
    .Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2

    ( dibujar tile 5 )
    ;pala/tile5 .Pantalla/direc DEO2
    ( obtener y no mantener el color de la pila de retorno: )
    STHr .Pantalla/sprite DEO
RTN

¡eso es todo!

ahora podemos llamarlo, por ejemplo, de la siguiente manera y obtener nuestra pala dibujada:

#0008 #0008 #c5 ;dibuja-pala JSR2
captura de pantalla de la pala dibujada sobre el fondo

es posible considerar formas más eficientes de dibujarla. por ejemplo, podríamos tener un dibuja-sprite generalizado que reciba la dirección inicial de un conjunto de tiles, y la anchura y altura en términos de número de tiles:

@dibuja-sprite ( x^ y^ ancho alto direc* color )

crear eso podría ser un buen ejercicio para probar! en este caso me mantendré con el método manual.

lo bueno de que este proceso esté en una subrutina es que podemos "olvidarnos" de su funcionamiento interno y simplemente usarlo :)

variables y constantes para las palas

reservemos un espacio en la página cero para las coordenadas x e `y` de cada pala.

( página cero )
|0000
   @izquierda [ &x $2 &y $2 ]
   @derecha [ &x $2 &y $2 ]

mencionamos al principio que la coordenada x es constante; sin embargo, si la convertimos en una variable entonces podemos asignar dinámicamente la posición x de las palas (especialmente la derecha) dependiendo del tamaño de la pantalla.

podemos tener un par de macros para mantener las dimensiones y el color de las palas para usarlas después:

%ANCHO-PALA { #0010 } ( 2 tiles )
%ALTO-PALA { #0018 } ( 3 tiles )
%COLOR-PALA { #c5 } 

un margen para separar las palas de los bordes podría ser agradable también:

%MARGEN { #0010 }

por último, recuperemos nuestra macro MITAD2 de días anteriores:

%MITAD2 { #01 SFT2 } ( corto -- corto/2 )

iniciar posiciones

ahora podemos iniciar las posiciones de las palas.

para la x izquierda, podemos simplemente asignar un valor constante:

MARGEN .izquierda/x STZ2

para la x derecha, podemos restar el margen y el ancho de la pala del ancho de la pantalla:

.Pantalla/ancho DEI2
MARGEN SUB2 ANCHO-PALA SUB2
.derecha/x STZ2

para centrar las coordenadas `y` podemos restar la altura de la pala a la altura de la pantalla, y luego dividir entre dos:

.Pantalla/alto DEI2 ALTO-PALA SUB2 MITAD2
DUP2
.izquierda/y STZ2
.derecha/y STZ2

dibujar palas

para dibujar cada pala, podemos hacer el siguiente procedimiento dentro de nuestro vector de pantalla en-cuadro:

( dibujar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2

el programa hasta ahora

omitiendo la definición de las subrutinas dibuja-fondo y dibuja-pala, y como forma de tener un punto de comprobación, ahora mismo nuestro programa tendría el siguiente aspecto:

captura de pantalla de las dos palas, centradas verticalmente y con el mismo margen respecto a los lados
( hola-pong.tal )

( dispositivos )
|00 @Sistema [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Pantalla [ &vector $2 &ancho $2 &alto $2 &auto $1 &pad $1 &x $2 &y $2 &direc $2 &pixel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &boton $1 &tecla $1 ]

( macros )
%RTN { JMP2r }
%MITAD2 { #01 SFT2 } ( corto -- corto/2 ) 

( constantes )
%ANCHO-PALA { #0010 } ( 2 tiles )
%ALTO-PALA { #0018 } ( 3 tiles )
%COLOR-PALA { #c5 } 
%MARGEN { #0010 }
%MARGEN-PARED { #0010 } ( margen en la parte superior e inferior )

( página cero )
|0000
   @izquierda [ &x $2 &y $2 ]
   @derecha [ &x $2 &y $2 ]

( programa principal )
|0100
@configuracion
    ( establecer los colores del sistema )
    #2ce9 .Sistema/r DEO2
    #01c0 .Sistema/g DEO2
    #2ce5 .Sistema/b DEO2

    ( establecer vector de pantalla )
    ;en-cuadro .Pantalla/vector DEO2

    ( dibujar fondo )
    ;dibuja-fondo JSR2 

    ( iniciar palas )
    MARGEN .izquierda/x STZ2
    .Pantalla/ancho DEI2
    MARGEN SUB2 ANCHO-PALA SUB2
    .derecha/x STZ2

    .Pantalla/alto DEI2 ALTO-PALA SUB2
    MITAD2 DUP2
    .izquierda/y STZ2
    .derecha/y STZ2

BRK

@en-cuadro ( -> )
    ( dibujar pala )
    .izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
    .derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA dibuja-pala JSR2
BRK

movimiento de la pala

para el movimiento de la pala podemos volver a los ejemplos anteriores de mover un sprite. el proceso que hemos seguido es:

borrar o dibujar sprite

ya tenemos el proceso para dibujar nuestras palas:

( dibujar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2

para borrarlos, podemos hacer lo mismo pero usando un byte del sprite correspondiente a borrar el tile en el primer plano:

( borrar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2

donde COLOR-BORRAR en este caso sería:

%COLOR-BORRAR { #40 } ( borrar el sprite del primer plano )

¡este es un buen recordatorio para revisar las tablas de los bytes de los sprites en el tutorial de uxn día 2!

actualizar posición

para actualizar la posición de nuestras palas, podemos recurrir al ejemplo hola-sprite-en-movimiento.tal del tutorial de uxn día 4.

podemos usar las flechas arriba y abajo para cambiar la posición de la pala izquierda, y los botones ctrl y alt (A y B) para cambiar la posición de la pala derecha.

podemos tener una macro para definir la velocidad de la pala, es decir, cuánto sumaremos o restaremos al mover cada cuadro:

%VEL-PALA { #0001 }

todo esto puede ir dentro de su propia subrutina para facilitar la lectura:

@actualiza-palas ( -- )
    &izquierda
       ( pala izquierda: botones arriba y abajo )
       .Controlador/boton DEI
       DUP #10 AND ( comprobar bit para arriba )
       ,&izquierda-arriba JCN
       DUP #20 AND ( comprobar bit para abajo ) 
       &izquierda-abajo JCN
    
    &derecha JMP ( salta si no se ha pulsado ninguno de los dos )
  
    &izquierda-arriba
       .izquierda/y LDZ2 VEL-PALA SUB2 .izquierda/y STZ2 
       ,&derecha JMP 
    &izquierda-abajo
       .izquierda/y LDZ2 VEL-PALA ADD2 .izquierda/y STZ2 
       ,&derecha JMP 

    &derecha
       ( pala derecha: botones ctrl/A y alt/B )
       DUP #01 AND ( comprobar bit para A )
       ,&derecha-arriba JCN
       DUP #02 AND ( comprobar bit para B ) 
       &derecha-abajo JCN
    
    &fin JMP ( salta si no se ha pulsado ninguno de los dos )
  
    &derecha-arriba
       .derecha/y LDZ2 VEL-PALA SUB2 .derecha/y STZ2 
       ,&fin JMP 
    &derecha-abajo
       .derecha/y LDZ2 VEL-PALA ADD2 .derecha/y STZ2 

    &fin
        POP ( hacer POP al valor duplicado del botón )
RTN

procedimiento completo

integrando todo, nuestra subrutina en-cuadro se vería como lo siguiente.

¡ahora somos capaces de mover nuestras palas!

@en-cuadro ( -> )
    ( borrar palas )
    .izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
    .derecha/x LDZ2 .derecha/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2

    ( actualizar palas )
    ;actualiza-palas JSR2 

    ( dibujar palas )
    .izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
    .derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
BRK

nota que somos capaces de mover las palas más allá de los límites de la pantalla.

te invito a que modifiques la subrutina actualiza-palas para que haya un límite en el movimiento de las palas. en el tutorial de uxn día 4 discutimos algunas posibles estrategias para lograrlo :)

la pelota

¡ahora vamos a poner la pelota en marcha!

aquí trabajaremos de nuevo con un sprite multi-tile dibujado en relación a las variables x e `y` para su esquina superior izquierda.

adicionalmente, usaremos esta sección para hablar de las estrategias para la detección de colisiones, con las paredes y las palas.

dibujando la pelota

usé nasu para dibujar una pelota compuesta por tiles de 2x2 2bpp, ordenados de la siguiente manera:

0 1
2 3

estos son sus datos:

@pelota-sprite
 &tile0 [ 03 0f 1f 39 70 70 f9 ff  00 00 00 06 0f 0f 06 00 ]
 &tile1 [ c0 f0 f8 fc fe fe ff ff  00 00 00 00 08 0c 06 06 ]
 &tile2 [ ff ff 7f 7f 3f 1f 0f 03  00 00 00 00 18 0f 01 00 ]
 &tile3 [ ff ff fe fe fc f8 f0 c0  06 06 0c 1c 38 f0 c0 00 ]

podemos definir un par de macros para referirse a sus parámetros:

%TAM-PELOTA { #0010 } ( tamaño: 2 tiles por lado )
%COLOR-PELOTA { #c5 } 

subrutina para dibujar la pelota

como vamos a dibujar una sola pelota, podemos escribir su subrutina de dibujo de manera que tome sus coordenadas de la página cero en lugar de obtenerlas como argumentos en la pila.

en nuestra página cero podemos definir las etiquetas para las coordenadas:

@pelota [ &x $2 &y $2 ]

y luego en nuestra subrutina de configuración podemos asignarles valores, por ejemplo, en el centro de la pantalla:

( dentro de configuración )
( iniciar la pelota )
.Pantalla/ancho DEI2 TAM-PELOTA SUB2
MITAD2
.pelota/x STZ2

.Pantalla/alto DEI2 TAM-PELOTA SUB2
MITAD2 
.pelota/y STZ2

las coordenadas están listas, así que ahora podemos usarlas dentro de nuestra subrutina.

hagamos que la subrutina reciba el color como argumento, para poder borrar la pelota como hacemos con las palas:

@dibuja-pelota ( color -- )
    ( fijar x e `y` iniciales )
    .pelota/x LDZ2 .Pantalla/x DEO2
    .pelota/y LDZ2 .Pantalla/y DEO2

    ( dibujar tile 0 )
    ;pelota-sprite/tile0 .Pantalla/direc DEO2
    ( el byte de color ya estaba en la pila )
    DUP .Pantalla/sprite DEO 

    ( mover a la derecha )
    .Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2

    ( dibujar tile 1 )
    ;pelota-sprite/tile1 .Pantalla/direc DEO2
    DUP .Pantalla/sprite DEO 

    ( mover hacia abajo )
    .Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2

    ( dibujar tile 3 )
    ;pelota-sprite/tile3 .Pantalla/direc DEO2
    DUP .Pantalla/sprite DEO 

    ( mover a la izquierda )
    .Pantalla/x DEI2 #0008 SUB2 .Pantalla/x DEO2

    ( dibujar tile 2 )
    ;pelota-sprite/tile2 .Pantalla/direc DEO2
    .Pantalla/sprite DEO 
RTN

para dibujarla, sólo tendríamos que hacer:

( dibujar pelota )
COLOR-PELOTA ;dibuja-pelota JSR2
captura de la pantalla mostrando las palas en su posición horizontal pero a diferentes alturas, y la pelota completamente centrada en la pantalla.

movimiento de la pelota

para el movimiento de la pelota, seguiremos la misma estructura que antes:

se vería algo como lo siguiente, y podría situarse a lo largo de los procedimientos equivalentes para las palas dentro de la subrutina en-cuadro:`

( dentro de en-cuadro )
( borrar pelota )
COLOR-BORRAR ;dibuja-pelota JSR2

( actualizar pelota )
;actualiza-pelota JSR2 

( dibujar pelota )
COLOR-PELOTA ;dibuja-pelota JSR2

ahora vamos a discutir cómo construir esa subrutina actualiza-pelota :)

contabilizando el cambio de dirección

además de nuestras variables para llevar la cuenta de la posición de la pelota, deberíamos poder llevar la cuenta de la dirección por-eje en la que se mueve.

un enfoque podría ser tener una bandera para cada x e `y` que indique si debemos incrementarlos o decrementarlos.

otro enfoque podría ser tener una variable de velocidad para cada x e `y`, que se cambia de acuerdo a la dirección que queremos que la pelota vaya.

utilizaremos este último enfoque de la variable velocidad, ya que nos ayudará a discutir algunas ventajas de la aritmética de enteros sin signo.

incluimos estas variables en nuestra página cero, complementando las x e `y` que ya teníamos:

@pelota [ &x $2 &y $2 &vel-x $2 &vel-y $2 ]

diferentes direcciones

si, por ejemplo, iniciamos vel-x con 1:

#0001 .pelota/vel-x STZ2

podemos hacer que la pelota se mueva hacia la derecha haciendo:

( dentro de actualiza-pelota )
.pelota/vel-x LDZ2 ( obtener vel-x )
.pelota/x LDZ2 ( obtener x )
ADD2 ( sumar ambas )
.pelota/x STZ2 ( almacenar nueva x )

para moverse hacia la izquierda, podríamos pensar que deberíamos sustituir ADD2 por SUB2. y sí, podríamos hacerlo.

pero, dejando nuestro código como está ahora: ¿hay algún valor de velocidad-x que haga que x se haga más pequeño al sumarlos?

en otros contextos, uno podría decir, ¡"-1"!

pero no hemos utilizado signos negativos en uxn; ¡no podemos!

entonces, ¿existe un valor positivo de vel-x que haga que x se reduzca al sumarlos?

¡normalmente pensaríamos que no lo hay y que la pregunta no tiene sentido!

sin embargo, aquí estamos limitados por 8 o 16 bits. ¿y qué implica eso?

por ejemplo, si tenemos el número ffff (16 bits, todos son unos), y sumamos 0001, ¿qué obtenemos?

   1111 1111 1111 1111
 + 0000 0000 0000 0001
 ---------------------
 1 0000 0000 0000 0000

de acuerdo, es un número mayor, pero el 1 de la izquierda queda fuera de los 16 bits. en otros contextos se llamaría bit de acarreo.

en uxn, el resultado de sumar ffff y 0001 es 0000: decimos que estamos desbordando los 16 bits.

veámoslo al revés: si tenemos 0001, y sumamos ffff, obtenemos 0000, ¡es decir, 1 menos que 1!

si tenemos 0002, y añadimos ffff:

   0000 0000 0000 0010
 + 1111 1111 1111 1111
 --------------------
 1 0000 0000 0000 0001

¡obtenemos 0001, que es 1 menos que 2!

en general, si sumamos ffff a un número de 16 bits, obtendremos un valor que es 1 menos que él mismo.

¡por lo tanto podemos pensar que ffff es como un "-1"!

para obtener otros "números negativos", observemos lo siguiente: si restamos 1 a ffff, obtenemos fffe. ¿qué ocurre si lo sumamos a 2?

   0000 0000 0000 0010: 0002
 + 1111 1111 1111 1110: fffe
 ---------------------
 1 0000 0000 0000 0000: 0000

¡obtenemos 0! ¡fffe funciona efectivamente como "-2"!

podríamos continuar así obteniendo más y más números "negativos" que funcionan gracias al tamaño restringido de la memoria del ordenador.

volviendo a nuestro código, si iniciamos nuestra velocidad con:

#ffff .pelota/vel-x STZ2

y luego usamos exactamente el mismo código para actualizar la posición:

( dentro de actualiza-pelota )
.pelota/vel-x LDZ2 ( obtener vel-x )
.pelota/x LDZ2 ( obtener x )
ADD2 ( sumar ambas )
.pelota/x STZ2 ( almacenar nueva x )

¡habremos disminuido la posición por 1!

podría tener sentido establecer estos valores como macros:

%PELOTA-VEL-POS { #0001 } ( +1 )
%PELOTA-VEL-NEG { #ffff } ( -1 )

implementando el movimiento de la pelota

basándonos en lo que acabamos de discutir, podemos empezar nuestra subrutina actualiza-pelota con lo siguiente:

@actualiza-pelota ( -- )
    ( obtener velocidad-x y `x` )
    .pelota/vel-x LDZ2 .pelota/x LDZ2
    ADD2 ( sumar ambas cosas )
    .pelota/x STZ2 ( guarda la nueva x )

    ( obtener velocidad-y e `y` )
    .pelota/vel-y LDZ2 .pelota/y LDZ2
    ADD2 ( sumar ambas )
    .pelota/y STZ2 ( almacenar nueva y )
RTN

si complementamos nuestra rutina de configuración con las velocidades iniciales, podremos ver cómo se mueve la pelota:

( dentro de configuración )
( iniciar pelota )
.Pantalla/ancho DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/x STZ2
.Pantalla/alto DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/y STZ2

( iniciar la velocidad de la pelota )
PELOTA-VEL-POS .pelota/vel-x STZ2
PELOTA-VEL-POS .pelota/vel-y STZ2

woohoo! se mueve, pero de momento sale volando :)

colisiones con las paredes

hemos definido la forma general de actualizar la posición de la pelota dada su velocidad en x e `y`.

¡ahora veamos cómo implementar el clásico "rebote"!

primero, empecemos con las paredes en la parte superior e inferior de la pantalla; recordando que hay un margen (MARGEN-PARED) entre el borde real de la pantalla, y las paredes.

para realizar estas detecciones de colisión, tendríamos que comprobar sólo la coordenada `y` de la pelota.

como siempre, hay muchas maneras de lograr esto. una podría ser:

otra:

en otros lenguajes probablemente sea más fácil escribir lo primero, pero aquí nos quedaremos con lo segundo: por claridad y por la forma en que tendremos que hacer las comprobaciones.

¡en cualquier caso, puede ser un buen ejercicio para ti intentar averiguar cómo "invertir" la velocidad con una sola operación aritmética o lógica!

pista: mira de nuevo las máscaras a nivel de bit discutidas en el tutorial de uxn día 3 :)

pared superior

si la pelota golpea la pared superior, significa que su coordenada `y` es menor que la coordenada `y` de la pared.

considerando que hay un margen en la parte superior, podemos hacer esta comprobación de la siguiente manera:

( dentro de actualiza-pelota )
 &verif-pared-sup
    .pelota/y LDZ2
    MARGEN-PARED
    LTH2 ( ¿es pelota-y menos que el margen? )
    ,&establecer-vel-pos JCN
    ,&verif-pared-inf JMP

    &establecer-vel-pos
        PELOTA-VEL-POS .pelota/vel-y STZ2
        &continuar JMP
 &verif-pared-inf

pared inferior

aquí el procedimiento sería similar, pero considerando el tamaño de la pelota.

queremos saber si la coordenada `y`, más el tamaño de la pelota, es mayor que la coordenada `y` de la pared inferior.

la coordenada `y` de la pared inferior sería la altura de la pantalla, menos el margen de la pared:

(dentro de actualiza-pelota )
 &verif-pared-inf
    .pelota/y LDZ2 TAM-PELOTA ADD2 ( y + tamaño de la pelota )
    .Pantalla/alto DEI2 
    MARGEN-PARED SUB2 ( altura - margen )
    GTH2 ( ¿es la pelota-y mayor que la pared-y? )
    ,&establecer-vel-neg JCN
    &continuar JMP
    
    &establecer-vel-neg 
        PELOTA-VEL-NEG .pelota/vel-y STZ2
 &continuar

actualiza-pelota hasta ahora

nuestra subrutina actualiza-pelota tiene el siguiente aspecto hasta el momento:

@actualiza-pelota ( -- )
    ( actualizar x )
    ( obtener velocidad-x y x )
    .pelota/vel-x LDZ2 .pelota/x LDZ2
    ADD2 ( sumar ambas cosas )
    .pelota/x STZ2 ( guardar la nueva x )

    ( actualizar y )
    ( obtener velocidad-y e `y` )
    .pelota/vel-y LDZ2 .pelota/y LDZ2
     ADD2 ( sumar ambas cosas )
    .pelota/y STZ2 ( almacenar nueva y )

    ( comprueba las colisiones con las paredes )
    &verif-pared-sup
        .pelota/y LDZ2
        MARGEN-PARED
        LTH2 ( ¿es la pelota-y menor que el margen? )
        ,&establecer-vel-pos JCN
        ,&verif-pared-inf JMP
    
        &establecer-vel-pos
            PELOTA-VEL-POS .pelota/vel-y STZ2
            &continuar JMP
    
    &verif-pared-inf
        .pelota/y LDZ2 TAM-PELOTA ADD2 ( y + tamaño de la pelota )
        .Pantalla/alto DEI2 
        MARGEN-PARED SUB2 ( altura - margen )
        GTH2 ( ¿es la pelota-y mayor que la pared-y? )
        ,&establecer-vel-neg JCN
        &continuar JMP
        
        &establecer-vel-neg
            PELOTA-VEL-NEG .pelota/vel-y STZ2
     &continuar
RTN

puedes probarlo usando diferentes velocidades iniciales dentro de la configuración. ¡la pelota debería estar rebotando en la parte superior e inferior ahora! :)

colisiones con las palas

¡trabajemos con lo que acabamos de hacer, y adaptémoslo para rebotar con las palas!

pala izquierda

en primer lugar, podemos identificar si la coordenada x de la pelota estaría golpeando la pala izquierda.

para ello, podemos comprobar si x es menor que la suma del margen y el ancho de la pala.

( dentro de actualiza-pelota )
 &verif-pala-izq
    .pelota/x LDZ2
    MARGEN ANCHO-PALA ADD2
    LTH2 ( ¿es la pelota-x menor que el margen + el ancho-de-pala? )
    ,&x-en-izquierda JCN
    ,&verif-pala-der JMP

    &x-en-izquierda
        ( ... )

 &verif-pala-der

una vez que sabemos que eso es cierto, podemos ver si la pelota está dentro del alcance vertical de la pala; la coordenada `y` de la pelota tiene que estar dentro de un cierto rango relativo a la coordenada `y` de la pelota.

en especifico, si queremos que la pelota pueda rebotar cuando cualquier parte de la pelota golpee cualquier parte de la pala, la coordenada `y` de la pelota tiene que ser:

si esas dos condiciones se cumplen, entonces podemos establecer una velocidad positiva para x:

( dentro de actualiza-pelota )
 &x-en-izquierda
    .pelota/y LDZ2 DUP2 
    .izquierda/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
    .izquierda/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera ) 
    STHr ( recupera la primera bandera ) 
    AND ( hacer AND en ambas banderas )
    &rebote-izquierda JCN

donde rebote-izquierda sería:

 &rebote-izquierda
    PELOTA-VEL-POS .pelota/vel-x STZ2
    ,&fin JMP

¿y qué pasa si no se cumplen las dos condiciones a la vez?

podemos dejar que la pelota siga moviéndose, pero comprobando que no ha cruzado la pared izquierda, comparando con 0000.

todo el código x-en-izquierda terminaría pareciendo:

( dentro de actualiza-pelota )
    &x-en-izquierda
        .pelota/y LDZ2 DUP2 
        .izquierda/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
        .izquierda/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera ) 
        STHr ( recupera la primera bandera ) 
        AND ( hacer AND en ambas banderas )
        ,&rebote-izquierda JCN

        .pelota/x LDZ2 #0000 NEQ2 ( ¿ha llegado a la pared? )
              ,&fin JCN

         &reset-izquierda
              ( aquí puedes aumentar la puntuación de
                la pala derecha )
              ;reset JSR2
              ,&fin JMP

         &rebote-izquierda
              PELOTA-VEL-POS .pelota/vel-x STZ2
              ,&fin JMP

 &verif-pala-der

"fin" sería una etiqueta al final de la subrutina, y "reset" es una subrutina de la cuálf hablaremos más adelante.

esta aproximación de comparar con 0000 es la más fácil, pero ten en cuenta que podría no funcionar si cambias la velocidad de la pelota: podría ocurrir que cruzara la pared pero con una coordenada x que nunca fuera igual a 0.

realmente no podemos comprobar si la coordenada x es menor que 0, porque como hemos comentado anteriormente, eso sería en realidad un número cercano a ffff.

si comprobáramos que la coordenada x es menor que ffff, ¡entonces cada valor posible activaría la bandera de comparación!

este puede ser otro buen ejercicio para ti: ¿cómo comprobarías si la pelota ha cruzado la pared izquierda independientemente de su velocidad?

pala derecha

para la pala derecha haremos lo mismo que arriba, pero cambiando las comparaciones relativas a la coordenada x de la pelota: usaremos el ancho de la pantalla como referencia para la pared derecha, y a partir de ahí le restaremos el margen y el ancho.

 &verif-pala-der
    .pelota/x LDZ2 TAM-PELOTA ADD2 ( pelota-x + tamaño-pelota )
    .Pantalla/ancho DEI2 MARGEN SUB2 ANCHO-PALA SUB2
    ( ¿es la coordenada derecha de la pelota mayor que el ancho de la pantalla - margen - ancho-pala? )
    GTH2
    ,&x-en-derecha JCN
    &fin JMP

    &x-en-derecha
        .pelota/y LDZ2 DUP2 
        .derecha/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
        .derecha/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera ) 
        STHr ( recupera la primera bandera ) 
        AND ( hacer AND en ambas banderas )
        ,&rebote-derecha JCN

        .pelota/x LDZ2 
        .Pantalla/ancho DEI2 NEQ2 ( ¿ha llegado a la pared? )
              ,&fin JCN 

         &reset-derecha
              ( aquí puedes aumentar la puntuación 
                de la pala izquierda )
              ;reset JSR2
              ,&fin JMP

         &rebote-derecha
              PELOTA-VEL-NEG .pelota/vel-x STZ2
              ,&fin JMP

 &fin
RTN

¡eso debería ser todo! ¡puedes encontrar la subrutina actualiza-pelota completa a continuación!

¡para poder ensamblar y ejecutar el juego, vamos a definir la subrutina reset!

reset

aquí solo definiremos una subrutina de reinicio o "reset" que devuelva la pelota al centro de la pantalla sin alterar su velocidad:

@reset ( -- )
    ( iniciar pelota )
    .Pantalla/ancho DEI2 TAM-PELOTA SUB2
    MITAD2 .pelota/x STZ2
    .Pantalla/alto DEI2 TAM-PELOTA SUB2
    MITAD2 .pelota/y STZ2
RTN

sería interesante tener algún mecanismo para también cambiar la velocidad: tal vez basado en el cuentafotogramas, en la posición de las palas, o cualquier otra cosa que elijas.

el programa completo

aquí está todo el código que hemos escrito hoy:

animación que muestra el pong en acción: las palas se mueven, la pelota rebota en las paredes superior e inferior y en las palas, y la pelota se reinicia desde el centro cuando la pelota golpea cualquier lado.

configuración

( hola-pong.tal )

( dispositivos )
|00 @Sistema [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Pantalla [ &vector $2 &ancho $2 &alto $2 &auto $1 &pad $1 
               &x $2 &y $2 &direc $2 &pixel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &boton $1 &tecla $1 ]

( macros )
%RTN { JMP2r }
%MITAD2 { #01 SFT2 } ( corto -- corto/2 )
%DOBLE2 { #10 SFT2 }

( constantes )
%ANCHO-PALA { #0010 } ( 2 tiles )
%ALTO-PALA { #0018 } ( 3 tiles )
%COLOR-PALA { #c5 } 
%VEL-PALA { #0001 }
%TAM-PELOTA { #0010 } ( 2 tiles )
%COLOR-PALA { #c5 } 
%PELOTA-VEL-POS { #0001 }
%PELOTA-VEL-NEG { #ffff }
%COLOR-BORRAR { #40 }
%MARGEN { #0010 } ( izquierda y derecha )
%MARGEN-PARED { #0010 } ( arriba y abajo )

( página cero )
|0000
    @izquierda [ &x $2 &y $2 ]
    @derecha [ &x $2 &y $2 ]
    @pelota [ &x $2 &y $2 &vel-x $2 &vel-y $2 ]

( configuración )
|0100
@configuracion ( -> )
    ( establecer los colores del sistema )
    #2ce9 .Sistema/r DEO2
    #01c0 .Sistema/g DEO2
    #2ce5 .Sistema/b DEO2

    ( establecer el vector de la pantalla )
    ;en-cuadro .Pantalla/vector DEO2

    ( dibujar fondo )
    ;dibuja-fondo JSR2 

    ( iniciar palas )
    MARGEN .izquierda/x STZ2
    .Pantalla/ancho DEI2
    MARGEN SUB2 ANCHO-PALA SUB2
    .derecha/x STZ2

    .Pantalla/alto DEI2 ALTO-PALA SUB2
    MITAD2 DUP2
    .izquierda/y STZ2
    .derecha/y STZ2

    ( iniciar pelota )
    ;reset JSR2

    ( iniciar velocidad de la pelota )
    PELOTA-VEL-NEG .pelota/vel-x STZ2
    PELOTA-VEL-POS .pelota/vel-y STZ2

BRK

en-cuadro

@en-cuadro ( -> )
    ( borrar palas )
    .izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
    .derecha/x LDZ2 .derecha/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2

    ( borrar pelota )
    COLOR-BORRAR ;dibuja-pelota JSR2

    ( actualizar palas )
    ;actualiza-palas JSR2 

    ( actualizar pelota )
    ;actualiza-pelota JSR2 

    ( dibujar palas )
    .izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
    .derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2

    ( dibujar pelota )
    COLOR-PELOTA ;dibuja-pelota JSR2
BRK

reset

@reset ( -- )
    ( iniciar pelota )
    .Pantalla/ancho DEI2 TAM-PELOTA SUB2
    MITAD2 .pelota/x STZ2
    .Pantalla/alto DEI2 TAM-PELOTA SUB2
    MITAD2 .pelota/y STZ2
RTN

relacionado con la pelota

actualiza-pelota

@actualiza-pelota ( -- )
    ( obtener velocidad-x y x )
    .pelota/vel-x LDZ2 .pelota/x LDZ2 ( obtener x )
    ADD2 ( sumar ambas cosas )
    .pelota/x STZ2 ( guardar la nueva x )

    ( obtener velocidad-y e `y` )
    .pelota/vel-y LDZ2 .pelota/y LDZ2 ( obtener y )
    ADD2 ( sumar ambas cosas )
    .pelota/y STZ2 ( guardar la nueva y )

    ( comprobar colisiones con las paredes )
    &verif-pared-sup
        .pelota/y LDZ2
        MARGEN-PARED
        LTH2 ( ¿es la pelota-y menor que el margen? )
        ,&establecer-vel-pos JCN
        ,&verif-pared-inf JMP
    
        &establecer-vel-pos
            PELOTA-VEL-POS .pelota/vel-y STZ2
            ,&continuar JMP
    
    &verif-pared-inf
        .pelota/y LDZ2 TAM-PELOTA ADD2 ( y + tamaño-pelota )
        .Pantalla/alto DEI2 MARGEN-PARED SUB2 ( altura - margen )
        GTH2
        ,&establecer-vel-neg JCN
        ,&continuar JMP
        
        &establecer-vel-neg 
            PELOTA-VEL-NEG .pelota/vel-y STZ2
    &continuar

    ( comprobar colisiones con las palas )
    &verif-pala-izq
        .pelota/x LDZ2
         MARGEN ANCHO-PALA ADD2
        LTH2 ( ¿es la pelota-x menor que el margen + ancho-pala? )
        ,&x-en-izquierda JCN
        ,&verif-pala-der JMP
    
        &x-en-izquierda
            .pelota/y LDZ2 DUP2 
            .izquierda/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
            .izquierda/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera ) 
            STHr ( recuperar la primera bandera ) 
            AND ( hacer AND en ambas banderas )
            ,&rebote-izquierda JCN

            .pelota/x LDZ2 #0000 NEQ2 ( ¿ha llegado a la pared? )
                  ,&fin JCN 

             &reset-izquierda
                  ( aquí puedes añadir un punto a la pala derecha )
                  ;reset JSR2
                  ,&fin JMP

             &rebote-izquierda
                  PELOTA-VEL-POS .pelota/vel-x STZ2
                  ,&fin JMP

     &verif-pala-der
        .pelota/x LDZ2 TAM-PELOTA ADD2
        .Pantalla/ancho DEI2 MARGEN SUB2 ANCHO-PALA SUB2
        ( ¿es pelota-x + tamaño-pelota mayor que la anchura de la pantalla - margen - ancho-pala? )
        GTH2
        ,&x-en-derecha JCN
        ,&fin JMP
    
        &x-en-derecha
            .pelota/y LDZ2 DUP2 
            .derecha/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
            .derecha/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera ) 
            STHr ( recuperar la primera bandera ) 
            AND ( hacer AND en ambas banderas )
            ,&rebote-derecha JCN

            .pelota/x LDZ2 
            .Pantalla/ancho DEI2 NEQ2 ( ¿ha llegado a la pared? )
                  ,&fin JCN 

             &reset-derecha
                  ( aquí puedes añadir un punto a la pala izquierda )
                  ;reset JSR2
                  ,&fin JMP

             &rebote-derecha
                  PELOTA-VEL-NEG .pelota/vel-x STZ2
                  ,&fin JMP

     &fin
RTN

dibuja-pelota

@dibuja-pelota ( color -- )
    ( fijar x e `y` iniciales )
    .pelota/x LDZ2 .Pantalla/x DEO2
    .pelota/y LDZ2 .Pantalla/y DEO2

    ( dibujar tile 0 )
    ;pelota/sprite0 .Pantalla/direc DEO2
    ( el byte de color ya estaba en la pila )
    DUP .Pantalla/sprite DEO 

    ( mover a la derecha )
    .Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2

    ( dibujar tile 1 )
    ;pelota-sprite/sprite1 .Pantalla/direc DEO2
    DUP .Pantalla/sprite DEO 

    ( mover hacia abajo )
    .Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2

    ( dibujar tile 3 )
    ;pelota-sprite/tile3 .Pantalla/direc DEO2
    DUP .Pantalla/sprite DEO 

    ( mover a la izquierda )
    .Pantalla/x DEI2 #0008 SUB2 .Pantalla/x DEO2

    ( dibujar tile 2 )
    ;pelota-sprite/tile2 .Pantalla/direc DEO2
    .Pantalla/sprite DEO 
RTN

relacionado-a-palas

actualiza-palas

@actualiza-palas ( -- )
    &izquierda
       ( pala izquierda: botones arriba 10 y abajo 20 )
       .Controlador/boton DEI
       DUP #10 AND ( comprobar bit para arriba )
       ,&izquierda-arriba JCN
       DUP #20 AND ( comprobar bit para abajo ) 
       ,&izquierda-abajo JCN
    
    ,&derecha JMP ( salta si no se ha pulsado ninguno de los dos )
  
    &izquierda-arriba
       .izquierda/y LDZ2 VEL-PALA SUB2 .izquierda/y STZ2 
       ,&derecha JMP 
    &izquierda-abajo
       .izquierda/y LDZ2 VEL-PALA ADD2 .izquierda/y STZ2 
       ,&derecha JMP 

    &derecha
       ( pala derecha: botones ctrl/A 01 y alt/B 02 )
       DUP #01 AND ( comprobar bit para A )
       ,&derecha-arriba JCN
       DUP #02 AND ( comprobar bit para B ) 
       ,&derecha-abajo JCN
    
    ,&fin JMP ( salta si no se ha pulsado ninguno de los dos )
  
    &derecha-arriba
       .derecha/y LDZ2 VEL-PALA SUB2 .derecha/y STZ2 
       ,&fin JMP 
    &derecha-abajo
       .derecha/y LDZ2 VEL-PALA ADD2 .derecha/y STZ2 

    &fin
        POP ( hacer POP al valor duplicado del botón )
RTN

dibuja-pala

@dibuja-pala ( x^ y^ color -- )
    ( guardar color )
    STH

    ( establecer `y` y x iniciales )
    .Pantalla/y DEO2 
    .Pantalla/x DEO2 

    ( dibujar tile 0 )
    ;pala-sprite/tile0 .Pantalla/direc DEO2
    ( copiar color de la pila de retorno: )
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a x: )
    .Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2

    ( dibujar tile 1 )
    ;pala-sprite/tile1 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a y: )
    .Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2

    ( dibujar tile 3 )
    ;pala-sprite/tile3 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO
    
    ( restar 8 a x: )
    .Pantalla/x DEI2 #0008 SUB2 .Pantalla/x DEO2

    ( dibujar tile 2 )
    ;pala-sprite/tile2 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a y: )
    .Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2

    ( dibujar tile 4 )
    ;pala-sprite/tile4 .Pantalla/direc DEO2
    STHkr .Pantalla/sprite DEO

    ( añadir 8 a x: )
    .Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2

    ( dibujar tile 5 )
    ;pala-sprite/tile5 .Pantalla/direc DEO2
    ( obtener y no mantener el color de la pila de retorno: )
    STHr .Pantalla/sprite DEO
RTN

dibuja-fondo

@dibuja-fondo ( -- )
    ;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
    
    .Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
    MARGEN-PARED ( establecer `y` inicial )
     &bucle-y
        DUP2 .Pantalla/y DEO2 ( establecer coordenada `y` )
    
        ( dibujar fila )
        .Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
         &bucle-x
            DUP2 .Pantalla/x DEO2 ( fijar coordenada x )
            #03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
            #0008 ADD2 ( incrementar x )
            GTH2k ( ¿es la anchura mayor que x? o también, ¿es x menor que la anchura? )
            ,&bucle-x JCN ( salta si x es menor que el límite )
        POP2 POP2 ( eliminar `x` y el límite )
    
        #0008 ADD2 ( incrementar y )
        GTH2k ( ¿es el límite mayor que `y`? o también, ¿es `y` menor que el límite? )
        ,&bucle-y JCN ( salta si `y` es menor que el límite )
    POP2 POP2 ( eliminar `y` y el límite )
RTN

data

@tile-fondo 1122 4488 1122 4488

@pala-sprite
 &tile0 [ 3f 7f e7 c3 c3 c3 c3 c3  00 00 18 3c 3c 3c 3c 3c ]
 &tile1 [ fc fe ff ff ff ff ff ff  00 00 00 00 00 00 06 06 ]
 &tile2 [ c3 c3 c3 c3 e7 ff ff ff  3c 3c 3c 3c 18 00 00 00 ]
 &tile3 [ ff ff ff ff ff ff ff ff  06 06 06 06 06 06 06 06 ]
 &tile4 [ ff ff ff ff ff ff 7f 3f  00 00 00 00 00 00 00 00 ]
 &tile5 [ ff ff ff ff ff ff fe fc  06 06 06 06 06 1e 3c 00 ]

@pelota-sprite
 &tile0 [ 03 0f 1f 39 70 70 f9 ff  00 00 00 06 0f 0f 06 00 ]
 &tile1 [ c0 f0 f8 fc fe fe ff ff  00 00 00 00 08 0c 06 06 ]
 &tile2 [ ff ff 7f 7f 3f 1f 0f 03  00 00 00 00 18 0f 01 00 ]
 &tile3 [ ff ff fe fe fc f8 f0 c0  06 06 0c 1c 38 f0 c0 00 ]

¡wiuf! :)

más posibilidades

aquí hay algunas posibilidades extra para que practiques y trates de implementar:

¡comparte lo que termines creando en base a todo esto! :)

día 7

en el tutorial de uxn día 7 hablamos de los dispositivos del ordenador varvara que aún no hemos cubierto: audio, archivo y fechahora o "datetime".

este debería ser un final ligero y tranquilo de nuestro recorrido, ya que tiene que ver menos con la lógica de programación y más con las convenciones de entrada y salida en estos dispositivos.

¡primero, te invito a tomar un descanso!

después, ¡sigue explorando y comparte tus descubrimientos!

apoyo

si te ha gustado este tutorial y te ha resultado útil, considera compartirlo y darle tu apoyo :)

incoming links

tutorial de uxn

tutorial de uxn día 7

tutorial de uxn apéndice a

tutorial de uxn día 5