icono de compudanzas

tutorial uxn apéndice a: repetir un tile dentro de un rectángulo

en la primera parte del tutorial de uxn día 6 hablamos de cómo cubrir el fondo de la pantalla con un tile dado.

aquí generalizaremos un procedimiento similar en una subrutina dibuja-tiles que dibuje un rectángulo relleno con un tile dado. recibirá las coordenadas x,y de la esquina superior izquierda del rectángulo, su anchura y altura en píxeles, y la dirección del tile:

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

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

vamos a detallar cómo llegar a dos versiones de esta subrutina, una que se basa en una fuerte manipulación de la pila, y otra que utiliza variables. esto con el fin de comparar ambos enfoques y darnos una visión más amplia de las posibilidades dentro de uxntal.

configuración

comencemos con el siguiente programa como plantilla. incluye los datos para un sprite de 1bpp compuesto por líneas diagonales.

( hola-fondo.tal )

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

( macros )
%RTN { JMP2r }

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

@tile-fondo 1122 4488 1122 4488

repetir un tile en una fila

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

una forma sería algo así como:

versión concreta

antes de abstraerlo, recomiendo que lo escribamos con números concretos.

digamos que nuestra x inicial es 0008, nuestro ancho es 0100, y el tile que estamos dibujando es tile-fondo.

el límite, x+ancho, sería 0108.

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

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

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

añadiendo 8 a x, ya sabemos:

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

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

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

integrando todo ello, podríamos obtener:

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

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

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

abstrayendo

ahora, digamos que queremos que el código anterior funcione con cualquier x inicial y ancho inicial dados, presentes en la pila antes de empezar.

podemos incluso pensar en ello como una subrutina por sí misma con la siguiente firma:

@dibuja-tiles-en-fila ( x^ ancho^ -- )

asumamos por el momento que la dirección del sprite ya fue establecida, para enfocarnos en `x` y en el ancho.

al iniciar la subrutina, el ancho está en la parte superior de la pila, seguido de la x inicial.

podemos utilizar estos dos valores para calcular el límite, que podemos almacenar en la pila de retorno.

una forma de conseguirlo, anotando el estado de la pila de trabajo después de cada instrucción, podría ser:

( estado inicial: ptra: x^ ancho^ )
OVR2 ( ptra: x^ ancho^ x^ )
ADD2 ( ptra: x^ límite^ )
STH2 ( ptra: x^ / pret: límite^ )

otra más:

( estado inicial: ptra: x^ ancho^ )
ADD2k ( ptra: x^ ancho^ límite^ )
STH2 ( ptra: x^ ancho^ / pret: límite^ )
POP2 ( ptra: x^ / pret: límite^ )

recuerda que estamos mostrando la parte superior de las pilas a su derecha.

después de estos pasos, la x inicial está en la parte superior de la pila, por lo que podemos enviarla directamente a la pantalla.

el último cambio que necesitaríamos es reemplazar nuestro límite codificado por una instrucción STH2kr (copiar el límite de la pila de retorno a la pila de trabajo), y terminar nuestra rutina con una POP2r (eliminar el límite de la pila de retorno).

nuestra subrutina se vería entonces de la siguiente manera:

@dibuja-tiles-en-fila ( x^ ancho^ -- )
    ( calcular y guardar el límite )
    OVR2 ( ptra: x^ ancho^ x^ )
    ADD2 ( ptra: x^ límite^ )
    STH2 ( ptra: x^ / pret: límite^ )
    .Pantalla/x DEO2 ( fijar x inicial )

    &bucle-x
        #03 .Pantalla/sprite DEO ( dibujar sprite con color 3 y 0 )
        .Pantalla/x DEI2 #0008 ADD2 DUP2 ( añadir 8 a x )
        .Pantalla/x DEO2 ( guardar la nueva x )
        STH2kr ( copiar límite de pret en ptra )
        LTH2 ,&bucle-x JCN ( saltar si x es menor que el límite )
    POP2r ( hacer POP en límite de pret )
