💾 Archived View for compudanzas.net › tutorial_de_uxn_d%C3%ADa_2.gmi captured on 2023-06-16 at 16:36:35. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-06-14)
-=-=-=-=-=-=-
in english:
¡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
antes de pasar a dibujar en la pantalla, tenemos que hablar de 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?
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!
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
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 :)
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.
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.
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 ) #2ce9 .Sistema/r DEO2 #01c0 .Sistema/g DEO2 #2ce5 .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 :)
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!
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.
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.
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.
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?
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:
capa del fondo:
capa del primer plano:
¡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 ) #2ce9 .Sistema/r DEO2 #01c0 .Sistema/g DEO2 #2ce5 .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 :)
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 } ( -- )
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 -- )
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 } ( -- )
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 #2ce9 .Sistema/r DEO2 #01c0 .Sistema/g DEO2 #2ce5 .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!
¡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.
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).
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
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.
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.
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.
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:
fondo:
primer plano:
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:
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.
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.
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!
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 ) #2ce9 .Sistema/r DEO2 #01c0 .Sistema/g DEO2 #2ce5 .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
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 ) #2ce9 .Sistema/r DEO2 #01c0 .Sistema/g DEO2 #2ce5 .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.
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.
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.
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
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.
¡veamos cómo utilizar el sprite byte para dibujar tiles de 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:
fondo:
primer plano:
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.
el nibble bajo nos permitirá elegir entre muchas combinaciones de colores asignados a cada uno de los diferentes estados de los píxeles:
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 ) #2ce9 .Sistema/r DEO2 #01c0 .Sistema/g DEO2 #2ce5 .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!
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.
comparémoslo con todo lo que hemos dicho sobre el byte "sprite".
nasu es una herramienta de 100R, escrita en uxntal, que facilita el diseño y la exportación de sprites 2bpp.
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!
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.
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!
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.
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
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 )
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
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 :)
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).
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.
¡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!
si te ha gustado este tutorial y te ha resultado útil, considera compartirlo y darle tu apoyo :)
textos, imágenes y código compartidos bajo la licencia de producción de pares