compudanzas

tutorial de uxn: día 2, la pantalla

in english: uxn tutorial day 2

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

en esta sección vamos a empezar a explorar los aspectos visuales de la computadora varvara ¡hablamos sobre los aspectos fundamentales del dispositivo de pantalla para que podamos empezar a dibujar en ella!

también discutiremos el trabajo con el modo corto o "short" (2 bytes) junto a los de un solo byte en uxntal.

si todavía no lo has hecho te recomiendo que leas las sección anterior en el tutorial de uxn día 1

¿dónde están tus cortos?

antes de pasar a dibujar en la pantalla, tenemos que hablar de bytes y cortos :)

bytes y cortos

aunque uxn es un ordenador que trabaja de forma nativa con palabras de 8 bits (bytes), nos encontramos en varias ocasiones que la cantidad de datos que es posible almacenar en un byte no es suficiente.

cuando utilizamos 8 bits, podemos representar 256 valores diferentes (2 a la potencia de 8). en cualquier momento dado, un byte almacenará solo uno de esos posibles valores.

en la sección anterior, hablamos de un caso en el que esta cantidad no es suficiente en uxn: el número de bytes que alberga la memoria principal, 65536.

ese número corresponde a los valores que se pueden representar usando dos bytes, o 16 bits, o un "corto": 2 a la potencia de 16. esa cantidad también se conoce como 64KB, donde 1KB corresponde a 1024 o 2 a la potencia de 10.

además de expresar direcciones en la memoria principal, hoy veremos otro caso en el que 256 valores no siempre son suficientes: las coordenadas x e y de los píxeles de nuestra pantalla.

para estos y otros casos, usar cortos en lugar de bytes será el camino a seguir.

¿cómo los tratamos?

el modo corto

contando de derecha a izquierda, el 6º bit de un byte que codifica una instrucción para el ordenador uxn es una bandera binaria que indica si el modo corto está activado o no.

siempre que el modo corto esté activado, es decir, cuando ese bit sea 1 en lugar de 0, la cpu uxn realizará la instrucción dada por los 5 primeros bits (el opcode) pero utilizando pares de bytes en lugar de bytes individuales.

el byte que esté más adentro de la pila será el byte "alto" del corto y el byte que esté más cerca de la parte superior de la pila será el byte "bajo" del corto.

en uxntal, indicamos que queremos poner esta bandera añadiendo el dígito '2' al final de una instrucción mnemónica.

¡veamos algunos ejemplos!

ejemplo del modo corto

LIT2

en primer lugar, recapitulemos. el siguiente código empujará el número 02 hacia abajo en la pila, luego empujará el número 30 (hexadecimal) hacia abajo en la pila y finalmente los sumará, dejando el número 32 en la pila:

#02 #30 ADD

este sería el estado final de la pila:

32 <- arriba