RTN

programa completo

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

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

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

( macros )
%RTN { JMP2r }

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

    ;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
    #0000 ( x inicial )
    .Pantalla/ancho DEI2 ( obtener el ancho de la pantalla )
    ;dibuja-tiles-en-fila JSR2
BRK

@dibuja-tiles-en-fila ( x^ ancho^ -- )
    OVR2 ( ptra: x^ límite^ x^ )
    ADD2 ( ptra: x^ límite^ )
    STH2 ( ptra: x^ / pret: límite^ )
    .Pantalla/x DEO2 ( fijar x inicial )

    &bucle-x
        #03 .Pantalla/sprite DEO ( dibujar sprite con color 3 y 0 )
        .Pantalla/x DEI2 #0008 ADD2 DUP2 ( añadir 8 a x )
        .Pantalla/x DEO2 ( guarda la nueva x )
        STH2kr ( copiar límite de pret en ptra )
        LTH2 ,&bucle-x JCN ( saltar si x es menor que el límite )
    POP2r ( sacar el límite de pret )
RTN

@tile-fondo 1122 4488 1122 4488

repetir una fila

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

siguiendo la misma estrategia, podríamos hacer:

versión concreta

utilicemos los mismos números que antes, suponiendo que nuestra `y` inicial es 0008, nuestra altura es 0100, y por tanto nuestro límite siendo 0108.

en el caso de x, comencemos en 0000 y tengamos un ancho correspondiente al ancho de la pantalla.

como la dirección no cambiaría en el proceso, podemos establecerla al principio y olvidarnos de ella.

el siguiente código se basa en el bucle anterior de x, pero ahora dibuja una fila en una coordenada `y` dada, le suma 8 y luego comprueba si es menor que el límite:

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

#0008 .Pantalla/y ( establecer y inicial )

 &bucle-y
    ( preparar y dibujar fila )
    #0000 ( x inicial )
    .Pantalla/ancho DEI2 ( obtener el ancho de la pantalla )
    ;dibuja-tiles-en-fila JSR2

    .Pantalla/y DEI2 #0008 ADD2 DUP2 ( añade 8 a y )
    .Pantalla/y DEO2 ( guardar la nueva y )
    #0108 ( poner límite en la parte superior de la pila )
    LTH2 ,&bucle-y JCN ( saltar si x es menor que el límite )

versión abstracta

ahora, antes de saltar directamente a la emulación de la solución para dibujar la fila, vamos a notar que en este caso no es tan fácil.

¿por qué? porque la idea de nuestra subrutina dibuja-tiles es que debe ser capaz de recibir la x inicial y el ancho del rectángulo, y ahora mismo estos valores están codificados dentro del bucle.

esta debería ser la firma de nuestra subrutina:

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

podemos abordar este problema bien con alguna "manipulación de la pila ", o bien con "variables ".

también hay que tener en cuenta que lo haremos porque estamos tratando de llegar a una subrutina generalizada.

si sólo quisiéramos cubrir toda la pantalla con un sprite, ya tenemos todo el código necesario: sólo tendríamos que adaptar el límite vertical del bucle para que se corresponda con la altura de la pantalla, ¡y ya está!

entonces podríamos pasar a la sección relativa de las paletas. sin embargo, lo que sigue puede ser interesante como una forma de ver un posible enfoque para escribir un código uxntal más complejo :)

usando la manipulación de la pila

en principio podríamos simplemente manipular los elementos dados en la pila, almacenándolos cuando sea apropiado, para adaptar nuestra subrutina a su firma.

en primer lugar, la dirección del tile es el valor de la parte superior de la pila. podemos consumirlo y olvidarnos de él:

( ptra inicial: x^ y^ ancho^ alto^ direc* )
.Pantalla/direc DEO2 ( ptra: x^ y^ ancho^ alto^ )

pensando en el bucle vertical, necesitamos calcular su límite sumando la altura a `y`, y necesitamos establecer la `y` inicial.

