icono de compudanzas

tutorial uxn: día 4, variables y bucle de animación

¡esta es la cuarta sección del tutorial de uxn!

aquí hablamos del bucle de animación del ordenador varvara, a través de su vector de dispositivo de pantalla.

también hablamos del uso de la memoria del programa como un espacio para datos usando "variables". esto nos permite guardar y recuperar datos durante el tiempo de ejecución de nuestros programas, y puede ahorrarnos manejos complejos en la pila :)

el vector pantalla

discutimos el dispositivo de pantalla de varvara en el día 2, pero nos saltamos su puerto vectorial para centrarnos en cómo dibujar con él:

|20 @Pantalla [ &vector $2 &ancho $2 &alto $2 &pad $2 &x $2 &y $2 &direc $2 &pixel $1 &sprite $1 ]

ahora que ya tenemos el concepto de vectores de dispositivos en el tutorial de uxn día 3, ¡vamos a entrar de lleno en cómo usar el de la pantalla!

asignación

la siguiente línea de uxntal asignaría la dirección absoluta de la etiqueta en-cuadro al vector pantalla:

;en-cuadro .Pantalla/vector DEO2

uxn saltará a la ubicación de la etiqueta a una frecuencia de 60 veces por segundo: podemos utilizar la subrutina bajo en-cuadro para cambiar el contenido de la pantalla, generando animación, y/o también podemos utilizarla para otros propósitos relacionados con la temporización.

una línea que crece

el siguiente programa demuestra un uso básico pero potente del vector pantalla: en cada fotograma, dibuja un píxel en las coordenadas x,y de la pantalla dadas, y añade 1 al valor de la coordenada x:

( hola-linea.tal )

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

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

      ( establecer coordenadas iniciales x,y )
      #0008 .Pantalla/x DEO2
      #0008 .Pantalla/y DEO2

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

@en-cuadro ( -> )
      ( dibujar un pixel en el fondo con el color 1 )
      #01 .Pantalla/pixel DEO

      ( incrementar Pantalla/x )
      .Pantalla/x DEI2 INC2 .Pantalla/x DEO2
BRK

nota que el código es muy similar al que escribimos el día 2 para dibujar una línea.

en ese, incrementamos manualmente el valor de Pantalla/x para dibujar 6 píxeles.

aquí, el código para incrementar Pantalla/x es llamado dentro de la subrutina en-cuadro en su lugar, haciendo que ocurra 60 veces por segundo.

posibilidades de crecimiento

estos son algunos cambios para que los pruebes y practiques:

variables

¡el vector de pantalla varvara abre todo un mundo de posibilidades!

merece señalar que muchas de estas posibilidades requieren formas de almacenar y recuperar datos entre fotogramas.

en el ejemplo anterior, estamos usando los puertos de pantalla para las coordenadas x e `y` como una forma de almacenar las coordenadas del pixel.

pero, ¿qué sucede cuando queremos dibujar diferentes objetos, cada uno con su propio conjunto de coordenadas y otras características que pueden cambiar con el tiempo?

¡podemos utilizar etiquetas en la memoria del programa para conseguirlo!

variables con direcciones absolutas

en cierto modo, al almacenar los datos de nuestros sprites ya hemos hecho algo así.

hemos etiquetado una sección de memoria con contenidos que no son instrucciones para uxn, sino datos; por ejemplo:

@cuadrado ff81 8181 8181 81ff

sin embargo, no hemos utilizado los datos directamente; hemos enviado su dirección al puerto del dispositivo de pantalla correspondiente.

etiquetas

podríamos utilizar un sistema similar para almacenar, por ejemplo, las coordenadas x e `y` en lugar de los datos del sprite:

@pixel-x 0008
@pixel-y 0008

o si no quisiéramos iniciarlas aquí, podríamos definirlas de la siguiente manera:

@pixel-x $2
@pixel-y $2

recuerda que $2 crea un pad relativo de dos bytes: esto hace que píxel-y sea una etiqueta para una dirección en memoria dos bytes después de pixel-x. y cualquier código posterior ocurrirá dos bytes después de píxel-y.

también podríamos usar etiquetas y sub-etiquetas, de manera muy similar a como definimos los dispositivos y sus puertos:

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

instrucciones: LDA y STA

¿cómo podríamos leer (cargar) y escribir (almacenar) el contenido de la memoria en esas etiquetas?

aquí están las dos instrucciones que nos ayudarían:

