compudanzas/src/tutorial_de_uxn_día_6.gmo

1699 lines
53 KiB
Plaintext

# tutorial uxn: día 6, hacia el pong
lang=es en->{uxn tutorial day 6}
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 con el byte auto de pantalla 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
antes de adentrarnos en dibujar las palas y la pelota vamos a cubrir el byte auto de pantalla que mencionamos en el {tutorial de uxn día 2}, pues nos ayudara a dibujar estos elementos.
# 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.
## configurando
empecemos con el siguiente programa como plantilla. incluye los datos para un sprite de 1bpp que consiste en líneas diagonales.
```
( hola-pong.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 &píxel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &botón $1 &tecla $1 ]
( macros )
%RTN { JMP2r }
( programa principal )
|0100
@configuración
( establecer los colores del sistema )
#2eef .Sistema/r DEO2
#1eb8 .Sistema/g DEO2
#1e2e .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:
* dibujar tile en x
* sumar 8 (el tamaño del tile) a x
* ¿es x menor que el límite? si lo es, repite el procedimiento, si no, termina
### una primera versión
digamos que nuestra x inicial es 0000, nuestro ancho es el ancho de la pantalla y el tile que estamos dibujando es tile-fondo.
el límite del bucle, x+ancho, sería también el ancho de la pantalla.
el primer paso, dibujar el tile en x sería:
```
;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
#0000 .Pantalla/x DEO2 ( establecer la x inicial )
#03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
```
el segundo paso, añadir 8 a x, ya lo sabemos:
```
.Pantalla/x DEI2 #0008 ADD2 ( añadir 8 a x )
.Pantalla/x DEO2 ( almacenar nueva x )
```
comprobar si x es menor que el límite, saltando si lo es, sería algo así:
```
.Pantalla/x DEI2 ( obtener x )
.Pantalla/ancho DEI2 ( obtener 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 ( fijar la dirección del tile )
#0000 .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 ( añadir 8 a la x )
DUP2 ( duplicar la nueva x )
.Pantalla/x DEO2 ( almacenar nueva x )
.Pantalla/ancho DEI2 ( obtener el límite )
LTH2 ,&bucle-x JCN ( saltar si x es menor que el límite )
```
nótese el uso de DUP2 para evitar releer el valor de x.
¡esto debería funcionar ahora! pero vamos a discutir una forma más agradable de hacerlo :)
### una segunda versión, usando la pila
en lugar de leer el ancho de pantalla y la coordenada x cada vez, podríamos usar la pila para almacenar y manipular esos valores.
después de establecer la dirección del tile, podemos empujar nuestro límite (el ancho de la pantalla) y el valor inicial hacia abajo en la pila:
```
.Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
```
usaremos ese valor en la parte superior de la pila como coordenada x.
dentro del bucle, podemos duplicarlo para establecerlo como la x de la pantalla, e incrementarlo.
entremedio, podemos enviar nuestro byte de sprite para dibujar el tile.
```
&bucle-x
DUP2 .Pantalla/x DEO2 ( establecer coordenada x )
#03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
#0008 ADD2 ( incrementar x )
```
en este punto, la pila tiene la nueva x en la parte superior de la pila y el ancho de la pantalla debajo.
podemos compararlos en modo mantener para conservar esos valores en la pila y hacer nuestro salto como antes:
```
GTH2k ( ¿es ese ancho mayor que x?, o también, ¿es x menor que el ancho? )
,&bucle-x JCN ( salta si x es menor que el límite )
```
cuando terminamos el bucle, tenemos que hacer POP a ambos valores.
usando esta estrategia, obtendríamos el siguiente bucle:
```
;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
.Pantalla/ancho DEI2 #0000 ( empujar límite y `x` inicial )
&bucle-x
DUP2 .Pantalla/x DEO2 ( establecer coordenada x )
#03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
#0008 ADD2 ( incrementar x )
GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
,&bucle-x JCN ( salta si x es menor que el límite )
POP2 POP2 ( eliminar x y el límite )
```
no solo es un código más corto, sino que también es más rápido porque realiza menos operaciones dentro del bucle.
¡es bueno tenerlo en cuenta!
### programa completo
lo siguiente muestra nuestro programa en contexto, llenando completamente la primera fila de nuestra pantalla con nuestro tile:
=> ./img/screenshot_uxn-background-row.png captura de pantalla mostrando la primera fila de la pantalla varvara rellenada con líneas diagonales
```
( hola-pong.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 &píxel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &botón $1 &tecla $1 ]
( macros )
%RTN { JMP2r }
( programa principal )
|0100
@configuración
( establecer los colores del sistema )
#2eef .Sistema/r DEO2
#1eb8 .Sistema/g DEO2
#1e2e .Sistema/b DEO2
( dibujar fondo )
;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
.Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
&bucle-x
DUP2 .Pantalla/x DEO2 ( fijar coordenada x )
#03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
#0008 ADD2 ( incrementar x )
GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
,&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?
siguiendo la misma estrategia, podríamos hacer:
* dibujar la fila en y
* añadir 8 (el tamaño del tile) a y
* ¿es `y` menor que el límite? si lo es, repetir el procedimiento, en caso contrario terminar
### el bucle vertical
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 )
.Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
MARGEN-PARED ( establecer `y` inicial )
&bucle-y
DUP2 .Pantalla/y DEO2 ( establecer coordenada y )
( - dibujar fila aquí - )
#0008 ADD2 ( incrementa `y` )
GTH2k ( ¿es ese límite mayor que `y`? o también, ¿es `y` menor que el límite? )
,&bucle-y JCN ( salta si `y` es menor que el límite )
POP2 POP2 ( eliminar `y` y el límite )
```
### bucles anidados
ahora que tenemos esta estructura, podemos sustituir el comentario "dibujar fila aquí" por nuestro bucle horizontal anterior:
```
;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
.Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
MARGEN-PARED ( establecer `y` inicial )
&bucle-y
DUP2 .Pantalla/y DEO2 ( establecer coordenada y )
( dibujar fila )
.Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
&bucle-x
DUP2 .Pantalla/x DEO2 ( fijar coordenada x )
( dibujar sprite de 1bpp con color 3 y 0 )
#03 .Pantalla/sprite DEO
#0008 ADD2 ( incrementar x )
GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
,&bucle-x JCN ( salta si x es menor que el límite )
POP2 POP2 ( eliminar x y el límite )
#0008 ADD2 ( incrementar y )
GTH2k ( ¿es ese límite mayor que y? o también, ¿es `y` menor que el límite? )
,&bucle-y JCN ( salta si `y` es menor que el límite )
POP2 POP2 ( eliminar `y` y el límite )
```
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 )
.Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
MARGEN-PARED ( establecer `y` inicial )
&bucle-y
DUP2 .Pantalla/y DEO2 ( establecer coordenada y )
( dibujar fila )
.Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
&bucle-x
DUP2 .Pantalla/x DEO2 ( establecer coordenada x )
( dibujar sprite de 1bpp con color 3 y 0 )
#03 .Pantalla/sprite DEO
#0008 ADD2 ( incrementar x )
GTH2k ( ¿es ese ancho mayor que x? o también, ¿es x menor que el ancho? )
,&bucle-x JCN ( salta si x es menor que el límite )
POP2 POP2 ( eliminar x y el límite )
#0008 ADD2 ( incrementar y )
GTH2k ( ¿es ese límite mayor que `y`? o también, ¿es `y` menor que el límite? )
,&bucle-y JCN ( salta si `y` es menor que el límite )
POP2 POP2 ( eliminar `y` y el límite )
RTN
```
que podemos llamar simplemente desde nuestra subrutina de iniciación:
```
;dibuja-fondo JSR2
```
=> ./img/screenshot_uxn-background-full.png captura de pantalla que muestra la pantalla 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 este 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 :)
# el byte auto de pantalla
el byte auto de pantalla es una característica avanzada del dispositivo de pantalla que nos permite dibujar fácilmente múltiples sprites con una sola llamada.
no lo cubrimos en el {tutorial de uxn día 2} para evitar añadir más complejidad, y también porque realmente brilla cuando se utiliza el modo mantener que acabamos de discutir en el {tutorial de uxn día 5}.
# campos
el nibble izquierdo del byte corresponde a la longitud del byte auto.
los cuatro bits restantes, numerados de derecha a izquierda y de 0 a 3, son banderas que indican lo siguiente:
+ <table>
+ <tr><th>bit</th><th>flag</th></tr>
+ <tr><td class="num">3</td><td>sin usar</td></tr>
+ <tr><td class="num">2</td><td>dirección automática</td></tr>
+ <tr><td class="num">1</td><td>auto y</td></tr>
+ <tr><td class="num">0</td><td>auto x</td></tr>
+ </table>
& * 3: sin usar
& * 2: dirección automática
& * 1: auto y
& * 0: auto x
## incrementando la posición
la primera aplicación del byte auto de pantalla es incrementar automáticamente las coordenadas x o y del dispositivo de pantalla después de dibujar un sprite.
### sin el byte auto
por ejemplo, sin el byte auto, si quisiéramos dibujar un sprite justo a la derecha de otro, haríamos algo como lo siguiente, leyendo la coordenada x de la pantalla, incrementándola, y luego guardándola de nuevo:
```
;cuadrado .Pantalla/direc DEO2 ( establecer dirección del sprite )
#41 .Pantalla/sprite DEO ( dibujar sprite )
( incrementar Pantalla/x por 8 píxeles: )
.Pantalla/x DEI2 #0008 ADD2 .Pantalla/x
#41 .Pantalla/sprite DEO ( dibujar sprite de nuevo )
```
la operación para incrementar la Pantalla x es sencilla, pero es tan común que el byte auto la internaliza.
## sin el byte autom
para replicar el mismo comportamiento podemos activar la bandera auto x, y en su lugar hacer:
```
;cuadrado .Pantalla/direc DEO2 ( establecer dirección del sprite )
( establecer byte auto: auto x )
#01 .Pantalla/auto
#41 .Pantalla/sprite DEO ( dibujar sprite )
#41 .Pantalla/sprite DEO ( dibujar sprite de nuevo )
```
si no hubiéramos puesto el auto byte las dos últimas líneas serían redundantes. sin embargo, ahora que lo hemos incluido, cada una de ellas dibuja un tile en una posición diferente.
### byte auto y modo mantener
notemos cómo en el código anterior estamos empujando en la pila (dos veces) el color y la dirección del puerto del sprite de la pantalla.
¡con el modo mantener podemos usar esos valores pero sin sacarlos de la pila!
por lo tanto, las dos últimas líneas del ejemplo anterior podrían ser escritas en su lugar usando DEOk:
```
#41 .Pantalla/sprite DEOk ( dibujar sprite y mantener valores )
DEO ( dibujar sprite de nuevo )
```
## incrementando la dirección
como veremos con las palas y la pelota del juego, en muchas ocasiones tiene sentido dibujar sprites formados por varios tiles.
adicionalmente, tiene sentido almacenar estos tiles contiguamente en memoria, como hemos hecho antes.
la bandera de dirección auto nos permitirá incrementar el corto de la dirección de pantalla para que apunte a la siguiente dirección en memoria del tile que se acaba de dibujar.
si lo que dibujamos fue un tile de 1bpp, la dirección se incrementará en 8 bytes, y si dibujamos un tile de 2bpp la dirección se incrementará en 16 bytes.
## múltiples sprites
por último, el campo de longitud del byte auto nos permitirá establecer cuántos sprites extra, además del original, queremos dibujar en una fila.
¡para reiterar, con las palas y la pelota de abajo veremos como todos estos campos pueden ser aplicados para dibujar sprites multi-tile!
# 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 cómo dibujar las palas en base a estos parámetros y también recapitularemos cómo cambiar sus coordenadas `y` con el controlador.
## dibujar las palas multi-tile
¡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
```
0 1
2 3
4 5
```
=> https://100r.co/site/nasu.html 100R - nasu
los datos resultantes son los siguientes:
```
@pala
&tile0 [ 3f 7f e7 c3 c3 c3 c3 00 00 18 3c 3c 3c 3c 3c ]
&tile1 [ fc fe ff ff ff ff ff 00 00 00 00 00 00 06 06 ]
&tile2 [ c3 c3 c3 e7 ff ff ff 3c 3c 3c 3c 18 00 00 00 ]
&tile3 [ ff ff ff ff ff ff 06 06 06 06 06 06 06 06 06 ]
&tile4 [ ff ff ff ff ff 7f 3f 00 00 00 00 00 00 00 00 ]
&tile5 [ ff ff ff ff ff fe fc 06 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 escribir 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á borrar la pala antes de moverla, como hemos hecho en días anteriores.
en principio la subrutina debería ser directa: 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.
si no quisiéramos utilizar el byte auto de pantalla podríamos hacer algo como lo siguiente:
* 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
sin embargo, como ese enfoque u otros similares implicarían muchas operaciones tiene sentido utilizar el byte auto.
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-pala ( x^ y^ color -- )
( almacenar color )
STH
( establecer `y` y x iniciales )
.Pantalla/y DEO2
.Pantalla/x DEO2
( establecer la primera dirección del sprite)
;pala-sprite .Pantalla/direc DEO2
( auto byte: largo: +1 sprite )
( establecer dirección auto y auto `y` )
#16 .Pantalla/auto DEO
( obtener el color de la pila de retorno: )
STHr
( dibujar tres filas: )
.Pantalla/sprite DEOk DEOk DEO
RTN
```
¡eso es todo!
notemos el uso del modo mantener al final: DEOk enviará el color al puerto de sprites de la pantalla, pero mantendrá ambos parámetros en la pila para que podamos reutilizarlos.
ahora que la subrutina está lista podemos llamarla, por ejemplo, de la siguiente manera y conseguir que se dibuje nuestra pala:
```
#0008 #0008 #c5 ;dibuja-pala JSR2
```
=> ./img/screenshot_uxn-pong-paddle.png captura de pantalla de la pala dibujada sobre el fondo
## 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:
```
%MARGEN { #0010 }
```
por último, recuperemos nuestra macro MITAD2 de días anteriores:
```
%MITAD2 { #01 SFT2 } ( corto -- corto/2 )
```
### iniciar posiciones
ahora podemos iniciar las posiciones de las palas.
para la x izquierda, podemos simplemente asignar un valor constante:
```
MARGEN .izquierda/x STZ2
```
para la x derecha, podemos restar el margen y el ancho de la pala del ancho de la pantalla:
```
.Pantalla/ancho DEI2
MARGEN SUB2 ANCHO-PALA SUB2
.derecha/x STZ2
```
para centrar las coordenadas `y` podemos restar la altura de la pala a la altura de la pantalla y luego dividir entre dos:
```
.Pantalla/alto DEI2 ALTO-PALA SUB2 MITAD2
DUP2
.izquierda/y STZ2
.derecha/y STZ2
```
### dibujar palas
para dibujar cada pala, podemos hacer el siguiente procedimiento dentro de nuestro vector de pantalla en-cuadro:
```
( dibujar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
```
### el programa hasta ahora
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
```
( hola-pong.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 &píxel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &botón $1 &tecla $1 ]
( macros )
%RTN { JMP2r }
%MITAD2 { #01 SFT2 } ( corto -- corto/2 )
( constantes )
%ANCHO-PALA { #0010 } ( 2 tiles )
%ALTO-PALA { #0018 } ( 3 tiles )
%COLOR-PALA { #c5 }
%MARGEN { #0010 }
%MARGEN-PARED { #0010 } ( margen en la parte superior e inferior )
( página cero )
|0000
@izquierda [ &x $2 &y $2 ]
@derecha [ &x $2 &y $2 ]
( programa principal )
|0100
@configuración
( establecer los colores del sistema )
#2eef .Sistema/r DEO2
#1eb8 .Sistema/g DEO2
#1e2e .Sistema/b DEO2
( establecer vector de pantalla )
;en-cuadro .Pantalla/vector DEO2
( dibujar fondo )
;dibuja-fondo JSR2
( iniciar palas )
MARGEN .izquierda/x STZ2
.Pantalla/ancho DEI2
MARGEN SUB2 ANCHO-PALA SUB2
.derecha/x STZ2
.Pantalla/alto DEI2 ALTO-PALA SUB2
MITAD2 DUP2
.izquierda/y STZ2
.derecha/y STZ2
BRK
@en-cuadro ( -> )
( dibujar pala )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA dibuja-pala JSR2
BRK
```
## movimiento de la pala
para el movimiento de la pala podemos volver a los ejemplos anteriores de mover un sprite. el proceso que hemos seguido es:
* borrar el sprite de la posición actual
* actualizar la posición
* dibujar el sprite en la nueva posición
### borrar o dibujar sprite
ya tenemos el proceso para dibujar nuestras palas:
```
( dibujar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
```
para borrarlos, podemos hacer lo mismo pero usando un byte del sprite correspondiente a borrar el tile en el primer plano:
```
( borrar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
```
donde COLOR-BORRAR en este caso sería:
```
%COLOR-BORRAR { #40 } ( borrar 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-en-movimiento.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:
```
%VEL-PALA { #0001 }
```
todo esto puede ir dentro de su propia subrutina para facilitar la lectura:
```
@actualiza-palas ( -- )
&izquierda
( pala izquierda: botones arriba y abajo )
.Controlador/botón DEI
DUP #10 AND ( comprobar bit para arriba )
,&izquierda-arriba JCN
DUP #20 AND ( comprobar bit para abajo )
&izquierda-abajo JCN
&derecha JMP ( salta si no se ha presionado ninguno de los dos )
&izquierda-arriba
.izquierda/y LDZ2 VEL-PALA SUB2 .izquierda/y STZ2
,&derecha JMP
&izquierda-abajo
.izquierda/y LDZ2 VEL-PALA ADD2 .izquierda/y STZ2
,&derecha JMP
&derecha
( pala derecha: botones ctrl/A y alt/B )
DUP #01 AND ( comprobar bit para A )
,&derecha-arriba JCN
DUP #02 AND ( comprobar bit para B )
&derecha-abajo JCN
&fin JMP ( salta si no se ha presionado ninguno de los dos )
&derecha-arriba
.derecha/y LDZ2 VEL-PALA SUB2 .derecha/y STZ2
,&fin JMP
&derecha-abajo
.derecha/y LDZ2 VEL-PALA ADD2 .derecha/y STZ2
&fin
POP ( hacer POP al valor duplicado del botón )
RTN
```
### procedimiento completo
integrando todo, nuestra subrutina en-cuadro se vería como lo siguiente.
¡ahora somos capaces de mover nuestras palas!
```
@en-cuadro ( -> )
( borrar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
( actualizar palas )
;actualiza-palas JSR2
( dibujar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
BRK
```
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:
```
0 1
2 3
```
estos son sus datos:
```
@pelota-sprite
&tile0 [ 03 0f 1f 39 70 70 f9 ff 00 00 00 06 0f 0f 06 00 ]
&tile1 [ c0 f0 f8 fc fe fe ff ff 00 00 00 00 08 0c 06 06 ]
&tile2 [ ff ff 7f 7f 3f 1f 0f 03 00 00 00 00 18 0f 01 00 ]
&tile3 [ ff ff fe 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:
```
( dentro de configuración )
( iniciar la pelota )
.Pantalla/ancho DEI2 TAM-PELOTA SUB2
MITAD2
.pelota/x STZ2
.Pantalla/alto DEI2 TAM-PELOTA SUB2
MITAD2
.pelota/y STZ2
```
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 borrar 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
( establecer dirección del sprite)
;pelota-sprite .Pantalla/direc DEO2
( byte auto: dibujar +1 sprite )
( establecer dirección auto y auto `y` )
#16 .Screen/auto DEO
( establecer el color desde la pila de trabajo: )
.Pantalla/sprite
( dibujar dos filas: )
DEOk DEO
RTN
```
¡notemos que estamos utilizando un enfoque muy similar al de la subrutina "dibujar-palas"!
para dibujarla solo 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:
* borrar 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 situarse a lo largo de los procedimientos equivalentes para las palas dentro de la subrutina en-cuadro:`
```
( dentro de en-cuadro )
( borrar pelota )
COLOR-BORRAR ;dibuja-pelota JSR2
( actualizar pelota )
;actualiza-pelota JSR2
( dibujar pelota )
COLOR-PELOTA ;dibuja-pelota JSR2
```
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 decrementarlos.
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 de la variable 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:
```
@pelota [ &x $2 &y $2 &vel-x $2 &vel-y $2 ]
```
### diferentes direcciones
si, por ejemplo, iniciamos vel-x con 1:
```
#0001 .pelota/vel-x STZ2
```
podemos hacer que la pelota se mueva hacia la derecha haciendo:
```
( dentro de actualiza-pelota )
.pelota/vel-x LDZ2 ( obtener vel-x )
.pelota/x LDZ2 ( obtener x )
ADD2 ( sumar ambas )
.pelota/x STZ2 ( almacenar nueva x )
```
para moverse hacia la izquierda, podríamos pensar que deberíamos sustituir ADD2 por SUB2. y sí, podríamos hacerlo.
pero, dejando nuestro código como está ahora: ¿hay algún valor de velocidad-x que haga que x se haga más pequeño al sumarlos?
en otros contextos, uno podría decir, ¡"-1"!
pero no hemos utilizado signos negativos en uxn; ¡no podemos!
entonces, ¿existe un valor positivo de vel-x que haga que x se reduzca al sumarlos?
¡normalmente pensaríamos que no lo hay y que la pregunta no tiene sentido!
sin embargo, aquí estamos limitados por 8 o 16 bits. ¿y qué implica eso?
por ejemplo, si tenemos el número ffff (16 bits, todos son unos) y sumamos 0001, ¿qué obtenemos?
```
1111 1111 1111 1111
+ 0000 0000 0000 0001
---------------------
1 0000 0000 0000 0000
```
de acuerdo, es un número mayor, pero el 1 de la izquierda queda fuera de los 16 bits. en otros contextos se llamaría bit de acarreo.
en uxn, el resultado de sumar ffff y 0001 es 0000: decimos que estamos desbordando los 16 bits.
veámoslo al revés: si tenemos 0001 y sumamos ffff, obtenemos 0000, ¡es decir, 1 menos que 1!
si tenemos 0002 y añadimos ffff:
```
0000 0000 0000 0010
+ 1111 1111 1111 1111
--------------------
1 0000 0000 0000 0001
```
¡obtenemos 0001, que es 1 menos que 2!
en general, si sumamos ffff a un número de 16 bits, obtendremos un valor que es 1 menos que él mismo.
¡por lo tanto podemos pensar que ffff es como un "-1"!
para obtener otros "números negativos", observemos lo siguiente: si restamos 1 a ffff, obtenemos fffe. ¿qué ocurre si lo sumamos a 2?
```
0000 0000 0000 0010: 0002
+ 1111 1111 1111 1110: fffe
---------------------
1 0000 0000 0000 0000: 0000
```
¡obtenemos 0! ¡fffe funciona efectivamente como "-2"!
podríamos continuar así obteniendo más y más números "negativos" que funcionan gracias al tamaño restringido de la memoria del ordenador.
volviendo a nuestro código, si iniciamos nuestra velocidad con:
```
#ffff .pelota/vel-x STZ2
```
y luego usamos exactamente el mismo código para actualizar la posición:
```
( dentro de actualiza-pelota )
.pelota/vel-x LDZ2 ( obtener vel-x )
.pelota/x LDZ2 ( obtener x )
ADD2 ( sumar ambas )
.pelota/x STZ2 ( almacenar nueva x )
```
¡habremos disminuido la posición por 1!
podría tener sentido establecer estos valores como macros:
```
%PELOTA-VEL-POS { #0001 } ( +1 )
%PELOTA-VEL-NEG { #ffff } ( -1 )
```
### implementando el movimiento de la pelota
basándonos en lo que acabamos de discutir, podemos empezar nuestra subrutina actualiza-pelota con lo siguiente:
```
@actualiza-pelota ( -- )
( obtener velocidad-x y `x` )
.pelota/vel-x LDZ2 .pelota/x LDZ2
ADD2 ( sumar ambas cosas )
.pelota/x STZ2 ( almacenar nueva x )
( obtener velocidad-y e `y` )
.pelota/vel-y LDZ2 .pelota/y LDZ2
ADD2 ( sumar ambas )
.pelota/y STZ2 ( almacenar nueva y )
RTN
```
si complementamos nuestra rutina de configuración con las velocidades iniciales, podremos ver cómo se mueve la pelota:
```
( dentro de configuración )
( iniciar pelota )
.Pantalla/ancho DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/x STZ2
.Pantalla/alto DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/y STZ2
( iniciar la velocidad de la pelota )
PELOTA-VEL-POS .pelota/vel-x STZ2
PELOTA-VEL-POS .pelota/vel-y STZ2
```
¡wuju! se mueve, pero de momento sale volando :)
## colisiones con las paredes
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 solo 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:
```
( dentro de actualiza-pelota )
&verif-pared-sup
.pelota/y LDZ2
MARGEN-PARED
LTH2 ( ¿es pelota-y menos que el margen? )
,&establecer-vel-pos JCN
,&verif-pared-inf JMP
&establecer-vel-pos
PELOTA-VEL-POS .pelota/vel-y STZ2
&continuar JMP
&verif-pared-inf
```
### pared inferior
aquí el procedimiento sería similar, pero considerando el tamaño de la pelota.
queremos saber si la coordenada `y`, más el tamaño de la pelota, es mayor que la coordenada `y` de la pared inferior.
la coordenada `y` de la pared inferior sería la altura de la pantalla, menos el margen de la pared:
```
(dentro de actualiza-pelota )
&verif-pared-inf
.pelota/y LDZ2 TAM-PELOTA ADD2 ( y + tamaño de la pelota )
.Pantalla/alto DEI2
MARGEN-PARED SUB2 ( altura - margen )
GTH2 ( ¿es la pelota-y mayor que la pared-y? )
,&establecer-vel-neg JCN
&continuar JMP
&establecer-vel-neg
PELOTA-VEL-NEG .pelota/vel-y STZ2
&continuar
```
### actualiza-pelota hasta ahora
nuestra subrutina actualiza-pelota tiene el siguiente aspecto hasta el momento:
```
@actualiza-pelota ( -- )
( actualizar x )
( obtener velocidad-x y x )
.pelota/vel-x LDZ2 .pelota/x LDZ2
ADD2 ( sumar ambas cosas )
.pelota/x STZ2 ( almacenar nueva x )
( actualizar y )
( obtener velocidad-y e `y` )
.pelota/vel-y LDZ2 .pelota/y LDZ2
ADD2 ( sumar ambas cosas )
.pelota/y STZ2 ( almacenar nueva y )
( comprueba las colisiones con las paredes )
&verif-pared-sup
.pelota/y LDZ2
MARGEN-PARED
LTH2 ( ¿es la pelota-y menor que el margen? )
,&establecer-vel-pos JCN
,&verif-pared-inf JMP
&establecer-vel-pos
PELOTA-VEL-POS .pelota/vel-y STZ2
&continuar JMP
&verif-pared-inf
.pelota/y LDZ2 TAM-PELOTA ADD2 ( y + tamaño de la pelota )
.Pantalla/alto DEI2
MARGEN-PARED SUB2 ( altura - margen )
GTH2 ( ¿es la pelota-y mayor que la pared-y? )
,&establecer-vel-neg JCN
&continuar JMP
&establecer-vel-neg
PELOTA-VEL-NEG .pelota/vel-y STZ2
&continuar
RTN
```
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.
```
( dentro de actualiza-pelota )
&verif-pala-izq
.pelota/x LDZ2
MARGEN ANCHO-PALA ADD2
LTH2 ( ¿es la pelota-x menor que el margen + el ancho-de-pala? )
,&x-en-izquierda JCN
,&verif-pala-der JMP
&x-en-izquierda
( ... )
&verif-pala-der
```
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:
```
( dentro de actualiza-pelota )
&x-en-izquierda
.pelota/y LDZ2 DUP2
.izquierda/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
.izquierda/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera )
STHr ( recupera la primera bandera )
AND ( hacer AND en ambas banderas )
&rebote-izquierda JCN
```
donde rebote-izquierda sería:
```
&rebote-izquierda
PELOTA-VEL-POS .pelota/vel-x STZ2
,&fin JMP
```
¿y qué pasa si no se cumplen las dos condiciones a la vez?
podemos dejar que la pelota siga moviéndose, pero comprobando que no ha cruzado la pared izquierda, comparando con 0000.
todo el código x-en-izquierda terminaría pareciendo:
```
( dentro de actualiza-pelota )
&x-en-izquierda
.pelota/y LDZ2 DUP2
.izquierda/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
.izquierda/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera )
STHr ( recupera la primera bandera )
AND ( hacer AND en ambas banderas )
,&rebote-izquierda JCN
.pelota/x LDZ2 #0000 NEQ2 ( ¿ha llegado a la pared? )
,&fin JCN
&reset-izquierda
( aquí puedes aumentar la puntuación de
la pala derecha )
;reset JSR2
,&fin JMP
&rebote-izquierda
PELOTA-VEL-POS .pelota/vel-x STZ2
,&fin JMP
&verif-pala-der
```
"fin" sería una etiqueta al final de la subrutina y "reset" es una subrutina de la cuálf hablaremos más adelante.
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!
este puede ser otro buen ejercicio para ti: ¿cómo comprobarías si la pelota ha cruzado la pared izquierda independientemente de su velocidad?
### pala derecha
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.
```
&verif-pala-der
.pelota/x LDZ2 TAM-PELOTA ADD2 ( pelota-x + tamaño-pelota )
.Pantalla/ancho DEI2 MARGEN SUB2 ANCHO-PALA SUB2
( ¿es la coordenada derecha de la pelota mayor que el ancho de la pantalla - margen - ancho-pala? )
GTH2
,&x-en-derecha JCN
&fin JMP
&x-en-derecha
.pelota/y LDZ2 DUP2
.derecha/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
.derecha/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera )
STHr ( recupera la primera bandera )
AND ( hacer AND en ambas banderas )
,&rebote-derecha JCN
.pelota/x LDZ2
.Pantalla/ancho DEI2 NEQ2 ( ¿ha llegado a la pared? )
,&fin JCN
&reset-derecha
( aquí puedes aumentar la puntuación
de la pala izquierda )
;reset JSR2
,&fin JMP
&rebote-derecha
PELOTA-VEL-NEG .pelota/vel-x STZ2
,&fin JMP
&fin
RTN
```
¡eso debería ser todo! ¡puedes encontrar la subrutina actualiza-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 también cambiar la velocidad: tal vez basado en el cuentafotogramas, 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.
## configuración
```
( hola-pong.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 &píxel $1 &sprite $1 ]
|80 @Controlador [ &vector $2 &botón $1 &tecla $1 ]
( macros )
%RTN { JMP2r }
%MITAD2 { #01 SFT2 } ( corto -- corto/2 )
%DOBLE2 { #10 SFT2 }
( constantes )
%ANCHO-PALA { #0010 } ( 2 tiles )
%ALTO-PALA { #0018 } ( 3 tiles )
%COLOR-PALA { #c5 }
%VEL-PALA { #0001 }
%TAM-PELOTA { #0010 } ( 2 tiles )
%COLOR-PALA { #c5 }
%PELOTA-VEL-POS { #0001 }
%PELOTA-VEL-NEG { #ffff }
%COLOR-BORRAR { #40 }
%MARGEN { #0010 } ( izquierda y derecha )
%MARGEN-PARED { #0010 } ( arriba y abajo )
( página cero )
|0000
@izquierda [ &x $2 &y $2 ]
@derecha [ &x $2 &y $2 ]
@pelota [ &x $2 &y $2 &vel-x $2 &vel-y $2 ]
( configuración )
|0100
@configuración ( -> )
( establecer los colores del sistema )
#2eef .Sistema/r DEO2
#1eb8 .Sistema/g DEO2
#1e2e .Sistema/b DEO2
( establecer el vector de la pantalla )
;en-cuadro .Pantalla/vector DEO2
( dibujar fondo )
;dibuja-fondo JSR2
( iniciar palas )
MARGEN .izquierda/x STZ2
.Pantalla/ancho DEI2
MARGEN SUB2 ANCHO-PALA SUB2
.derecha/x STZ2
.Pantalla/alto DEI2 ALTO-PALA SUB2
MITAD2 DUP2
.izquierda/y STZ2
.derecha/y STZ2
( iniciar pelota )
;reset JSR2
( iniciar velocidad de la pelota )
PELOTA-VEL-NEG .pelota/vel-x STZ2
PELOTA-VEL-POS .pelota/vel-y STZ2
BRK
```
## en-cuadro
```
@en-cuadro ( -> )
( borrar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-BORRAR ;dibuja-pala JSR2
( borrar pelota )
COLOR-BORRAR ;dibuja-pelota JSR2
( actualizar palas )
;actualiza-palas JSR2
( actualizar pelota )
;actualiza-pelota JSR2
( dibujar palas )
.izquierda/x LDZ2 .izquierda/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
.derecha/x LDZ2 .derecha/y LDZ2 COLOR-PALA ;dibuja-pala JSR2
( dibujar pelota )
COLOR-PELOTA ;dibuja-pelota JSR2
BRK
```
## reset
```
@reset ( -- )
( iniciar pelota )
.Pantalla/ancho DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/x STZ2
.Pantalla/alto DEI2 TAM-PELOTA SUB2
MITAD2 .pelota/y STZ2
RTN
```
## relacionado con la pelota
### actualiza-pelota
```
@actualiza-pelota ( -- )
( obtener velocidad-x y x )
.pelota/vel-x LDZ2 .pelota/x LDZ2 ( obtener x )
ADD2 ( sumar ambas cosas )
.pelota/x STZ2 ( almacenar nueva x )
( obtener velocidad-y e `y` )
.pelota/vel-y LDZ2 .pelota/y LDZ2 ( obtener y )
ADD2 ( sumar ambas cosas )
.pelota/y STZ2 ( almacenar nueva y )
( comprobar colisiones con las paredes )
&verif-pared-sup
.pelota/y LDZ2
MARGEN-PARED
LTH2 ( ¿es la pelota-y menor que el margen? )
,&establecer-vel-pos JCN
,&verif-pared-inf JMP
&establecer-vel-pos
PELOTA-VEL-POS .pelota/vel-y STZ2
,&continuar JMP
&verif-pared-inf
.pelota/y LDZ2 TAM-PELOTA ADD2 ( y + tamaño-pelota )
.Pantalla/alto DEI2 MARGEN-PARED SUB2 ( altura - margen )
GTH2
,&establecer-vel-neg JCN
,&continuar JMP
&establecer-vel-neg
PELOTA-VEL-NEG .pelota/vel-y STZ2
&continuar
( comprobar colisiones con las palas )
&verif-pala-izq
.pelota/x LDZ2
MARGEN ANCHO-PALA ADD2
LTH2 ( ¿es la pelota-x menor que el margen + ancho-pala? )
,&x-en-izquierda JCN
,&verif-pala-der JMP
&x-en-izquierda
.pelota/y LDZ2 DUP2
.izquierda/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
.izquierda/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera )
STHr ( recuperar la primera bandera )
AND ( hacer AND en ambas banderas )
,&rebote-izquierda JCN
.pelota/x LDZ2 #0000 NEQ2 ( ¿ha llegado a la pared? )
,&fin JCN
&reset-izquierda
( aquí puedes añadir un punto a la pala derecha )
;reset JSR2
,&fin JMP
&rebote-izquierda
PELOTA-VEL-POS .pelota/vel-x STZ2
,&fin JMP
&verif-pala-der
.pelota/x LDZ2 TAM-PELOTA ADD2
.Pantalla/ancho DEI2 MARGEN SUB2 ANCHO-PALA SUB2
( ¿es pelota-x + tamaño-pelota mayor que la anchura de la pantalla - margen - ancho-pala? )
GTH2
,&x-en-derecha JCN
,&fin JMP
&x-en-derecha
.pelota/y LDZ2 DUP2
.derecha/y LDZ2 TAM-PELOTA SUB2 GTH2 ( primera bandera ) STH
.derecha/y LDZ2 ALTO-PALA ADD2 LTH2 ( segunda bandera )
STHr ( recuperar la primera bandera )
AND ( hacer AND en ambas banderas )
,&rebote-derecha JCN
.pelota/x LDZ2
.Pantalla/ancho DEI2 NEQ2 ( ¿ha llegado a la pared? )
,&fin JCN
&reset-derecha
( aquí puedes añadir un punto a la pala izquierda )
;reset JSR2
,&fin JMP
&rebote-derecha
PELOTA-VEL-NEG .pelota/vel-x STZ2
,&fin JMP
&fin
RTN
```
### dibuja-pelota
```
@dibuja-pelota ( color -- )
( fijar x e `y` iniciales )
.pelota/x LDZ2 .Pantalla/x DEO2
.pelota/y LDZ2 .Pantalla/y DEO2
( establecer dirección del sprite )
;pelota-sprite .Pantalla/direc DEO2
( byte auto: dibujar +1 sprite )
( establecer dirección auto y auto `y` )
#16 .Pantalla/auto DEO
( establecer el color desde la pila de trabajo: )
.Pantalla/sprite
( dibujar dos filas: )
DEOk DEO
RTN
```
## relacionado-a-palas
### actualiza-palas
```
@actualiza-palas ( -- )
&izquierda
( pala izquierda: botones arriba 10 y abajo 20 )
.Controlador/botón DEI
DUP #10 AND ( comprobar bit para arriba )
,&izquierda-arriba JCN
DUP #20 AND ( comprobar bit para abajo )
,&izquierda-abajo JCN
,&derecha JMP ( salta si no se ha presionado ninguno de los dos )
&izquierda-arriba
.izquierda/y LDZ2 VEL-PALA SUB2 .izquierda/y STZ2
,&derecha JMP
&izquierda-abajo
.izquierda/y LDZ2 VEL-PALA ADD2 .izquierda/y STZ2
,&derecha JMP
&derecha
( pala derecha: botones ctrl/A 01 y alt/B 02 )
DUP #01 AND ( comprobar bit para A )
,&derecha-arriba JCN
DUP #02 AND ( comprobar bit para B )
,&derecha-abajo JCN
,&fin JMP ( salta si no se ha presionado ninguno de los dos )
&derecha-arriba
.derecha/y LDZ2 VEL-PALA SUB2 .derecha/y STZ2
,&fin JMP
&derecha-abajo
.derecha/y LDZ2 VEL-PALA ADD2 .derecha/y STZ2
&fin
POP ( hacer POP al valor duplicado del botón )
RTN
```
## dibuja-pala
```
@dibuja-pala ( x^ y^ color -- )
( almacenar color )
STH
( establecer `y` y x iniciales )
.Pantalla/y DEO2
.Pantalla/x DEO2
( establecer dirección del sprite )
;pala-sprite .Pantalla/direc DEO2
( byte auto: largo: +1 sprite )
( establecer dirección auto y auto `y` )
#16 .Pantalla/auto DEO
( obtener el color de la pila de trabajo: )
STHr .Pantalla/sprite
( dibujar tres filas: )
DEOk DEOk DEO
RTN
```
## dibuja-fondo
```
@dibuja-fondo ( -- )
;tile-fondo .Pantalla/direc DEO2 ( establecer la dirección del tile )
.Pantalla/alto DEI2 MARGEN-PARED SUB2 ( establecer límite )
MARGEN-PARED ( establecer `y` inicial )
&bucle-y
DUP2 .Pantalla/y DEO2 ( establecer coordenada `y` )
( dibujar fila )
.Pantalla/ancho DEI2 #0000 ( establecer límite y `x` inicial )
&bucle-x
DUP2 .Pantalla/x DEO2 ( fijar coordenada x )
#03 .Pantalla/sprite DEO ( dibujar sprite de 1bpp con color 3 y 0 )
#0008 ADD2 ( incrementar x )
GTH2k ( ¿es la anchura mayor que x? o también, ¿es x menor que la anchura? )
,&bucle-x JCN ( salta si x es menor que el límite )
POP2 POP2 ( eliminar `x` y el límite )
#0008 ADD2 ( incrementar y )
GTH2k ( ¿es el límite mayor que `y`? o también, ¿es `y` menor que el límite? )
,&bucle-y JCN ( salta si `y` es menor que el límite )
POP2 POP2 ( eliminar `y` y el límite )
RTN
```
## data
```
@tile-fondo 1122 4488 1122 4488
@pala-sprite
&tile0 [ 3f 7f e7 c3 c3 c3 c3 c3 00 00 18 3c 3c 3c 3c 3c ]
&tile1 [ fc fe ff ff ff ff ff ff 00 00 00 00 00 00 06 06 ]
&tile2 [ c3 c3 c3 c3 e7 ff ff ff 3c 3c 3c 3c 18 00 00 00 ]
&tile3 [ ff ff ff ff ff ff ff ff 06 06 06 06 06 06 06 06 ]
&tile4 [ ff ff ff ff ff ff 7f 3f 00 00 00 00 00 00 00 00 ]
&tile5 [ ff ff ff ff ff ff fe fc 06 06 06 06 06 1e 3c 00 ]
@pelota-sprite
&tile0 [ 03 0f 1f 39 70 70 f9 ff 00 00 00 06 0f 0f 06 00 ]
&tile1 [ c0 f0 f8 fc fe fe ff ff 00 00 00 00 08 0c 06 06 ]
&tile2 [ ff ff 7f 7f 3f 1f 0f 03 00 00 00 00 18 0f 01 00 ]
&tile3 [ ff ff fe fe fc f8 f0 c0 06 06 0c 1c 38 f0 c0 00 ]
```
¡wiuf! :)
# más posibilidades
aquí hay algunas posibilidades extra para que practiques y trates de implementar:
* contar y dibujar algún tipo de puntuación
* cambiar el color de la pelota o de la pala cuando chocan
* cambiar la dirección o el tipo de rebote dependiendo de la parte de la pala que golpea la bola
* iniciar el juego cuando se pulsa un botón
* posición "aleatoria" inicial de la pelota
* velocidad variable de la pelota o de las palas
* ¡etc!
¡comparte lo que termines creando en base a todo esto! :)
# día 7
en el {tutorial de uxn día 7} hablamos de los dispositivos del ordenador varvara que aún no hemos cubierto: audio, archivo y fechahora o "datetime".
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} :)