podríamos hacer lo siguiente:

ROT2 ( ptra: x^ ancho^ altura^ y^ )
DUP2 ( ptra: x^ ancho^ altura^ y^ y^ )

( establecer y inicial: )
.Pantalla/y DEO2 ( ptra: x^ ancho^ alto^ y^ )

( calcular y almacenar el límite vertical )
ADD2 ( ptra: x^ ancho^ limite-y^ )
STH2 ( ptra: x^ ancho^ / pret: limite-y^ )

ahora, podríamos también almacenar el ancho y `x`, ya que las necesitamos después en su orden original (primero x, luego el ancho )

STH2 ( ptra: x^ / pret: limite-y^ ancho^ )
STH2 ( ptra: / pret: limite-y^ ancho^ x^ )

en teoría, la primera parte de nuestra subrutina podría verse como:

@dibuja-tiles ( x^ y^ ancho^ alto^ direc* -- )
    ( establecer la dirección del tile )
    .Pantalla/direc DEO2 ( ptra: x^ y^ ancho^ alto^ )

    ROT2 ( ptra: x^ ^ altura^ y^ )
    DUP2 ( ptra: x^ ^ altura^ y^ )

    ( fijar `y` inicial )
    .Pantalla/y DEO2 ( fijar `y` inicial, ptra: x^ ^ altura^ y^ )

    ( calcular y guardar limite-y )
    ADD2 ( ptra: x^ ^ limite-y^ )
    STH2 ( ptra: x^ ^ / pret: limite-y^ )

    ( almacenar ancho y `x` )
    STH2 STH2 ( ptra: / pret: limite-y^ ancho^ x^ )

    &bucle-y
        ( preparar y dibujar la fila )
        ( recuperar x )
        STH2r ( ptra: x^ / pret: limite-y^ ancho^ )

        ( recuperar el ancho )
        STH2r ( ptra: x^ ancho^ / pret: limite-y^ )
        ;dibuja-tiles-en-fila JSR2

el problema es que dentro del bucle, ambas instrucciones STH2r recuperan y consumen los valores de `x` y ancho de la pila de retorno. por tanto, en la siguiente iteración no podríamos volver a utilizarlos, ya que se perderían.

podemos pensar que podríamos reemplazar estas instrucciones con STH2kr:

    &bucle-y
        ( preparar y dibujar la fila )
        ( recuperar x )
        STH2kr ( ptra: x^ / pret: limite-y^ ancho^ x^ )

¡pero entonces no podemos recuperar el ancho porque la x sigue en la parte superior de la pila de retorno!

oh, muchas dificultades, pero por el bien del ejemplo de la pila, vamos a seguir resolviendo esto (?)

¿cómo podemos poner el ancho en la parte superior de la pila de retorno? tal vez con un intercambio aplicado a la pila de retorno:

SWP2r ( ptra: x^ / pret: limite-y^ x^ ancho^ )

entonces podemos recuperar el ancho y utilizarlo:

STH2kr ( ptra: x^ ancho^ / pret: limite-y^ x^ ancho^ )
;dibuja-tiles-en-fila JSR2 ( ptra: / pret: limite-y^ x^ ancho^ )

¿qué sigue? añadir 8 a `y`, y comprobar si es menor que el límite. la primera parte va sin problemas:

.Pantalla/y DEI2 #0008 ADD2 DUP2 ( añade 8 a `y`; ptra: y^ y^ / pret: limite-y^ x^ ancho^ )
.Pantalla/y DEO2 ( almacenar nueva `y`; ptra: y^ / pret: limite-y^ x^ ancho^ )

para conseguir el límite en la pila de trabajo para la comparación, tenemos que girar la pila de retorno:

ROT2r ( ptra: y^ / pret: x^ ancho^ limite-y^ )
STH2kr ( ptra: y^ limite-y^ / pret: x^ ancho^ limite-y^ )