como ya hemos comentado, una dirección absoluta siempre tendrá una longitud de dos bytes.

en el modo corto, LDA2 cargará un corto desde la dirección dada, y STA2 almacenará un corto en la dirección dada.

ejemplos

como ejemplo, el siguiente código leería los dos bytes de pixel/x, los incrementaría en uno, y los almacenaría de nuevo en pixel/x:

;pixel/x LDA2 ( cargar pixel/x en la pila )
INC2 ( incrementar )
;pixel/x STA2 ( almacenar el resultado en pixel/x )

BRK

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

nótese el uso de BRK antes de la etiqueta del píxel para que uxn se detenga antes de leer los datos como instrucciones.

lo siguiente es una variación que también duplica el nuevo valor de pixel/x para enviarlo a Pantalla/x:

;pixel/x LDA2 ( cargar pixel/x en la pila )
INC2 ( incrementar )
DUP2 ( duplicar el resultado )
;pantalla/x DEO2 ( establecer como pantalla/x )
;pixel/x STA2 ( y guardar el resultado en pixel/x )

BRK

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

nótese que podríamos haber conseguido el mismo resultado almacenando el resultado, y luego recargándolo y enviándolo como salida.

aquí podemos ver cómo un DUP2 puede facilitar esa operación, siempre y cuando mantengamos un modelo mental (¡o tangible!) de lo que ocurre en la pila.

valores iniciales

una posible ventaja de utilizar direcciones absolutas es que podemos iniciar el contenido de nuestras variables en el momento de ensamblaje, por ejemplo:

@pixel [ &x 0008 &y 0008 ]

estos contenidos iniciales cambiarán cada vez que usemos una instrucción STA allí :)

variables en la página cero

las variables con dirección absoluta funcionan bien para los casos en los que queremos poder acceder a su contenido desde cualquier parte de nuestro programa (es decir, "variables globales").

sin embargo, uxn tiene un mecanismo mejor para esos casos: ¡la página cero!

como recordarás, la página cero consiste en las primeras 256 direcciones de la memoria del programa. normalmente, un programa comienza en la dirección 0100, que es la siguiente dirección después de la página cero.

podemos referirnos a cualquiera de las 256 direcciones de la página cero utilizando un solo byte, en lugar de los dos bytes que se necesitan para las direcciones absolutas.

algo importante a tener en cuenta es que el contenido de la página cero no está presente en las roms uxn.

esto significa que una salvedad de usar variables allí, es que para iniciarlas necesitamos hacerlo durante el tiempo de ejecución, almacenando valores de la pila en ellas.

etiquetas en la página cero

las etiquetas para la página cero funcionarían igual que antes; sólo tenemos que especificar que están en la página cero con una almohadilla absoluta:

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

para referirnos a ellas, utilizaríamos la runa punto (.) para las direcciones literales de página cero, en lugar de la runa dos puntos (;) para las direcciones literales absolutas.

instrucciones: LDZ, STZ

las instrucciones para cargar (leer) y almacenar (escribir) desde y hacia la página cero son:

en estas instrucciones, la dirección siempre será de un byte.

en el modo corto, LDZ2 cargará un corto desde la dirección dada, y STZ2 almacenará un corto en la dirección dada.

ejemplos

el siguiente ejemplo consiste en la misma línea que crece, pero ahora utilizando la página cero para almacenar las coordenadas x e `y` del píxel en lugar de los puertos x e `y` de la pantalla.

en este caso el programa es más largo, pero puede ser visto como una buena plantilla para tener otras líneas que se comporten de diferentes maneras:

( hola-linea.tal )

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

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

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

      ( establecer coordenadas iniciales x,y )
      #0008 .pixel/x STZ2
      #0008 .pixel/y STZ2
      
      ( establecer el vector de pantalla )
      ;en-cuadro .Pantalla/vector DEO2
BRK

@en-cuadro ( -> )
      ( cargar las coordenadas x,y de la página cero y enviarlas a la pantalla )
      .pixel/x LDZ2 .Pantalla/x DEO2
      .pixel/y LDZ2 .Pantalla/y DEO2

      ( dibujar un pixel en el fondo con color 1 )
      #01 .Pantalla/pixel DEO

      ( incrementar el pixel/x )
      .pixel/x LDZ2 INC2 .pixel/x STZ2
BRK

notemos el uso de la runa literal de dirección de página cero (.) para referirse a la etiqueta .pixel.