en el día anterior mencionamos que la runa hexadecimal literal (#) es una abreviatura de la instrucción LIT. por lo tanto, podríamos haber escrito nuestro código de la siguiente manera:

LIT 02 LIT 30 ADD ( código ensamblado: 80 02 80 30 18 )

ahora, si añadimos el sufijo '2' a la instrucción LIT, podríamos escribir en su lugar:

LIT2 02 30 ADD ( código ensamblado: a0 02 30 18 )

en lugar de empujar un byte, LIT2 está empujando el corto (dos bytes) que sigue en la memoria, hacia abajo en la pila.

podemos utilizar la runa hexadecimal literal (#) con un corto (cuatro nibbles) en lugar de un byte (dos nibbles) y funcionará como una abreviatura de LIT2:

#0230 ADD

ADD2

ahora veamos que pasa con la instrucción ADD cuando usamos el modo corto.

¿cuál sería el estado de la pila después de ejecutar este código?

#0004 #0008 ADD

¡así es! la pila tendrá los siguientes valores, porque estamos empujando 4 bytes hacia abajo en la pila, sumando (ADD) los dos más cercanos a la parte superior y empujando el resultado hacia abajo en la pila.

00 04 08 <- arriba

ahora, comparemos con lo que ocurre con el ADD2:

#0004 #0008 ADD2

en este caso estamos empujando los mismos 4 bytes hacia abajo en la pila, pero ADD2 está haciendo las siguientes acciones:

la pila acaba teniendo el siguiente aspecto:

00 0c <- arriba

puede que no necesitemos pensar demasiado en las manipulaciones por byte de las operaciones aritméticas, porque normalmente podemos pensar que están haciendo la misma operación que antes, pero utilizando pares de bytes en lugar de bytes individuales. su orden no cambia realmente.

en cualquier caso, es útil tener en cuenta cómo funcionan para algunos comportamientos que podríamos necesitar más adelante :)

DEO2, DEI, DEI2

hablemos ahora de la instrucción DEO ("device out" o salida de dispositivo) de la que hablamos el día anterior, ya que su modo corto implica algo especial.

la instrucción DEO necesita un valor (1 byte) para salir y una dirección entrada/salida (1 byte) en la pila, para poder sacar ese valor en esa dirección.

DEO ( valor dirección -- )

esta instrucción tiene una contrapartida: DEI ("device in" o entrada de dispositivo).

la instrucción DEI toma una dirección de entrada/salida (1 byte) de la pila y va a empujar hacia abajo en la pila el valor (1 byte) que corresponde a la lectura de esa entrada.

DEI ( dirección -- valor )

¿qué crees que harán DEO2 y DEI2?

en el caso del modo corto de DEO y DEI, el aspecto corto se aplica al valor de salida o entrada y no a la dirección.

recuerda que las 256 direcciones de entrada/salida ya están cubiertas usando un solo byte, por lo que usar un corto para ellas sería redundante: el byte alto sería siempre 00.

considerando esto, los siguientes son los comportamientos que podemos esperar:

la instrucción DEO2 necesita un valor (1 corto) para salir y una dirección entrada/salida (1 byte) en la pila, para poder sacar ese valor a esa dirección. por lo tanto necesita un total de 3 bytes en la pila para operar.

por otro lado, la instrucción DEI2 necesita una dirección entrada/salida (1 byte) en la pila y empujará hacia abajo en la pila el valor (1 corto) que corresponde a esa entrada.

en la siguiente sección veremos algunos ejemplos en los que podremos utilizar estas instrucciones.

el puerto de 'escritura' del dispositivo de la consola que utilizamos la última vez tiene un tamaño de 1 byte, por lo que no podemos utilizar estas nuevas instrucciones de forma significativa con él.

dispositivo de sistema y colores

el dispositivo del sistema es el dispositivo varvara con una dirección de 00. sus puertos de salida (que comienzan en la dirección 08) corresponden a tres cortos diferentes: uno llamado rojo (r), el otro verde (g) y el último azul (b).

en los ejemplos uxntal podemos ver sus etiquetas definidas de la siguiente manera:

|00 @Sistema  [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]

ignoraremos los primeros elementos por el momento y nos centraremos en los componentes de color.

colores del sistema

el dispositivo de pantalla varvara solo puede mostrar un máximo de cuatro colores a la vez.

estos cuatro colores se denominan color 0, color 1, color 2 y color 3.

cada color tiene una profundidad total de 12 bits: 4 bits para el componente rojo, 4 bits para el componente verde y 4 bits para el componente azul.

podemos definir los valores de estos colores fijando los valores r, g, b del dispositivo del sistema.

podemos escribirlo de la siguiente manera:

( hola-pantalla.tal )

( dispositivos )
|00 @Sistema  [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]

( programa principal )
|0100
      ( establecer los colores del sistema )
      #2eef .Sistema/r DEO2
      #1eb8 .Sistema/g DEO2
      #1e2e .Sistema/b DEO2

¿cómo podríamos leer lo que significan esos cortos literales?

podemos leer cada uno de los colores verticalmente, de izquierda a derecha:

si ejecutamos el programa ahora veremos una pantalla de color púrpura oscuro, en lugar de negro como lo que teníamos antes.

prueba cambiar los valores del color 0, es decir, la primera columna, y mira lo que pasa :)

el dispositivo de pantalla

como recapitulación: mencionamos que el dispositivo de pantalla solo puede mostrar cuatro colores diferentes en un momento dado y que estos colores están numerados del 0 al 3. fijamos estos colores usando los puertos correspondientes en el dispositivo del sistema.

¡ahora hablemos del dispositivo de pantalla y empecemos a usarlo!

puertos de dispositivo

en los programas uxntal para el ordenador varvara puedes encontrar las etiquetas correspondientes a los puertos de este dispositivo de la siguiente manera:

|20 @Pantalla  [ &vector $2 &ancho $2 &alto $2 &auto $1 &pad $1 &x $2 &y $2 &direc $2 &píxel $1 &sprite $1 ]

estos son los puertos en formato de lista:

hablaremos sobre el corto vector en el tutorial de uxn día 4 y sobre el byte auto en el tutorial de uxn día 6.

primer plano y plano de fondo

el dispositivo de pantalla tiene dos capas superpuestas del mismo tamaño, el primer plano y el plano de fondo.

lo que se dibuje sobre la capa del primer plano cubrirá todo lo que se dibuje en la misma posición en la capa del plano de fondo.

al principio, la capa del primer plano es completamente transparente: un proceso de mezcla alfa asegura que podamos ver la capa de fondo.

dibujando un píxel

la primera y más sencilla forma de dibujar en la pantalla es dibujando un solo píxel.

para hacer esto necesitamos establecer un par de coordenadas x,y donde queremos que se dibuje el píxel y necesitamos establecer el byte 'píxel' a un valor específico para realizar realmente el dibujo.

estableciendo las coordenadas