pero ah, antes de hacer la comparación y el salto, debemos reordenar la pila de retorno para que se corresponda con la ordenación que teníamos al principio del bucle:

SWP2r ( ptra: y^ limite-y^ / pret: x^ limite-y^ ancho^ )
ROT2r ( ptra: y^ limite-y^ / pret: limite-y^ ancho^ x^ )

ahora podemos hacer la comparación y saltar:

LTH2 ,&bucle-y JCN ( salta si x es menor que el límite )

después debemos limpiar la pila de retorno:

POP2r POP2r POP2r

después de todo esto, nuestra subrutina tendría el siguiente aspecto:

@dibuja-tiles ( x^ y^ ancho^ alto^ direc* -- )
    ( establecer la dirección del tile )
    .Pantalla/direc DEO2 ( ptra: x^ y^ ancho^ alto^ )

    ROT2 ( ptra: x^ ^ altura^ y^ )
    DUP2 ( ptra: x^ ^ altura^ y^ )

    ( fijar `y` inicial )
    .Pantalla/y DEO2 ( fijar `y` inicial, ptra: x^ ^ altura^ y^ )

    ( calcular y almacenar limite-y )
    ADD2 ( ptra: x^ ^ limite-y^ )
    STH2 ( ptra: x^ ^ / pret: limite-y^ )

    ( almacenar ancho y `x` )
    STH2 STH2 ( ptra: / pret: limite-y^ ancho^ x^ )

    &bucle-y
        ( preparar y dibujar la fila )
        ( recuperar x )
        STH2kr ( ptra: x^ / pret: limite-y^ ancho^ x^ )

        ( recuperar el ancho  )
        SWP2r ( ptra: x^ / pret: limite-y^ x^ ancho^ )
        STH2kr ( ptra: x^ ancho^ / pret: limite-y^ x^ ancho^ )
        ;dibuja-tiles-en-fila JSR2 ( ptra: / pret: limite-y^ x^ ancho^ )

        .Pantalla/y DEI2 #0008 ADD2 DUP2 ( añadir 8 a y )
        .Pantalla/y DEO2 ( almacenar la nueva y )

        ( recuperar limite-y )
        ROT2r ( ptra: y^ / pret: x^ ^ limite-y^ )
        STH2kr ( ptra: y^ limite-y^ / pret: x^ ancho^ limite-y^ )

        ( reordenar la pila de retorno )
        SWP2r ( ptra: y^ limite-y^ / pret: x^ limite-y^ ancho^ )
        ROT2r ( ptra: y^ limite-y^ / pret: limite-y^ ancho^ x^ )

        LTH2 ,&bucle-y JCN ( salta si x es menor que el límite )

     POP2r POP2r POP2r ( limpiar la pila de retorno )
RTN

podemos entonces llamarlo de la siguiente manera para obtener un cuadrado de 256x256 lleno de tiles:

#0008 #0008 ( `x` y `y` )
#0100 #0100 ( ancho y alto )
;tile-fondo 
;dibuja-tiles JSR2
captura de pantalla que muestra un gran cuadrado en la pantalla varvara compuesto por líneas diagonales

usando variables

comparemos el enfoque anterior con el uso de variables relativas.

iremos "con todo" de una manera relativamente derrochadora, sin optimizar los procedimientos que podrían beneficiarse de la manipulación de la pila.

declararemos las siguientes etiquetas para nuestras variables, después del RTN que termina la subrutina:

( variables )
 &alto $2 &ancho $2 &y $2 &x $2 &limite-y $2

ahora, iniciamos la subrutina de la misma manera que antes, estableciendo la dirección para nuestro sprite:

( ptra inicial: x^ y^ ancho^ alto^ direc* )
.Pantalla/direc DEO2 ( ptra: x^ y^ ancho^ alto^ )

entonces, simplemente almacenamos los siguientes valores en direcciones relativas:

,&alto STR2
,&ancho STR2

nótese que vamos en orden inverso.

después de estas operaciones las pilas están vacías.