además, observe que en el caso de .pixel la dirección se refiere a la página cero, a la que se accede con LDZ/STZ, y en el caso de .Pantalla la dirección se refiere al espacio de direcciones entrada/salida (i/o), al que se accede con DEI/DEO.

un poco de práctica de manipulación en la pila

nota que las siguientes instrucciones también incrementarían .pixel/x, pero estableciendo su dirección sólo una vez:

.pixel/x DUP LDZ2 INC2 ROT STZ2

te recomiendo que sigas cómo cambia el contenido de la pila en cada uno de los siguientes pasos: ten en cuenta que algunas de las instrucciones están en modo corto :)

esta línea de código contiene la misma cantidad de bytes que la anterior.

una posible desventaja es que podría ser menos legible. pero una posible ventaja es que podría convertirse en una macro:

( incrementar un corto desde la página cero )
%PC-INC2 { DUP LDZ2 INC2 ROT STZ2 } ( pc-dirección -- )

variables en direcciones relativas

otra posibilidad que tenemos en uxn y que podría ser más apropiada para las "variables locales", consiste en utilizar direcciones relativas.

de forma similar a las variables de la página cero, para direccionar estas variables sólo necesitamos un byte.

sin embargo, como estas direcciones se dan como desfases ("offsets") relativos y pueden considerarse positivas y negativas, sólo se pueden alcanzar si están dentro de los 256 bytes que rodean a la instrucción que las carga o almacena.

instrucciones: LDR, STR

las instrucciones para trabajar de esta manera son:

similar a LDZ y STZ, en estas instrucciones la dirección siempre será de un byte.

en el modo corto, LDR2 cargará un corto desde la dirección dada, y STR2 almacenará un corto en la dirección dada.

ejemplos

lo siguiente es la subrutina en-cuadro que dibuja la línea creciente, pero almacenando las coordenadas de los píxeles en una variable "local" a la que se accede mediante LDR y STR.

@en-cuadro ( -> )
      ( carga las coordenadas x,y desde la página cero y las envía a la pantalla )
      ,pixel/x LDR2 .Pantalla/x DEO2
      ,pixel/y LDR2 .Pantalla/y DEO2

      ( dibujar un pixel en el fondo con el color 1 )
      #01 .Pantalla/pixel DEO

      ( incrementa pixel/x )
       ,pixel/x LDR2 INC2 ,pixel/x STR2 
BRK

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

nótese el uso de la runa coma (,) para indicar que es una dirección relativa; uxnasm calcula el desfase requerido asumiendo que será utilizado en la siguiente instrucción.

en este caso realmente no podemos duplicar ese desfase como hicimos anteriormente con la dirección de página cero, porque es específica de la posición en el código en que fue escrita.

si declaráramos estas variables como sub-etiquetas de en-cuadro, el código quedaría como sigue:

@en-cuadro ( -> )
      ( carga las coordenadas x,y de la página cero y las envía a la pantalla )
      ,&pixel-x LDR2 .Pantalla/x DEO2
      ,&pixel-y LDR2 .Pantalla/y DEO2

      ( dibujar un píxel en el fondo con el color 1 )
      #01 .Pantalla/pixel DEO

      ( incrementa píxel/x )
       ,&pixel-x LDR2 INC2 ,&pixel-x STR2 
BRK
( variables locales de en-cuadro )
 &pixel-x $2 &pixel-y $2 

observe que en este caso, la runa de la coma (,) va acompañada de la runa de la sub-etiqueta (&).

el uso de este tipo de variables tendrá más sentido en el día 5 del tutorial :)

cambio de posición del sprite

el uso de "variables" nos ayudará ahora a discutir tres formas diferentes de animar un sprite:

los revisaremos por separado para mantener los ejemplos relativamente simples y legibles.

tenga en cuenta que estos ejemplos también sirven para discutir más posibilidades de programación uxntal, y pueden llegar a ser un poco abrumadores.

te recomiendo que revises y experimentes con uno a la vez, pacientemente :)

cambio de posición autónomo

ya discutimos como hacer que uxn cambie la posición de un pixel en la pantalla, dejando un rastro.

cambiar ese programa para dibujar un sprite de 8x8 en su lugar sería relativamente sencillo, y puede que ya lo hayas probado: tendríamos que usar Pantalla/sprite en lugar de Pantalla/pixel, con un byte apropiado para definir su color y orientación, y tendríamos que establecer la dirección de los datos de nuestro sprite en Pantalla/direc.