las coordenadas x,y siguen las convenciones comunes a otros programas de gráficos por ordenador:

si quisiéramos dibujar un píxel en coordenadas (8, 8), estableceríamos sus coordenadas de esta manera:

#0008 .Pantalla/x DEO2
#0008 .Pantalla/y DEO2

alternativamente, podríamos empujar primero los valores de las coordenadas hacia abajo en la pila y la salida de ellos después:

#0008 #0008 .Pantalla/x DEO2 .Pantalla/y DEO2

una pregunta para ti: si quisiéramos establecer las coordenadas como (x: 4, y: 8), ¿cuál de los cortos en el código anterior deberías cambiar por 0004?

estableciendo el color

el envío de un único byte a .Pantalla/píxel realizará el dibujo en la pantalla.

el nibble alto de ese byte, es decir, el dígito hexadecimal de la izquierda, determinará la capa en la que dibujaremos:

el nibble inferior del byte, es decir, el dígito hexadecimal de la derecha, determinará su color.

las 8 posibles combinaciones del byte 'píxel' que tenemos para dibujar un píxel son:

píxelcolorcapa
000fn
011
022
033
400pp
411
422
433

hola píxel

¡probemos todo juntos! el siguiente código dibujará un píxel con el color 1 en la capa del primer plano, en las coordenadas (8,8):

#0008 .Pantalla/x DEO2
#0008 .Pantalla/y DEO2
#41 .Pantalla/píxel DEO

el programa completo se vería de la siguiente manera:

( hola-píxel.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 &píxel $1 &sprite $1 ]

( programa principal )
|0100
      ( establecer los colores del sistema )
      #2eef .Sistema/r DEO2
      #1eb8 .Sistema/g DEO2
      #1e2e .Sistema/b DEO2

      ( dibujar un píxel en la pantalla )
      #0008 .Pantalla/x DEO2
      #0008 .Pantalla/y DEO2
      #41 .Pantalla/píxel DEO ( capa de primer plano, color 1 )

¡wuju!

recuerda que puedes usar F1 para cambiar de nivel de zoom y F3 para hacer capturas de pantalla de tus bocetos :)

hola píxeles

los valores que establecemos en las coordenadas x e `y` permanecen ahí hasta que los sobrescribimos.

por ejemplo, podemos dibujar múltiples píxeles en una línea horizontal, estableciendo la coordenada `y` solo una vez:

( establecer coordenadas y )
#0008 .Pantalla/y DEO2

( dibujar 6 píxeles en una línea horizontal )
#0008 .Pantalla/x DEO2
#41 .Pantalla/píxel DEO

#0009 .Pantalla/x DEO2
#41 .Pantalla/píxel DEO

#000a .Pantalla/x DEO2
#41 .Pantalla/píxel DEO

#000b .Pantalla/x DEO2
#41 .Pantalla/píxel DEO

#000c .Pantalla/x DEO2
#41 .Pantalla/píxel DEO

#000d .Pantalla/x DEO2
#11 .Pantalla/píxel DEO

nótese que tenemos que establecer el color para cada píxel que dibujamos; esa operación señala el dibujo y tiene que repetirse.

podemos definir una macro para que este proceso sea más fácil de escribir:

%DIBUJAR-PÍXEL { #41 .Pantalla/píxel DEO } ( -- )

leyendo y manipulando coordenadas

todavía no cubriremos las estructuras repetitivas, pero esta es una buena oportunidad para empezar a alinear nuestro código hacia eso.

aunque las coordenadas x e `y` del dispositivo de pantalla están pensadas como salidas, también podemos leerlas como entradas.

por ejemplo, para leer la coordenada x, empujando su valor hacia abajo en la pila, podemos escribir:

.Pantalla/x DEI2

teniendo en cuenta esto, ¿se puede saber qué haría este código?

.Pantalla/x DEI2
#0001 ADD2
.Pantalla/x DEO2

¡lo has adivinado bien, espero!

ese conjunto de instrucciones incrementa la coordenada x de la pantalla por uno :)

parecen útiles, así que también podríamos guardarlos como una macro:

%INC-X { .Pantalla/x DEI2 #0001 ADD2 .Pantalla/x DEO2 } ( -- )

aquí hay otra pregunta para ti: ¿cómo escribirías una macro ADD-X que te permita incrementar la coordenada x en una cantidad arbitraria que pongas en la pila?

%ADD-X {   } ( incremento -- )

instrucción INC

añadir 1 al valor de la parte superior de la pila es tan común que hay una instrucción para conseguirlo utilizando menos espacio, INC:

INC ( a -- a+1 )

INC toma el valor de la parte superior de la pila, lo incrementa por uno y lo empuja de vuelta.

en el caso del modo corto, INC2 hace lo mismo pero incrementando un corto en lugar de un byte.

nuestra macro para incrementar la coordenada x podría entonces escribirse como sigue:

%INC-X { .Pantalla/x DEI2 INC2 .Pantalla/x DEO2 } ( -- )

hola píxeles usando macros

usando estas macros que definimos arriba, nuestro código puede terminar viéndose de la siguiente forma:

( hola-píxeles.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 &píxel $1 &sprite $1 ]

( macros )
%DIBUJAR-PÍXEL { #41 .Pantalla/píxel DEO } ( -- )
%INC-X { .Pantalla/x DEI2 INC2 .Pantalla/x DEO2 } ( -- )

( programa princpal )
|0100
      #2eef .Sistema/r DEO2
      #1eb8 .Sistema/g DEO2
      #1e2e .Sistema/b DEO2

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

      ( dibujar 6 píxeles en una línea horizontal )
      DIBUJAR-PÍXEL INC-X
      DIBUJAR-PÍXEL INC-X
      DIBUJAR-PÍXEL INC-X
      DIBUJAR-PÍXEL INC-X
      DIBUJAR-PÍXEL INC-X
      DIBUJAR-PÍXEL

agradable, ¿no? ¡las operaciones ahora se ven más claras! y si quisiéramos tener esta línea disponible para usarla en otras posiciones, podríamos definir una macro para ella:

%DIBUJAR-LÍNEA {  } ( -- )

¡intenta escribiendo la macro y utilizándola en diferentes posiciones de la pantalla!

dibujando sprites

¡ahora, veremos cómo aprovechar el soporte incorporado para "sprites" en el dispositivo de pantalla varvara para dibujar muchos píxeles a la vez!

el dispositivo de pantalla varvara nos permite utilizar y dibujar tiles de 8x8 píxeles, también llamados sprites.

hay dos modos posibles: 1bpp (1 bit por píxel) y 2bpp (2 bits por píxel).

los mosaicos o "tiles" de 1bpp usan dos colores y se codifican usando 8 bytes; usar un bit por píxel significa que solo podemos codificar si ese píxel está usando un color o el otro.

los tiles de 2bpp utilizan cuatro colores y se codifican utilizando 16 bytes; el uso de dos bits por píxel significa que podemos codificar cuál de los cuatro colores disponibles tiene el píxel.

almacenaremos y accederemos a estos tiles desde la memoria principal.

dibujando sprites de 1bpp

un tile de 1bpp consiste en un conjunto de 8 bytes que codifican el estado de sus 8x8 píxeles.

cada byte corresponde a una fila del tile y cada bit de una fila corresponde al estado de un píxel de izquierda a derecha: puede estar "encendido" (1) o "apagado" (0).

codificando un sprite de 1bpp

por ejemplo, podríamos diseñar un azulejo que corresponda al contorno de un cuadrado de 8x8, activando o desactivando sus píxeles en consecuencia.

11111111
10000001
10000001
10000001
10000001
10000001
10000001
11111111

como cada una de las filas es un byte, podemos codificarlas como números hexadecimales en lugar de binarios.

vale la pena notar (o recordar) que los grupos de cuatro bits corresponden a un nibble y cada combinación posible en un nibble puede ser codificada como un dígito hexadecimal.

basándonos en eso, podríamos codificar nuestro cuadrado de la siguiente manera:

11111111: ff
10000001: 81
10000001: 81
10000001: 81
10000001: 81
10000001: 81
10000001: 81
11111111: ff

almacenando el sprite

en uxntal, necesitamos etiquetar y escribir en la memoria principal los datos correspondientes al sprite. escribimos los bytes que van de arriba a abajo del sprite:

@cuadrado ff81 8181 8181 81ff

tengamos en cuenta que aquí no estamos utilizando la runa hexadecimal literal (#): queremos utilizar los bytes en bruto en la memoria y no necesitamos empujarlos hacia abajo en la pila.

para asegurarse de que estos bytes no son leídos como instrucciones por la cpu uxn, es una buena práctica precederlos con la instrucción BRK: esto interrumpirá la ejecución del programa antes de llegar aquí, dejando a uxn en un estado en el que está esperando entradas.

un poco más adelante revisaremos dónde colocar este código.

configurando la dirección

para dibujar el sprite, necesitamos enviar su dirección en memoria al dispositivo de pantalla y necesitamos asignar un byte de sprite apropiado.

para lograr esto, escribimos lo siguiente:

;cuadrado .Pantalla/direc DEO2

¡una nueva runa está aquí! la runa de dirección absoluta literal (;) nos permite empujar hacia abajo en la pila la dirección absoluta de la etiqueta dada en la memoria principal.

una dirección absoluta tendría 2 bytes de longitud y se introduce en la pila con LIT2, incluida por el ensamblador cuando se utiliza esta runa.

como la dirección es de 2 bytes, la imprimimos con DEO2.

configurando el color

de forma similar a lo que ya vimos con el píxel, el envío de un byte a .Pantalla/sprite realizará el dibujo en la pantalla.

nibble alto de sprite para 1bpp

el nibble alto del byte 'sprite' determinará la capa en la que dibujaremos, igual que cuando dibujábamos usando el byte 'píxel'.

sin embargo, en este caso tendremos otras posibilidades: podemos invertir el sprite en el eje horizontal (x) o en el vertical (y).

los ocho valores posibles de este nibble alto de 'sprite', utilizados para dibujar un sprite de 1bpp, son:

sprite altocapainv-yinv-x
0fnnono
1
2no
3
4ppnono
5
6no
7

si se observa con atención, se puede ver algún patrón: cada bit del nibble alto del byte del sprite corresponde a un aspecto diferente de este comportamiento.

lo siguiente muestra el significado de cada uno de estos bits en el nibble alto, suponiendo que estamos contando los bits del byte de derecha a izquierda y de 0 a 7:

bitbandera01
7modo1bpp2bpp
6capafnpp
5inv-yno
4inv-xno

como por ejemplo, cuando el nibble alto del 'sprite' es 0, que en binario es 0000, significa que todas las banderas están apagadas: por eso dibuja un sprite de 1bpp (0) en el fondo (0), que no esta invertido ni verticalmente (0) ni horizontalmente (0).

un nibble alto de 1, es decir, 0001 en binario, tiene la última bandera encendida, por eso se invierte horizontalmente, y así sucesivamente.

nibble bajo de sprite para 1bpp

el nibble bajo del byte 'sprite' determinará los colores que se utilizan para dibujar los píxeles "encendido" (1) y "apagado" (0) de los tiles.

colores para:
sprite bajo10
0borrarborrar
110
220
330
401
51nada
621
731
802
912
a2nada
b32
c03
d13
e23
f3nada

por ejemplo, esto significa que si establecemos el nibble bajo de 'sprite' con el valor de 6, varvara dibujará el sprite utilizando el color 2 para los pixeles "encendidos" y el color 1 para los pixeles "apagados".

notemos que un 0 en el este nibble borrará el tile.

además, 5, 'a' y 'f' en el nibble bajo dibujarán los píxeles que están "encendidos" pero dejarán los que están "apagados" como están: esto le permitirá dibujar sobre algo que ha sido dibujado antes, sin borrarlo completamente.

no te preocupes si esto no está teniendo mucho sentido, ¡veamos un ejemplo!

hola sprite

el siguiente programa dibujará nuestro sprite una vez:

( hola-sprite.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 &píxel $1 &sprite $1 ]

( programa principal )
|0100
      ( establecer los colores del sistema )
      #2eef .Sistema/r DEO2
      #1eb8 .Sistema/g DEO2
      #1e2e .Sistema/b DEO2

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

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

      ( dibujar el sprite en el fondo )
      ( usando el color 1 para el contorno )
      #01 .Pantalla/sprite DEO

BRK

@cuadrado ff81 8181 8181 81ff

hola sprites

captura de pantalla del resultado del programa, mostrando 16 cuadrados coloreados con diferentes combinaciones de contorno y relleno.

el siguiente código dibujará nuestro sprite cuadrado con las 16 combinaciones de color:

( hola-sprites.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 &píxel $1 &sprite $1 ]

( macros )
%INIT-X { #0008 .Pantalla/x DEO2 } ( -- )
%INIT-Y { #0008 .Pantalla/y DEO2 } ( -- )
%8ADD-X { .Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2 } ( -- )
%8ADD-Y { .Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2 } ( -- )

( programa principal )
|0100
      ( establecer los colores del sistema )
      #2eef .Sistema/r DEO2
      #1eb8 .Sistema/g DEO2
      #1e2e .Sistema/b DEO2

      ( establecer coordenadas iniciales x,y )
      INIT-X INIT-Y

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

      #00 .Pantalla/sprite DEO 8ADD-X
      #01 .Pantalla/sprite DEO 8ADD-X
      #02 .Pantalla/sprite DEO 8ADD-X
      #03 .Pantalla/sprite DEO 8ADD-Y

      INIT-X
      #04 .Pantalla/sprite DEO 8ADD-X
      #05 .Pantalla/sprite DEO 8ADD-X
      #06 .Pantalla/sprite DEO 8ADD-X
      #07 .Pantalla/sprite DEO 8ADD-Y

      INIT-X
      #08 .Pantalla/sprite DEO 8ADD-X
      #09 .Pantalla/sprite DEO 8ADD-X
      #0a .Pantalla/sprite DEO 8ADD-X
      #0b .Pantalla/sprite DEO 8ADD-Y

      INIT-X
      #0c .Pantalla/sprite DEO 8ADD-X
      #0d .Pantalla/sprite DEO 8ADD-X
      #0e .Pantalla/sprite DEO 8ADD-X
      #0f .Pantalla/sprite DEO

BRK

@cuadrado ff81 8181 8181 81ff

observemos que en este caso, tenemos un par de macros 8ADD-X y 8ADD-Y para incrementar cada coordenada por 0008: ese es el tamaño del tile.

experimentos de inversión

como el sprite cuadrado es simétrico, no podemos ver el efecto de invertirlo.

aquí están los sprites de la roca y del personaje de darena:

@piedra 3c4e 9ffd f962 3c00
@personaje 3c7e 5a7f 1b3c 5a18

te invito a que intentes usar estos sprites para explorar cómo dibujarlos invertidos en diferentes direcciones.

dibujando sprites de 2bpp

en los sprites de 2bpp cada píxel puede tener uno de los cuatro colores posibles.

podemos pensar que, para asignar estos colores, codificaremos uno de los cuatro estados en cada uno de los píxeles del sprite.

cada uno de estos estados puede codificarse con una combinación de dos bits. a estos estados se les puede asignar diferentes combinaciones de los cuatro colores del sistema utilizando los valores apropiados en el byte 'sprite' de la pantalla.

un solo tile de 2bpp de 8x8 píxeles necesita 16 bytes para ser codificada. estos bytes se ordenan según un formato llamado chr.

codificando un sprite de 2bpp

para demostrar esta codificación, vamos a remezclar nuestro cuadrado de 8x8, asignando uno de los cuatro estados posibles (0, 1, 2, 3) a cada uno de los píxeles:

00000001
03333311
03333211
03332211
03322211
03222211
01111111
11111111

podemos pensar en cada uno de estos dígitos como un par de bits: 0 es 00, 1 es 01, 2 es 10 y 3 es 11.

de esta manera, podríamos pensar en nuestro sprite de la siguiente manera:

(00) (00) (00) (00) (00) (00) (00) (01)
(00) (11) (11) (11) (11) (11) (01) (01)
(00) (11) (11) (11) (11) (10) (01) (01)
(00) (11) (11) (11) (10) (10) (01) (01)
(00) (11) (11) (10) (10) (10) (01) (01)
(00) (11) (10) (10) (10) (10) (01) (01)
(00) (01) (01) (01) (01) (01) (01) (01)
(01) (01) (01) (01) (01) (01) (01) (01)

la codificación chr requiere una interesante manipulación de esos bits: podemos pensar que cada par de bits tiene un bit alto en la izquierda y un bit bajo en la derecha.

separamos nuestro tile en dos cuadrados diferentes, uno para los bits altos y otro para los bits bajos:

00000000     00000001
01111100     01111111
01111100     01111011
01111100     01110011
01111100     01100011
01111100     01000011
00000000     01111111
00000000     11111111

ahora podemos pensar en cada uno de estos cuadrados como sprites de 1bpp y codificarlos en hexadecimal como lo hicimos antes:

00000000: 00 00000001: 01
01111100: 7c 01111111: 7f
01111100: 7c 01111011: 7b
01111100: 7c 01110011: 73
01111100: 7c 01100011: 63
01111100: 7c 01000011: 43
00000000: 00 01111111: 7f
00000000: 00 11111111: ff

almacenando el sprite

para escribir este sprite en la memoria, primero almacenamos el cuadrado correspondiente a los bits bajos y luego el cuadrado correspondiente a los bits altos. cada uno de ellos, de arriba a abajo:

@nuevo-cuadrado  017f 7b73 6343 7fff   007c 7c7c 7c7c 0000

podemos establecer esta dirección en el dispositivo de pantalla igual que antes:

;nuevo-cuadrado .Pantalla/direc DEO2

el dispositivo de pantalla tratará esta dirección como un sprite 2bpp cuando usemos el byte de color apropiado.

configurando el color

¡veamos cómo utilizar el sprite byte para dibujar tiles de 2bpp!

nibble alto de sprite para 2bpp

el nibble alto para los sprites de 2bpp nos permitirá elegir la capa que queremos que se dibuje y la dirección de rotación.

los ocho valores posibles para este nibble son:

sprite altocapainv-yinv-x
8fnnono
9
ano
b
cppnono
d
eno
f

notemos que estos ocho valores tienen todos un bit más a la izquierda en 1: este bit señala que vamos a dibujar un sprite de 2bpp. los otros tres bits del nibble se comportan como se ha descrito anteriormente en el caso de 1bpp.

sprite de nibble bajo para 2bpp

el nibble bajo nos permitirá elegir entre muchas combinaciones de colores asignados a cada uno de los diferentes estados de los píxeles:

colores para:
sprite bajo0 123
00012
10123
20231
30312
41012
5nada123
61231
71312
82012
92123
anada231
b2312
c3012
d3123
e3231
fnada312

¡hola nuevos sprites!

captura de pantalla de la salida del programa, mostrando 16 cuadrados coloreados con diferentes combinaciones de contorno y relleno.

el siguiente código mostrará nuestro sprite en las 16 diferentes combinaciones de color. hay un poco de margen entre las baldosas para poder apreciarlas mejor:

( hola-sprite-2bpp.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 &píxel $1 &sprite $1 ]

( macros )
%INIT-X { #0008 .Pantalla/x DEO2 } ( -- )
%INIT-Y { #0008 .Pantalla/y DEO2 } ( -- )
%cADD-X { .Pantalla/x DEI2 #000c ADD2 .Pantalla/x DEO2 } ( -- )
%cADD-Y { .Pantalla/y DEI2 #000c ADD2 .Pantalla/y DEO2 } ( -- )

( programa principal )
|0100
      ( establecer los colores del sistema )
      #2eef .Sistema/r DEO2
      #1eb8 .Sistema/g DEO2
      #1e2e .Sistema/b DEO2

      ( establecer coordenadas iniciales x,y )
      INIT-X INIT-Y
      ( establecer dirección del sprite )
      ;nuevo-cuadrado .Pantalla/direc DEO2

      #80 .Pantalla/sprite DEO cADD-X
      #81 .Pantalla/sprite DEO cADD-X
      #82 .Pantalla/sprite DEO cADD-X
      #83 .Pantalla/sprite DEO cADD-Y

      INIT-X
      #84 .Pantalla/sprite DEO cADD-X
      #85 .Pantalla/sprite DEO cADD-X
      #86 .Pantalla/sprite DEO cADD-X
      #87 .Pantalla/sprite DEO cADD-Y

      INIT-X
      #88 .Pantalla/sprite DEO cADD-X
      #89 .Pantalla/sprite DEO cADD-X
      #8a .Pantalla/sprite DEO cADD-X
      #8b .Pantalla/sprite DEO cADD-Y

      INIT-X
      #8c .Pantalla/sprite DEO cADD-X
      #8d .Pantalla/sprite DEO cADD-X
      #8e .Pantalla/sprite DEO cADD-X
      #8f .Pantalla/sprite DEO

BRK

@nuevo-cuadrado  017f 7b73 6343 7fff     007c 7c7c 7c7c 0000

¡intenta rotar los tiles!

screen.tal y las combinaciones del byte del sprite

el ejemplo screen.tal en el repo de uxn consiste en una tabla que muestra todas las posibles (¡256!) combinaciones de nibbles altos y bajos en el byte del sprite.

captura de pantalla del ejemplo screen.tal, que muestra un sprite coloreado y volteado de diferentes maneras.

código de screen.tal

comparémoslo con todo lo que hemos dicho sobre el byte "sprite".

diseñando sprites

nasu es una herramienta de 100R, escrita en uxntal, que facilita el diseño y la exportación de sprites 2bpp.

100R - nasu

además de usarlo para dibujar con los colores 1, 2, 3 (y borrar para obtener el color 0), puedes usarlo para encontrar los colores de tu sistema, para ver cómo se verán tus sprites con los diferentes modos de color (también conocidos como modos de mezcla) y para ensamblar objetos hechos de múltiples sprites.

con nasu puedes exportar e importar archivos chr que después puedes incluir en tu código.

para esto conviene usar una herramienta como hexdump para obtener su representación hexadecimal:

$ hexdump -C sprites.chr

¡te recomiendo que pruebes nasu!

tamaño de la pantalla y capacidad de respuesta

lo último que cubriremos hoy tiene que ver con las suposiciones que hace varvara sobre el tamaño de su pantalla y algunas estrategias de código que podemos usar para lidiar con ellas.

en resumen, ¡no hay un tamaño de pantalla estándar!

por defecto, la pantalla del emulador varvara tiene un tamaño de 512x320 píxeles (o 64x40 tiles).

sin embargo, y a modo de ejemplo, el ordenador virtual también funciona en la nintendo ds, con una resolución de 256x192 píxeles (32x24 tiles), y en el teletipo, con una resolución de 128x64 píxeles (16x8 tiles)

como programadorxs, se espera que decidamos qué hacer con ellos: nuestros programas pueden adaptarse a los distintos tamaños de pantalla, pueden tener distintos modos según el tamaño de la pantalla, etc.

cambiando el tamaño de la pantalla

adicionalmente, podemos cambiar el tamaño de la pantalla varvara escribiendo en los puertos .Pantalla/ancho y .Pantalla/alto.

por ejemplo, el siguiente código cambiaría la pantalla a una resolución de 640x480:

#0280 .Pantalla/ancho DEO2 ( anchura de 640 )
#01e0 .Pantalla/alto DEO2 ( altura de 480 )

tengamos en cuenta que esto solo funcionaría para las instancias del emulador varvara en las que el tamaño de la pantalla puede cambiarse realmente, por ejemplo, porque la pantalla virtual es una ventana.

¡sería importante tener en cuenta los aspectos de la capacidad de respuesta que se discuten a continuación, para los casos en los que no podemos cambiar el tamaño de la pantalla!

tamaño de pantalla por defecto

originalmente, la forma de cambiar el tamaño de la pantalla en uxnemu implicaba editar su código fuente.

si te has descargado el repositorio con el código fuente, verás que dentro del directorio src/ hay un uxnemu.c, con un par de líneas parecidas a las siguientes:

#define WIDTH 64 * 8
#define HEIGHT 40 * 8

esos dos números, 64 y 40 (ancho y alto), son el tamaño de pantalla por defecto en tiles, como mencionamos anteriormente.

puedes cambiarlos, guardar el archivo y volver a ejecutar el script build.sh para que uxnemu funcione con esta nueva resolución.

leyendo y adaptando el tamaño de la pantalla (lo básico)

como recordarás de los puertos de dispositivos de pantalla mencionados anteriormente, la pantalla nos permite leer su anchura y altura como cortos.

si quisiéramos, por ejemplo, dibujar un píxel en el centro de la pantalla independientemente del tamaño de la misma, podemos traducir a uxntal una expresión como la siguiente:

x =  anchopantalla/2
y =  altopantalla/2

división uxntal

para esto, vamos a introducir las instrucciones MUL y DIV: funcionan como ADD y SUB, pero para la multiplicación y la división:

usando DIV, nuestra expresión traducida para el caso de la coordenada x, podría verse como:

.Pantalla/ancho DEI2 ( obtener el ancho de la pantalla en la pila )
#0002 DIV2 ( dividir sobre 2 )
.Pantalla/x DEO2 ( tomar el resultado de la pila y enviarlo a .Pantalla/x )

desplazamiento de bits

si lo que queremos es dividir por encima o multiplicar por potencias de dos (como en este caso), también podemos utilizar la instrucción SFT.

esta instrucción toma un número y un "valor de desplazamiento" que indica la cantidad de posiciones de bit a desplazar a la derecha o a la izquierda.

el nibble inferior del valor de desplazamiento indica a uxn cuántas posiciones hay que desplazar a la derecha y el nibble superior expresa cuántos bits hay que desplazar a la izquierda.

para dividir un número por encima de 2, tendríamos que desplazar sus bits un espacio a la derecha.

por ejemplo, dividir 10 (en decimal) entre 2 podría expresarse de la siguiente manera:

#0a #01 SFT ( resultado: 05 )

0a es 0000 1010 en binario y 05 es 0000 0101 en binario: los bits de 0a se desplazaron una posición a la derecha y se introdujo un cero como bit más a la izquierda.

para multiplicar por 2, desplazamos un espacio a la izquierda:

#0a #10 SFT ( resultado: 14 en hexadecimal )

14 en hexadecimal (20 en decimal), es 0001 0100 en binario: los bits de 0a fueron desplazados una posición a la izquierda y un cero fue introducido como el bit más a la derecha.

en modo corto, el número a desplazar es un corto, pero el valor de desplazamiento sigue siendo un byte.

por ejemplo, lo siguiente dividirá el ancho de la pantalla en dos, utilizando el desplazamiento a nivel de bits:

.Pantalla/ancho DEI2
#01 SFT2

macros MITAD

para seguir ilustrando el uso de las macros, podríamos definir unas macros MITAD y MITAD2, utilizando DIV o SFT.

usando DIV:

%MITAD { #02 DIV } ( número -- número/2 )
%MITAD2 { #0002 DIV2 } ( número -- número/2 )

usando SFT:

%MITAD { #01 SFT } ( número -- número/2 )
%MITAD2 { #01 SFT2 } ( número -- número/2 )

y utilizar cualquiera de ellos para calcular el centro:

.Pantalla/ancho DEI2 MITAD2 .Pantalla/x DEO2
.Pantalla/alto DEI2 MITAD2 .Pantalla/y DEO2

notemos que la macro MITAD2 que utiliza SFT2 necesitará un byte menos que la que utiliza DIV2. esto puede o no ser importante dependiendo de tus prioridades :)

dibujando sprites en posiciones específicas

como ejercicio para ti, te invito a que escribas el código que lograría algo o todo lo siguiente:

una vez que lo logres, te invito a que hagas lo mismo, pero utilizando una imagen compuesta por múltiples tiles (por ejemplo, tiles de 2x2, tiles de 1x2, etc).

instrucciones del día 2

además de cubrir los fundamentos del dispositivo de pantalla hoy, discutimos estas nuevas instrucciones:

también cubrimos el modo corto, que le indica a la cpu que debe operar con palabras de 2 bytes de longitud.

día 3

¡en el tutorial de uxn día 3 empezamos a trabajar con la interactividad usando el teclado y cubrimos en profundidad varias instrucciones uxntales!

sin embargo, ¡te invito a que te tomes un descanso y a que sigas explorando el dibujo en la pantalla de uxn a través del código antes de continuar!

apoyo

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

enlaces entrantes