esta es la sexta sección del {tutorial de uxn}! aquí hablamos de cómo podemos integrar todo lo que hemos cubierto para crear subrutinas y programas más complejos para el ordenador varvara.
basamos nuestra discusión en una recreación del clásico juego pong.
además de utilizar estrategias y fragmentos de código anteriores, cubrimos estrategias para dibujar y controlar sprites de varios tiles, y para comprobar las colisiones.
# lógica general
aunque pong pueda parecer sencillo y fácil de programar, una vez que lo analicemos nos daremos cuenta de que hay varios aspectos a tener en cuenta. afortunadamente, la mayoría de ellos se pueden dividir como diferentes subrutinas que podemos discutir por separado.
abordaremos los siguientes elementos en orden:
* dibujo del fondo
* dibujo y movimiento de las palas
* dibujo y rebote de la pelota
# dibujando el fondo: repitiendo un tile
en el {tutorial de uxn día 5} hablamos de una forma de crear un bucle para repetir un tile de 1bpp varias veces seguidas.
aquí ampliaremos ese procedimiento para que también se repita verticalmente en toda la pantalla.
## configuración
empecemos con el siguiente programa como plantilla. incluye los datos para un sprite de 1bpp que consiste en líneas diagonales.
,&bucle-x JCN ( salta si x es menor que el límite )
POP2 POP2 ( eliminar x y el límite )
BRK
@tile-fondo 1122 4488 1122 4488
```
## repitiendo una fila
similar a lo que acabamos de hacer: ¿cuál es el procedimiento que podríamos seguir para repetir verticalmente una fila empezando por y, y terminando en un límite correspondiente a y+altura?
para ilustrar un pequeño cambio, supongamos que queremos tener un margen en la parte superior e inferior de la pantalla. podemos definir este margen como una macro:
```
%MARGEN-PARED { #0010 } ( margen en la parte superior e inferior )
```
nuestra y inicial sería MARGEN-PARED, y nuestro límite sería la altura de la pantalla menos MARGEN-PARED.
podemos usar la misma estructura que antes, pero usando y:
```
;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
observa cómo, al asegurarnos de que nuestro bucle interno deja limpia la pila al terminar, podemos ponerlo allí sin problemas: los valores correspondientes al bucle externo permanecen intactos en la pila.
## subrutina dibuja-fondo
ahora podemos envolver estos bucles anidados dentro de una subrutina:
```
@dibuja-fondo ( -- )
;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
que podemos llamar simplemente desde nuestra subrutina de inicialización:
```
;dibuja-fondo JSR2
```
=> ./img/screenshot_uxn-background-full.png captura de pantalla que muestra la pantalla de varvara cubierta de líneas diagonales excepto por un margen en la parte superior e inferior.
¡lindo!
en el {tutorial de uxn apéndice a} puedes encontrar una discusión detallada de cómo generalizar un procedimiento como éste en una subrutina dibuja-tiles que dibuje un rectángulo arbitrario rellenado con un tile dado.
se habla de varias posibilidades para usar uxntal de esa manera abstracta: yo diría que es muy interesante, pero está definitivamente fuera del alcance para hacer el juego :)
# las palas
podemos pensar en las dos palas del juego como dos rectángulos, cada uno con sus propias coordenadas x e y, y ambos con la misma anchura y altura.
la coordenada x de cada pala puede ser constante, y la coordenada y debe ser de seguro una variable.
en esta parte veremos como dibujar las palas en base a estos parámetros, y también recapitularemos como cambiar sus coordenadas `y` con el controlador.
¡quiero usar las palas como ejemplo de cómo dibujar un sprite compuesto por múltiples tiles!
### los datos
he utilizado nasu para dibujar una pala compuesta por tiles de 2x3 en modo 2bpp. las numeraré de izquierda a derecha y de arriba a abajo de la siguiente manera
&tile5 [ ff ff ff ff ff fe fc 06 06 06 06 1e 3c 00 ]
```
se pueden obtener estos números leyendo la notación hexadecimal en nasu en la parte superior derecha, primero la columna de la izquierda y luego la de la derecha, o utilizando una herramienta como hexdump con el archivo chr correspondiente:
```
$ hexdump -C pong.chr
```
he dibujado el sprite usando el modo de mezcla 85 como indica nasu, pero lo cambiaré a c5 para dibujarlo en el primer plano.
### subrutina de dibujo de la pala
construyamos una subrutina que dibuje las 6 fichas de la pala en el orden correspondiente.
podríamos tener la subrutina recibiendo como argumentos la posición x e y de su esquina superior izquierda:
```
@dibuja-pala ( x^ y^ -- )
```
pero añadamos también un byte de color para el byte del sprite:
```
@dibuja-pala ( x^ y^ color -- )
```
recordemos que estamos utilizando la convención de añadir un signo de intercalación (^) después del nombre de un valor para indicar que es un corto, y un asterisco (*) para indicar que es un corto que funciona como un puntero (es decir, una dirección en la memoria del programa)
por un lado esta segunda versión nos permitiría cambiar de color cuando, por ejemplo, le demos a la pelota, pero lo más importante es que esto nos permitirá limpiar la pala antes de moverla, como hemos hecho en días anteriores.
en principio la subrutina debería ser sencilla: tenemos que establecer las coordenadas x e y de cada una de las fichas, relativas a las coordenadas x e y dadas, y dibujarlas con el color dado.
hay muchas maneras de hacerlo, dependiendo del gusto.
podríamos por ejemplo dibujar los tiles en el siguiente orden, con las siguientes operaciones:
* dibujar el tile 0, luego añadir 8 a x
* dibujar el tile 1, luego añadir 8 a y
* dibujar el tile 3, luego restar 8 a x
* dibujar el tile 2, luego añadir 8 a y
* dibujar el tile 4, luego añadir 8 a x
* dibujar el tile 5
o podríamos hacerlo de forma más tradicional:
* dibujar el tile 0, luego añadir 8 a x
* dibujar el tile 1, luego restar 8 a x, y añadir 8 a y
* dibujar el tile 2, luego añadir 8 a x
* dibujar el tile 3, luego restar 8 a x, y añadir 8 a y
* dibujar el tile 4, luego añadir 8 a x
* dibujar el tile 5
en lugar de restar podríamos recuperar x de la pila de retorno, o de una variable relativa.
una posible ventaja de ir en orden es que podemos incrementar la dirección del sprite en 10 (16 en decimal) para llegar a la dirección del siguiente tile. para esto, y/o para los cambios de coordenadas, podemos aprovechar el auto byte de pantalla.
sin embargo, en este caso voy a ir por la primera opción, y voy a establecer manualmente la dirección para cada tile.
adicionalmente, guardaré el color en la pila de retorno:
( obtener y no mantener el color de la pila de retorno: )
STHr .Pantalla/sprite DEO
RTN
```
¡eso es todo!
ahora podemos llamarlo, por ejemplo, de la siguiente manera y obtener nuestra pala dibujada:
```
#0008 #0008 #c5 ;dibuja-pala JSR2
```
=> ./img/screenshot_uxn-pong-paddle.png captura de pantalla de la pala dibujada sobre el fondo
es posible considerar formas más eficientes de dibujarla. por ejemplo, podríamos tener un dibuja-sprite generalizado que reciba la dirección inicial de un conjunto de tiles, y la anchura y altura en términos de número de tiles:
```
@dibuja-sprite ( x^ y^ ancho alto direc* color )
```
crear eso podría ser un buen ejercicio para probar! en este caso me quedaré con el método manual.
lo bueno de que este proceso esté en una subrutina es que podemos "olvidarnos" de su funcionamiento interno y simplemente usarlo :)
## variables y constantes para las palas
reservemos un espacio en la página cero para las coordenadas x e y de cada pala.
```
( página cero )
|0000
@izquierda [ &x $2 &y $2 ]
@derecha [ &x $2 &y $2 ]
```
mencionamos al principio que la coordenada x es constante; sin embargo, si la convertimos en una variable entonces podemos asignar dinámicamente la posición x de las palas (especialmente la derecha) dependiendo del tamaño de la pantalla.
podemos tener un par de macros para mantener las dimensiones y el color de las palas para usarlas después:
```
%ANCHO-PALA { #0010 } ( 2 tiles )
%ALTO-PALA { #0018 } ( 3 tiles )
%COLOR-PALA { #c5 }
```
un margen para separar las palas de los bordes podría ser agradable también:
omitiendo la definición de las subrutinas dibuja-fondo y dibuja-pala, y como forma de tener un punto de comprobación, ahora mismo nuestro programa tendría el siguiente aspecto:
=> ./img/screenshot_uxn-pong-paddles.png captura de pantalla de las dos palas, centradas verticalmente y con el mismo margen respecto a los lados
%COLOR-BORRAR { #40 } ( limpiar el sprite del primer plano )
```
¡este es un buen recordatorio para revisar las tablas de los bytes de los sprites en el {tutorial de uxn día 2}!
### actualizar posición
para actualizar la posición de nuestras palas, podemos recurrir al ejemplo hola-sprite-enmovimiento.tal del {tutorial de uxn día 4}.
podemos usar las flechas arriba y abajo para cambiar la posición de la pala izquierda, y los botones ctrl y alt (A y B) para cambiar la posición de la pala derecha.
podemos tener una macro para definir la velocidad de la pala, es decir, cuánto sumaremos o restaremos al mover cada cuadro:
nota que somos capaces de mover las palas más allá de los límites de la pantalla.
te invito a que modifiques la subrutina actualiza-palas para que haya un límite en el movimiento de las palas. en el {tutorial de uxn día 4} discutimos algunas posibles estrategias para lograrlo :)
# la pelota
¡ahora vamos a poner la pelota en marcha!
aquí trabajaremos de nuevo con un sprite multi-tile dibujado en relación a las variables x e y para su esquina superior izquierda.
adicionalmente, usaremos esta sección para hablar de las estrategias para la detección de colisiones, con las paredes y las palas.
## dibujando la pelota
usé nasu para dibujar una pelota compuesta por tiles de 2x2 2bpp, ordenados de la siguiente manera:
&tile3 [ ff ff fe fc f8 f0 c0 06 06 0c 1c 38 f0 c0 00 ]
```
podemos definir un par de macros para referirse a sus parámetros:
```
%TAM-PELOTA { #0010 } ( tamaño: 2 tiles por lado )
%COLOR-PELOTA { #c5 }
```
### subrutina para dibujar la pelota
como vamos a dibujar una sola pelota, podemos escribir su subrutina de dibujo de manera que tome sus coordenadas de la página cero en lugar de obtenerlas como argumentos en la pila.
en nuestra página cero podemos definir las etiquetas para las coordenadas:
```
@pelota [ &x $2 &y $2 ]
```
y luego en nuestra subrutina de configuración podemos asignarles valores, por ejemplo, en el centro de la pantalla:
las coordenadas están listas, así que ahora podemos usarlas dentro de nuestra subrutina.
hagamos que la subrutina reciba el color como argumento, para poder limpiar la pelota como hacemos con las palas:
```
@dibuja-pelota ( color -- )
( fijar x e y iniciales )
.pelota/x LDZ2 .Pantalla/x DEO2
.pelota/y LDZ2 . pantalla/y DEO2
( dibujar tile 0 )
;pelota-sprite/tile0 .Pantalla/direc DEO2
( el byte de color ya estaba en la pila )
DUP .Pantalla/sprite DEO
( mover a la derecha )
.Pantalla/x DEI2 #0008 ADD2 .Pantalla/x DEO2
( dibujar tile 1 )
;pelota-sprite/tile1 .Pantalla/direc DEO2
DUP .Pantalla/sprite DEO
( mover hacia abajo )
.Pantalla/y DEI2 #0008 ADD2 .Pantalla/y DEO2
( dibujar tile 3 )
;pelota-sprite/tile3 .Pantalla/direc DEO2
DUP .Pantalla/sprite DEO
( mover a la izquierda )
.Pantalla/x DEI2 #0008 SUB2 .Pantalla/x DEO2
( dibujar tile 2 )
;pelota-sprite/tile2 .Pantalla/direc DEO2
.Pantalla/sprite DEO
RTN
```
para dibujarla, sólo tendríamos que hacer:
```
( dibujar pelota )
COLOR-PELOTA ;dibuja-pelota JSR2
```
=> ./img/screenshot_uxn-pong-paddles-and-ball.png captura de la pantalla mostrando las palas en su posición horizontal pero a diferentes alturas, y la pelota completamente centrada en la pantalla.
## movimiento de la pelota
para el movimiento de la pelota, seguiremos la misma estructura que antes:
* despejar la pelota en la posición actual
* actualizar su posición
* dibujar la pelota en la nueva posición
se vería algo como lo siguiente, y podría sentarse a lo largo de los procedimientos equivalentes para las palas dentro de la subrutina en-cuadro:
ahora vamos a discutir cómo construir esa subrutina actualiza-pelota :)
### contabilizando el cambio de dirección
además de nuestras variables para llevar la cuenta de la posición de la pelota, deberíamos poder llevar la cuenta de la dirección por-eje en la que se mueve.
un enfoque podría ser tener una bandera para cada x e y que indique si debemos incrementarlos o disminuirlos.
otro enfoque podría ser tener una variable de velocidad para cada x e y, que se cambia de acuerdo a la dirección que queremos que la pelota vaya.
utilizaremos este último enfoque con la velocidad, ya que nos ayudará a discutir algunas ventajas de la aritmética de enteros sin signo.
incluimos estas variables en nuestra página cero, complementando las x e y que ya teníamos:
hemos definido la forma general de actualizar la posición de la pelota dada su velocidad en x e y.
¡ahora veamos cómo implementar el clásico "rebote"!
primero, empecemos con las paredes en la parte superior e inferior de la pantalla; recordando que hay un margen (MARGEN-PARED) entre el borde real de la pantalla, y las paredes.
para realizar estas detecciones de colisión, tendríamos que comprobar sólo la coordenada y de la pelota.
como siempre, hay muchas maneras de lograr esto. una podría ser:
* comprobar si la pelota está golpeando alguna de las paredes
* si lo hace, entonces invierte la velocidad
otra:
* comprobar si la pelota está golpeando la pared superior
* si lo hace, entonces pon una velocidad positiva
* si no es así, comprueba si la pelota golpea la pared inferior
* si lo hace, entonces establece una velocidad negativa
en otros lenguajes probablemente sea más fácil escribir lo primero, pero aquí nos quedaremos con lo segundo: por claridad y por la forma en que tendremos que hacer las comprobaciones.
¡en cualquier caso, puede ser un buen ejercicio para ti intentar averiguar cómo "invertir" la velocidad con una sola operación aritmética o lógica!
pista: mira de nuevo las máscaras a nivel de bit discutidas en el {tutorial de uxn día 3} :)
### pared superior
si la pelota golpea la pared superior, significa que su coordenada y es menor que la coordenada y de la pared.
considerando que hay un margen en la parte superior, podemos hacer esta comprobación de la siguiente manera:
puedes probarlo usando diferentes velocidades iniciales dentro de la configuración. ¡la pelota debería estar rebotando en la parte superior e inferior ahora! :)
## colisiones con las palas
¡trabajemos con lo que acabamos de hacer, y adaptémoslo para rebotar con las palas!
### pala izquierda
en primer lugar, podemos identificar si la coordenada x de la pelota estaría golpeando la pala izquierda.
para ello, podemos comprobar si x es menor que la suma del margen y el ancho de la pala.
una vez que sabemos que eso es cierto, podemos ver si la pelota está dentro del alcance vertical de la pala; la coordenada y de la pelota tiene que estar dentro de un cierto rango relativo a la coordenada y de la pelota.
en especifico, si queremos que la pelota pueda rebotar cuando cualquier parte de la pelota golpee cualquier parte de la pala, la coordenada y de la pelota tiene que ser:
* mayor que la coordenada y de la pala menos la altura AND de la pelota
* menor que la coordenada y de la pala más la altura de la pala
si esas dos condiciones se cumplen, entonces podemos establecer una velocidad positiva para x:
esta aproximación de comparar con 0000 es la más fácil, pero ten en cuenta que podría no funcionar si cambias la velocidad de la pelota: podría ocurrir que cruzara la pared pero con una coordenada x que nunca fuera igual a 0.
realmente no podemos comprobar si la coordenada x es menor que 0, porque como hemos comentado anteriormente, eso sería en realidad un número cercano a ffff.
si comprobáramos que la coordenada x es menor que ffff, ¡entonces cada valor posible activaría la bandera de comparación!
para la pala derecha haremos lo mismo que arriba, pero cambiando las comparaciones relativas a la coordenada x de la pelota: usaremos el ancho de la pantalla como referencia para la pared derecha, y a partir de ahí le restaremos el margen y el ancho.
¡eso debería ser todo! ¡puedes encontrar la subrutina de actualización de la pelota completa a continuación!
¡para poder ensamblar y ejecutar el juego, vamos a definir la subrutina reset!
## reset
aquí solo definiremos una subrutina de reinicio o "reset" que devuelva la pelota al centro de la pantalla sin alterar su velocidad:
```
@reset ( -- )
( iniciar pelota )
.Pantalla/ancho DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/x STZ2
.Pantalla/alto DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/y STZ2
RTN
```
sería interesante tener algún mecanismo para cambiar también la velocidad: tal vez basado en el conteo de cuadros, en la posición de las palas, o cualquier otra cosa que elijas.
# el programa completo
aquí está todo el código que hemos escrito hoy:
=> ./img/screencap_uxn-pong.gif animación que muestra el pong en acción: las palas se mueven, la pelota rebota en las paredes superior e inferior y en las palas, y la pelota se reinicia desde el centro cuando la pelota golpea cualquier lado.
este debería ser un final ligero y tranquilo de nuestro recorrido, ya que tiene que ver menos con la lógica de programación y más con las convenciones de entrada y salida en estos dispositivos.
¡primero, te invito a tomar un descanso!
después, ¡sigue explorando y comparte tus descubrimientos!
# apoyo
si te ha gustado este tutorial y te ha resultado útil, considera compartirlo y darle tu {apoyo} :)