eso daría como resultado un sprite que se mueve y que además deja un rastro: ¡te invito a que lo pruebes primero!

sin rastro

ok, eso puede ser útil en algunos casos, pero, ¿cómo podemos evitar dejar el rastro?

una posible forma de conseguirlo sería siguiendo este orden de operaciones dentro de la subrutina en-cuadro:

esto nos permite borrar el sprite de su posición en el fotograma anterior, actualizar sus coordenadas a una nueva posición, y luego dibujarlo ahí.

código de ejemplo

el siguiente programa ilustra los puntos anteriores, haciendo que nuestro cuadrado del día 2 se desplace de izquierda a derecha en el centro de nuestra pantalla.

¡combina varias cosas que hemos cubierto en los últimos días!

( hola-sprite-animado.tal )

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

( macros/constantes )
%MITAD2 { #01 SFT2 } ( desplazar un bit a la derecha ) ( corto -- corto/2 )
%color-borrar { #40 } ( borrar 1bpp sprite del primer plano )
%color-2 { #4a } ( dibujar sprite de 1bpp con color 2 y transparencia )

( página cero )
|0000
@sprite [ &pos-x $2 &pos-y $2 ]

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

      ( fijar la Pantalla/y a la mitad de la pantalla, menos 4 )
      .Pantalla/alto DEI2 MITAD2 #0004 SUB2 .Pantalla/y DEO2

      ( fijar la dirección del sprite )
      ;cuadrado .Pantalla/direc DEO2

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

@en-cuadro ( -> )
      ( 1: borrar sprite )
      ( borrar sprite del primer plano )
      color-borrar .Pantalla/sprite DEO

      ( 2: cambiar posición )
      ( incrementar sprite/pos-x )
      .sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2 

      ( 3 : dibujar sprite )
      ( carga la coordenada x de la página cero y la envía a la pantalla )
      .sprite/pos-x LDZ2 .Pantalla/x DEO2

      ( dibujar sprite en el primer plano con color 2 y transparencia )
      color-2 .Pantalla/sprite DEO
BRK

( datos del sprite )
@cuadrado ff81 8181 8181 81ff

nítido, ¿no? :)

como esto es sólo un ejemplo para ilustrar un punto, hay algunas cosas que podrían ser optimizadas para hacer nuestro programa más pequeño, y hay algunas cosas que podrían ser útiles pero fueron omitidas. por ejemplo, no hay un valor inicial para la coordenada x, o la coordenada `y` no se utiliza.

posibilidades adicionales

con respecto a la optimización, y como un ejemplo, la sección 2 y la primera parte de la sección 3 de en-cuadro podrían haber sido escritas de la siguiente manera:

      ( 2: cambio de posición )
      ( incrementar sprite/pos-x )
      .sprite/pos-x LDZ2 INC2
      DUP2 ( duplicar resultado )
      .sprite/pos-x STZ2 ( almacenar la primera copia del resultado )

      ( 3 : dibujar sprite )
      ( usar la coordenada x de la pila y enviarla a la pantalla )
      .Pantalla/x DEO2

como siempre, depende de nosotros cómo queremos navegar entre un código más corto y la legibilidad :)

aquí hay algunas preguntas para que reflexiones y pruebes:

cambio de posición interactivo

cuando usamos el vector controlador, estamos actuando en base a un cambio en el/los botón/es o tecla/s que fueron presionados o soltados. esto puede ser muy útil para algunas aplicaciones.

pero, ¿cómo podemos tratar de hacer una acción continua cuando una tecla se mantiene presionada?

en algunos sistemas operativos, si mantenemos una tecla pulsada, ésta dispara el vector controlador varias veces, ¡pero no necesariamente al mismo ritmo que el vector pantalla!

¡esta repetición puede no permitir un movimiento suave como el que podemos conseguir si comprobamos el estado del controlador dentro de la subrutina en-cuadro!

cuadrado horizontalmente interactivo

el siguiente programa nos permite controlar la posición horizontal de nuestro cuadrado mediante las teclas de dirección.

animación que muestra un cuadrado moviéndose horizontalmente en la pantalla, aparentemente controlado por un humano.

¡nótese las similitudes entre el programa anterior, y lo que cubrimos en el tutorial de uxn día 3!

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

( macros/constantes )
%MITAD2 { #01 SFT2 } ( desplazar un bit a la derecha ) ( corto -- corto/2 )
%color-borrar { #40 } ( borrar 1bpp sprite del primer plano )
%color-2 { #4a } ( dibujar sprite de 1bpp con color 2 y transparencia )

( página cero )
|0000
@sprite [ &pos-x $2 &pos-y $2 ]

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

      ( fijar la Pantalla/y a la mitad de la pantalla, menos 4 )
      .Pantalla/alto DEI2 MITAD2 #0004 SUB2 .Pantalla/y DEO2

      ( fijar la dirección del sprite )
      ;cuadrado .Pantalla/direc DEO2

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

@en-cuadro ( -> )
      ( 1: borrar sprite )
      ( borrar sprite del primer plano)
      color-borrar .Pantalla/sprite DEO

      ( 2: cambiar de posición con las flechas )
      &verificar-flechas
        .Controlador/boton DEI
        #40 AND ( aislar el bit 6, correspondiente a la izquierda )
        ,&izquierda JCN ( saltar si no es 0 )

        .Controlador/boton DEI
        #80 AND ( aislar el bit 7, correspondiente a la derecha )
        ,&derecha JCN ( saltar si no es 0 )

      ( si no se ha pulsado ninguna de esas teclas, dibujar sin cambios )
      ,&dibujar JMP 

      &izquierda
      ( disminuir sprite/pos-x )
      .sprite/pos-x LDZ2 #0001 SUB2 .sprite/pos-x STZ2 
      ,&dibujar JMP

      &derecha
      ( incrementar sprite/pos-x )
      .sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2 

      ( 3 : dibujar sprite )
      &dibujar
      ( carga la coordenada x de la página cero y la envía a la pantalla )
      .sprite/pos-x LDZ2 .Pantalla/x DEO2

      ( dibujar sprite en primero plano con el color 2 y transparencia )
      color-2 .Pantalla/sprite DEO
BRK

( datos del sprite )
@cuadrado ff81 8181 8181 81ff

¡te invito a que adaptes el código para que puedas controlar el sprite en las cuatro direcciones cardinales!

moviéndose dentro de los límites

como habrás notado, estos dos programas anteriores permiten que nuestro sprite se salga de la pantalla.

si quisiéramos evitar eso, una forma de hacerlo sería añadiendo (más) condicionales.

límites condicionales

por ejemplo, en lugar de tener un incremento incondicional en la coordenada x:

( incremento sprite/pos-x )
.sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2 

podríamos comprobar primero si ha alcanzado una cantidad específica, como el ancho de la pantalla, y no incrementar si es el caso:

.sprite/pos-x LDZ2 ( carga x )
.Pantalla/ancho DEI2 #0008 SUB2 ( obtener el ancho de la pantalla menos 8 )
EQU2 ( ¿es x igual a la anchura de la pantalla - 8? )

 &incremento
      ( incrementar sprite/pos-x )
      .sprite/pos-x LDZ2 INC2 .sprite/pos-x STZ2 

 &continuar

módulo

otra posibilidad podría ser aplicar una operación de módulo a nuestras coordenadas cambiadas para que siempre se mantengan dentro de los límites, volviendo a la izquierda cuando se cruce con la derecha, y viceversa.

un posible conjunto de macros de módulo podría ser:

%MOD { DUP2 DIV MUL SUB } ( a b -- a%b )
%MOD2 { OVR2 OVR2 DIV2 MUL2 SUB2 } ( a b -- a%b )

(hay un conjunto más optimizado pero lo discutiremos más adelante :)

podemos aplicar esas macros después de incrementar o decrementar. por ejemplo:

( incrementar sprite/pos-x )
.sprite/pos-x LDZ2 INC2 
.Pantalla/ancho DEI2 MOD2 ( aplicar modulo de ancho de pantalla )
.sprite/pos-x STZ2 ( almacenar el resultado )

animación de sprite con fotogramas

otra estrategia de animación consistiría en cambiar el sprite que se dibuja en una posición determinada.

¡podrías tener una secuencia de sprites/fotogramas y animarlos ejecutándolos en secuencia!

los fotogramas

para efectos prácticos te recomendaría tener un número de fotogramas correspondiente a una potencia de dos, como 2, 4, 8, 16, 32, etc.

por ejemplo, lo siguiente es una secuencia de ocho sprites de 1bpp que corresponden a una línea diagonal que se mueve desde abajo a la derecha hasta arriba a la izquierda:

@animacion
  &fotograma0 00 00 00 00 00 00 01 03
  &fotograma1 00 00 00 00 01 03 06 0c
  &fotograma2 00 00 01 03 06 0c 18 30
  &fotograma3 01 03 06 0c 18 30 60 c0
  &fotograma4 03 06 0c 18 30 60 c0 80
  &fotograma5 0c 18 30 60 c0 80 00 00
  &fotograma6 30 60 c0 80 00 00 00 00
  &fotograma7 c0 80 00 00 00 00 00 00

nótese que cada fotograma consta de 8 bytes. eso implica que hay un desfase de 8 bytes entre las direcciones correspondientes a cada subetiqueta.

por ejemplo, la dirección de &fotograma1 sería 8 bytes más que la dirección de &fotograma0.

los fotogramas que utilizas también podrían estar compuestos por sprites de 2bpp. en ese caso, el desfase entre fotogramas sería de 16 en decimal (10 en hexadecimal) bytes.

conteo de fotogramas

para tener una animación compuesta por esos fotogramas necesitamos cambiar la dirección de Pantalla/direc a intervalos específicos para que apunte a un sprite diferente cada vez.

¿cómo podemos saber la dirección del sprite que debemos utilizar en cada fotograma?

una forma de conseguirlo es teniendo una "variable global" en la página cero que cuente los fotogramas del programa. además, tendríamos que tener ese conteo acotado en un rango correspondiente a la cantidad de fotogramas de nuestra animación.

¡ya sabemos cómo hacer la primera parte, y más o menos sabemos cómo hacer la segunda!

cargar, incrementar y almacenar la cuenta de fotogramas

en la página cero declaramos la etiqueta para nuestro cuentafotogramas.

( página cero )
|0000
@cuentaftg $1

y en la subrutina en-cuadro lo incrementamos:

( incrementar cuenta de fotograma )
.cuentaftg LDZ INC .cuentaftg STZ

ten en cuenta que estamos usando un solo byte para contar, por lo que pasará de 0 a 255 en poco más de 4 segundos, y luego se reiniciará cuando sobrepase su cuenta.

para algunas aplicaciones podría ser mejor tener un cuentafotograma en un corto, que contaría de 0 a 65535 y se sobrepasaría en un poco más de 18 minutos.

módulo rápido

para que ese recuento de fotogramas se limite a un rango correspondiente a nuestro número de fotogramas, podemos utilizar una operación de módulo.

cuando tenemos un número de fotogramas que corresponde a una potencia de dos, como se recomienda más arriba, podemos utilizar una "máscara AND" para realizar esta operación de módulo más rápidamente que si utilizáramos las macros MOD sugeridas anteriormente.

por ejemplo, si tenemos 8 fotogramas numerados del 0 al 7, podemos observar que esos números sólo requieren tres bits para ser representados.

para construir nuestra máscara AND, ponemos como 1 esos tres bits, y 0 los demás:

0000 0111: 07

esta máscara AND "dejará pasar" los tres bits menos significativos de otro byte, y desactivará los demás.

en uxntal este proceso se vería de la siguiente manera:

.cuentaftg LDZ ( cargar cuentafotograma )
#07 AND ( aplicar máscara AND, correspondiente al módulo 8 )

el resultado de la operación será un conteo que va repetidamente de 0 a 7.

podríamos definir esta operación de módulo rápido como una macro para hacer el código más legible:

%8MOD { #07 AND } ( byte -- byte%8 )

si esto no te ha quedado muy claro, te recomiendo que vuelvas a mirar el tutorial de uxn día 3, en particular la discusión de las operaciones lógicas.

aritmética de punteros

¿cómo podemos usar esa cuenta para seleccionar el sprite para el cuadro de animación que queremos mostrar?

podríamos usar varios saltos condicionales, o podríamos usar una forma más divertida que se puede llamar aritmética de punteros :)

observa que la subetiqueta para el primer fotograma (fotograma0) de nuestra animación tiene la misma dirección que la etiqueta para toda la animación. y, como ya mencionamos, el siguiente fotograma (fotograma1) comienza 8 bytes después.

la subetiqueta para cada fotograma siguiente está 8 bytes después de la anterior.

o, otra forma de verlo:

generalizando, ¡el fotogramaN esta (N veces 8) bytes después de la etiqueta de animación!

esto significa que si obtenemos la dirección absoluta de la etiqueta de animación, y le añadimos (N veces 8) bytes, obtendremos la dirección absoluta del fotogramaN :)

la cantidad de bytes que separa cada subetiqueta se llama "offset" o desfase.

calculando el desfase

después de aplicar el módulo 8 a nuestro cuentafotogramas podemos multiplicarlo por 8 para obtener el desfase respecto a la etiqueta de la animación:

.cuentaftg LDZ ( cargar cuentafotograma )
8MOD ( aplicar el módulo 8 para obtener la secuencia entre 0 y 7 )
#08 MUL ( multiplicar por 8 para obtener el desfase )

de byte a corto

nota que hasta ahora hemos estado trabajando con bytes, y todo ha ido bien.

sin embargo, ¡las direcciones absolutas son cortos!

esto significa que tenemos que convertir nuestro desfase en un corto para poder añadirlo a la dirección de los datos de la animación.

una forma de hacerlo es con esta macro que añade un 00 antes del elemento superior de la pila:

%A-CORTO { #00 SWP } ( byte -- corto )

nuestro código quedaría de la siguiente manera

.cuentaftg LDZ ( cargar cuentafotograma )
8MOD ( aplicar el módulo 8 para obtener la secuencia entre 0 y 7 )
#08 MUL ( multiplicar por 8 para obtener el desfase )
A-CORTO ( convertir a corto )

otra forma, menos clara pero bastante divertida (y algo más corta en memoria de programa), consistiría en empujar el 00 antes de que ocurra cualquier otra cosa:

#00 ( empujar el byte alto del desfase )
.cuentaftg LDZ ( cargar cuentafotograma )
8MOD ( aplicar el módulo 8 para obtener la secuencia entre 0 y 7 )
#08 MUL ( multiplicar por 8 para obtener el desfase )

añadiendo el desfase

añadir este desfase a la dirección de nuestra animación es comparativamente sencillo:

.cuentaftg LDZ ( cargar cuentafotogramas )
8MOD ( aplicar el módulo 8 para obtener la secuencia entre 0 y 7 )
#08 MUL ( multiplicar por 8 para obtener el desfase )
A-CORTO ( convertir a corto )
;animacion ( obtener la dirección de la animación )
ADD2 ( añadir el desfase a la dirección )

y entonces podríamos enviar eso al puerto Pantalla/direc:

.Pantalla/direc DEO2 ( establecer la dirección calculada )

el programa completo

el programa que hace todo esto tendría el siguiente aspecto.

nota que utiliza una secuencia similar a la de los programas anteriores:

la sección "borrar el sprite" no es realmente necesaria en este caso debido a los colores que se utilizan, pero lo sería cuando se utilizan colores con transparencia en ellos :)

animación de una franja diagonal dentro de un cuadrado pixelado. la diagonal se mueve desde abajo a la derecha hasta arriba a la izquierda
( hola-animacion.tal )

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

( macros/constantes )
%MITAD2 { #01 SFT2 } ( desplazar un bit a la derecha ) ( corto -- corto/2 )
%8MOD { #07 AND } ( byte -- byte%8 )
%A-CORTO { #00 SWP } ( byte -- corto )

%color-borrar { #40 } ( borrar sprite de 1bpp del primer plano )
%color-2-3 { #4e } ( dibujar el sprite de 1bpp con el color 2 y 3 )

( página cero )
|0000
@cuentaftg $1

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

      ( fijar Pantalla/x e `y` a la mitad de la pantalla, menos 4 )
      .Pantalla/ancho DEI2 MITAD2 #0004 SUB2 .Pantalla/x DEO2
      .Pantalla/alto DEI2 MITAD2 #0004 SUB2 .Pantalla/y DEO2

      ( establecer la dirección del sprite )
      ;animacion .Pantalla/direc DEO2

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

BRK

@en-cuadro ( -> )
      ( 0: incrementar el número de fotogramas )
      .cuentaftg LDZ INC .cuentaftg STZ

      ( 1: borrar sprite )
      ( borrar sprite del primer plano )
      color-clear .Pantalla/sprite DEO

      ( 2: actualizar la dirección del sprite )
      .cuentaftg LDZ ( cargar cuentafotograma )
      8MOD ( aplicar el módulo 8 para obtener la secuencia entre 0 y 7 )
      #08 MUL ( multiplicar por 8 para obtener el desfase )
      A-CORTO ( convertir a corto )
      ;animacion ( obtener la dirección de la animación )
      ADD2 ( añadir el desfase a la dirección )
      .Pantalla/direc DEO2 ( establecer la dirección calculada )

      ( dibujar sprite en el primer plano con el color 2 y 3 )
      color-2-3 .Pantalla/sprite DEO
BRK

( datos del sprite )
@animacion
  &fotograma0 00 00 00 00 00 00 01 03
  &fotograma1 00 00 00 00 01 03 06 0c
  &fotograma2 00 00 01 03 06 0c 18 30
  &fotograma3 01 03 06 0c 18 30 60 c0
  &fotograma4 03 06 0c 18 30 60 c0 80
  &fotograma5 0c 18 30 60 c0 80 00 00
  &fotograma6 30 60 c0 80 00 00 00 00
  &fotograma7 c0 80 00 00 00 00 00 00

no era tan complicado, ¿verdad? :) este ejemplo incluye muchos conceptos que merecen ser estudiados, ¡así que te invito a leerlo con atención!

para algunas posibilidad divertidas, ¡te invito a dibujar el tile varias veces en diferentes lugares y posiblemente con diferentes modos de rotación! ¡eso puede generar animaciones más interesantes!

o, mejor aún, ¡diseña y utiliza tus propios sprites!

¡más despacio!

hasta ahora, todo lo que hemos estado haciendo ha sucedido a 60 fotogramas por segundo, ¡eso puede ser demasiado rápido para algunas aplicaciones!

afortunadamente, podemos usar algo de aritmética simple con nuestro cuentafotograma para desacelerar sus efectos.

por ejemplo, si queremos actualizar nuestros fotogramas a la mitad de esa velocidad (30 fotogramas por segundo), podemos dividir entre dos el valor del cuentafotograma antes de aplicar el módulo.

como recordarás, esta división se puede hacer con SFT en el caso de potencias de dos, o con DIV para cualquier otro caso.

%MITAD { #01 SFT } ( byte -- byte/2 )
%CUARTO { #02 SFT } ( byte -- byte/4 )
%OCTAVO { #03 SFT } ( byte -- byte/8 )

podemos utilizar estas macros para dividir la frecuencia en nuestro código:

      ( 2: actualizar la dirección del sprite )
      .cuentaftg LDZ ( cargar cuentafotograma )
      CUARTO ( dividir entre 4 la frecuencia )
      8MOD ( aplicar el módulo 8 para obtener la secuencia entre 0 y 7 )
      #08 MUL ( multiplicar por 8 para obtener el desfase )
      A-CORTO ( convertir a corto )
      ;animacion ( obtener la dirección de la animación )
      ADD2 ( añadir el desfase a la dirección )
      .Pantalla/direc DEO2 ( establecer la dirección calculada )
animación de una franja diagonal dentro de un cuadrado pixelado. la diagonal se mueve desde abajo a la derecha hasta arriba a la izquierda. se mueve más lentamente que la anterior.

ah, ¡mucho mejor!

potencias de dos no

ten en cuenta que si quieres dividir la frecuencia a números que no son potencias de 2, podrías empezar a ver algunas fallas ("glitches") aproximadamente cada 4 segundos: esto se debe a que el cuentafotograma se sobrepasa y no da una buena secuencia de resultados para esos divisores.

esto también puede ocurrir si tienes una animación que consta de un número de fotogramas que no es una potencia de 2, y utilizas una operación MOD normal para calcular el desfase al fotograma correspondiente.

la solución más sencilla para estos problemas sería utilizar un número de fotogramas de pequeño tamaño que sólo causara esos fallos de sobreflujo aproximadamente cada 18 minutos.

tendrías que adaptar el programa para que funcione con ese tamaño de cuentafotograma - ¡siento y pienso que es un buen ejercicio!

instrucciones del día 4

¡estas son todas las instrucciones de uxntal que hemos discutido hoy!

las direcciones de LDA y STA son siempre cortos, mientras que las direcciones de las demás instrucciones son siempre un byte.

en modo corto, estas instrucciones cargan o almacenan cortos desde o hacia la memoria.

día 5

en el tutorial de uxn día 5 introducimos el dispositivo de ratón varvara para explorar más interacciones posibles, y cubrimos los elementos restantes de uxntal y uxn: la pila de retorno, el modo de retorno y el modo mantener.

¡también discutimos posibles estructuras para crear bucles y programas más complejos utilizando estos recursos!

¡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 6

tutorial de uxn día 3

tutorial de uxn día 5