entonces podemos fijar la `y` inicial y calcular el límite vertical, utilizando los valores almacenados en las variables:

( establecer `y` inicial )
,&y LDR2 DUP2 ( ptra: y^ y^ )
.Pantalla/y DEO2 ( ptra: y^ )

( calcular limite-y )
,&alto LDR2 ( ptra: y^ alto^ )
ADD2 ( ptra: limite-y^ )
,&limite-y STR2 ( ptra: )

nuestro bucle ahora tendría el siguiente aspecto:

 &bucle-y
   ( recuperar `x` y ancho )
   ,&x LDR2 
   &ancho LDR2
   ( dibujar fila )
   ;dibuja-tiles-en-fila JSR2 

   .Pantalla/y DEI2 #0008 ADD2 DUP2 ( añade 8 a y )
   .Pantalla/y DEO2 ( almacenar la nueva y )

   ( recuperar el límite vertical )
   ,&limite-y LDR2

   LTH2 ,&bucle-y JCN ( saltar si x es menor que el límite )

¡y eso es todo!

compara esto con la versión "concreta" que desarrollamos anteriormente, ¡es muy similar en su estructura!

la subrutina completa tendría el siguiente aspecto:

@dibuja-tiles ( x^ y^ ancho^ alto^ direc* -- )
    ( establecer la dirección del tile )
    .Pantalla/direc DEO2 ( ptra: x^ y^ ancho^ alto^ )

    ( almacenar valores )
    ,&altura STR2
    ,&ancho STR2
    &y STR2
    ,&x STR2
    
    ( establecer `y` inicial )
    ,&y LDR2 DUP2 ( ptra: y^ y^ )
    .Pantalla/y DEO2 ( ptra: y^ )

    ( calcular el límite vertical )
    ,&alto LDR2 ( ptra: y^ alto^ )
    ADD2 ( ptra: limite-y^ )
    ,&limite-y STR2 ( ptra: )
    
    &bucle-y
       ( recuperar `x` y ancho )
       ,&x LDR2 
       ,&ancho LDR2
       ( dibujar fila )
       ;dibuja-tiles-en-fila JSR2 
    
       .Pantalla/y DEI2 #0008 ADD2 DUP2 ( añade 8 a y )
       .Pantalla/y DEO2 ( almacenar la nueva y )
    
       ( recuperar  el límite vertical )
       ,&limite-y LDR2
    
       LTH2 ,&bucle-y JCN ( saltar si x es menor que el límite )

RTN
( variables )
 &alto $2 &ancho $2 &y $2 &x $2 &limite-y $2

como he dicho antes, podemos encontrar aquí algunas oportunidades de optimización.

tal vez el límite vertical puede ser escondido en la pila de retorno como en el bucle dibuja-tiles-en-fila, o tal vez la variable para la altura y para la `y` inicial no son necesarios.

dejo que lo resuelvas :)

nota que esta subrutina tal como está requiere 24 bytes de memoria de programa más que la versión de la pila.

en nuestro caso eso no es un gran problema, pero es una buena manera de evaluar nuestras prioridades: código súper legible pero probablemente ineficiente (como esta última subrutina), código súper optimizado pero probablemente ilegible (código de sólo escritura, dicen), o algo en el medio.

dibuja-fondo

ahora que tenemos estas bonitas subrutinas, podemos simplemente envolverlas en otra que cubrirá todo el fondo con nuestro tile elegido.

por ejemplo:

@dibuja-fondo ( -- )
    #0000 #0010 ( x e `y` iniciales )
    .Pantalla/ancho DEI2
    .Pantalla/alto DEI2 #0010 SUB2
    ;tile-fondo 
    ;dibuja-tiles JSR2
RTN

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

;dibuja-fondo JSR2
;dibuja-fondo JSR2
captura de pantalla mostrando la pantalla varvara cubierta de líneas diagonales.

¡bonito! te recomiendo que te tomes un pequeño descanso; ¡esto ha sido pesado!

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