martes, 16 de octubre de 2012

UCBLogo: videojuego del frontón



Vamos a realizar ya nuestro primer videojuego, algo sencillo. Para ello programaremos el videojuego del frontón. Como una imagen vale más que mil palabras, pongamos una captura del juego ya realizado para entender de qué va el juego.



Básicamente el juego consta de una pelota (el círculo que vemos), que se va moviendo en una dirección y rebota cuando choca con las pareces del campo. Por un lado el campo está abierto y nosotros controlamos la raqueta (el rectángulo dibujado a la izquierda) con un par de teclas (en nuestro caso q arriba y a abajo). Si la pelota sobrepasa a la raqueta por la izquierda, se pierde, y si acertamos a dar a la pelota, esta rebotará. Además, para que el videojuego sea más ameno, la velocidad de la pelota irá en aumento a medida que la golpeemos.

Para realizar un videojuego de este tipo, nos va a facilitar mucho las cosas definir procedimientos adecuados, uno que dibuje la raqueta, otro que dibuje la pelota, una de fin, etc.

El campo podemos dibujarlo una sola vez, pues nunca se altera. Sin embargo, elementos como la pelota o la raqueta están en movimiento. Simularemos el movimiento borrando el elemento dibujado en una posición y dibujándolo en la posición siguiente.

Definiremos también procedimientos de comienzo (una presentación con información sobre el videojuego y las teclas, tal y como podemos ver en esta captura):



Y de finalización (ver esta otra captura de pantalla):



Los procedimientos que se definen en el juego (como veremos luego en el código) son los siguientes: inicio, presentacion, campo, raqueta, frontón y final. Cada uno ellos sólo se ejecutan una única vez, salvo frontón (que es el procedimiento que presenta recursividad y por lo tanto el alma del juego) y raqueta (que será llamado desde el procedimiento frontón tanto para dibujar la raqueta como para borrarla). Si los representamos en un diagrama de flujo:
Analicemos el código de alguno de ellos:



Procedimiento campo
;
; el procedimiento campo sencillamente dibuja los limites del fronton
;
to campo
penup
setxy -250 -100
pendown
rt 90
fd 500
lt 90
fd 280
lt 90
fd 500
lt 90
fd 10
lt 90
fd 490
rt 90
fd 260
rt 90
fd 490
lt 90
fd 10
rt 180
penup
end

Las tres primeras líneas empiezas por el símbolo ; que significa que son comentarios y por lo tanto no son interpretados por el compilador. Nos sirven para reutilizar el código y para que otras personas puedan entender mejor qué hace cada parte del código.

El comando to y el comando end sirven para iniciar y terminar un procedimiento que estamos definiendo (tal y como vimos en el ejemplo del telesketch), en este caso el procedimiento campo.

Los comandos lt, rt y fd también han sido vistos y sirven para girar a la izquierda, girar a la derecha y avanzar respectivamente y, en definitiva, son los que dibujan el campo.

Usamos, no obstante, 3 comandos nuevos: penup, setxy y pendown que explicaremos a continuación.

El comando setxy -250 -100 sirve para situar la tortuga en la posición de pantalla -250 -100, utilizando referenciar absolutas, es decir, el punto -250, -100 es siempre el mismo independientemente de la posición de la tortuga en ese momento.

Los comandos penup y pendown sirve para indicar, respectivamente, que la tortuga no va a dejar rastro (penup) o que sí lo va a dejar (pendown) en su desplazamiento.


Procedimiento presentacion:

to presentacion
penup
setxy -100 110
setpencolor 4
label "Juego_del_Fronton
setxy -100 80
label "Teclas:_q_arriba_a_abajo
setxy -100 50
setpencolor 7
label "Evita_que_la_bola_salga_del_campo_de_juego
setxy -100 20
label "Con_cada_golpe_la_bola_aumenta_de_velocidad
setxy -100 -10
label "Pulsa_cualquier_tecla_para_empezar
make "var2 rc
end

Esta pantalla no hace más que mostrar texto y esperar a que el usuario pulse cualquier tecla para seguir. Para ello evitamos que la tortuga trace líneas al desplazarse (usando penup) y escribimos texto con diferentes colores. Para escribir en rojo usamos setpencolor 4 (que sirve para indicar que a partir de ahora todo irá en rojo) y para escribir en blanco escribimos setpencolor 7 (que sirve para indicar que a partir de ahora todo se dibuje en blanco). Para conocer los colores no tenemos más que hacer help “setpencolor en el compilador LOGO y nos da la asociación entre el número y el color.

Para escribir texto usamos el comando label, de tal forma que label “Juego_del_Fronton nos escribe Juego_del_Fronton en la posición de pantalla en la que la tortuga está.

La última línea, make “var2 rc, sencillamente crea una variable de nombre var2 que lo único que hace es dar un valor al carácter pulsado que no usaremos, sencillamente lo ponemos porque espera a que el usuario pulse cualquier tecla para continuar.

Procedimiento final
to final
clearscreen
penup
setxy -100 50
label "Has_perdido_la_partida
setxy -100 30
label "Escribe_inicio_para_volver_a_jugar
end

El procedimiento final nos escribe la pantalla final. En este caso está programado para que acabe, por lo que el control volvería a la línea de comandos del compilador. Para volver a jugar habría que teclear el procedimiento de comienzo del juego: inicio.

Procedimiento inicio
El procedimiento inicio es un procedimiento que se ejecuta una sola vez y es el encargado de ejecutar las instrucciones precisas antes de empezar el procedimiento principal: llama a ciertos procedimientos y establece las condiciones iniciales para las variables del juego.

Para este juego vamos a manejar varias variables que deberán tener un valor inicial antes de entrar en el procedimiento principal del juego fronton. Ahora es el momento de hacerlo. Las variables que vamos a manejar son las siguientes:
·        yraqueta: posición en el eje cartesiano “y” de la raqueta (a qué altura está). En este caso como sólo se mueve arriba y abajo y no se mueve izquierda-derecha, una sola variable marca la posición de la raqueta.
·        xbola: posición en el eje cartesiano “x” de la bola.
·        ybola: posición en el eje cartesiano “y” de la bola, pues la bola puede moverse en cualquier parte de la pantalla, siempre que no sobresalga del campo de juego.
·        xvelbola: velocidad en el eje cartesiano “x” de la velocidad de la bola.
·        yvelbola: velocidad en el eje cartesiano “y” de la velocidad de la bola.
·        velocidad: variable que sirve para perder tiempo (a menor tiempo perdido, más rápido irá el juego). Nos sirve para dos cosas: un valor inicial para ajustar a la velocidad del ordenador (no todos los ordenadores compilan igual de rápido) y por otro lado, para cambiar la velocidad del juego durante el transcurso del mismo).
·        tecla: memoriza la última tecla pulsada.

El código en concreto del procedimiento inicio (incluidos algunos comentarios, que son los que van detrás del punto y coma), es el siguiente (llegados a este punto creo que todo debiera ser comprensible):

to inicio
clearscreen
hideturtle
presentacion            ; llama al procedimiento de la pantalla de presentacion
clearscreen
pendown
penpaint
campo                      ; llama el procedimiento de dibujar el campo
make "yraqueta 0    ; posicion inicial de la raqueta
make "xbola 0          ; xbola, ybola son las coordenadas iniciales de la pelota
make "ybola 0
make "xvelbola 1     ; xvelbola, yvelbola son las velocidades iniciales
make "yvelbola 1
make "velocidad 200  ; velocidad sirve para perder tiempo
penup
setxy xbola ybola
pendown
arc 360 5         ; dibujamos la bola
arc 360 4
arc 360 3
penup
setxy -250 yraqueta
pendown
raqueta            ; dibujamos la raqueta
make "tecla 0
fronton
end

Aparece un comando nuevo penpaint que sirve para que la tortuga dibuje al desplazarse (y no borre, algo que se puede hacer con penerase, como veremos posteriormente).

Hay una llamada al procedimiento raqueta que sencillamente dibuja la “raqueta” que no es otra cosa que un rectángulo:

to raqueta
fd 20
rt 90
fd 5
rt 90
fd 20
rt 90
fd 5
rt 90
end

Procedimiento fronton
Este procedimiento es el alma del juego. Para entenderlo, haremos unas pocas consideraciones:

1.- Usando el teclado para poder jugar: hasta ahora hemos usado el teclado de una manera que no nos permitirá realizar muchos videojuegos, pues a con la instrucción make “tecla rc, el compilador no hace nada hasta que no pulsemos una tecla. Para un telesketch, esto está bien porque no hay nada que hacer, pero en este caso (y en general en el resto), sí suceden cosas aunque no haya teclas pulsadas (por ejemplo, la bola debe moverse y rebotar con las paredes tanto si pulso una tecla para mover la raqueta como si no). Para ello añadiremos esta línea de código: if keyp [make "tecla rc], que quiere decir, que sólo actualizaremos la variable tecla (que es la que almacena qué tecla estamos pulsando) si hay alguna tecla pulsada. keyp es un comando que mira si hay o no alguna tecla pulsada. Si no la hay devuelve FALSE, por lo que lo que hay dentro de los corchetes no se lee. Si hay alguna tecla pulsada, devuelve TRUE, por lo que entonces ejecutará lo que hay dentro del paréntesis. La usamos combinada con el condicional if, comando que nos permite introducir condiciones y es la que decidirá si ejecutar el código entre corchetes o no en función de que la condición inicial se cumpla (si se cumple lo ejecuta y si no se cumple no se ejecuta).

2.- Movimiento de la raqueta: la raqueta es controlada por el teclado. Inicialmente está quieta. Una vez que pulsamos una tecla (arriba o abajo) se moverá en la dirección adecuada hasta que cambiemos la tecla pulsada. Esto lo conseguimos usando condiciones y cambiando la variable yraqueta (posición de la raqueta) en función de qué tecla haya sido pulsada. Lo que haremos será fácil: miramos dónde está la raqueta, la borramos, añadimos (o disminuimos si es para descender) un píxel a la posición de la raqueta y la volvemos a dibujar.

Veamos este código:

if tecla = "q [penup setxy -250 yraqueta penerase raqueta make "yraqueta yraqueta+1 setxy -250 yraqueta penpaint raqueta]

la primera parte, if tecla = “q, es la que establece la condición (es decir, si pulsamos la tecla “q”). La segunda parte, la que va entre corchetes, es el código que debe cumplir si la tecla “q” es la tecla pulsada. Pues si la tecla “q” es pulsada, lo que debe ocurrir es que la raqueta se mueva hacia arriba. Para ello debemos ir en primer lugar al punto en el que se encuentra la raqueta, así que hacemos penup (que se mueva la tortuga pero que no pinte), luego setxy -250 yraqueta (es decir, que se mueva a la coordenada en “x” -250 que es la coordenada “x” en la que vamos a dibujar siempre la raqueta y a la coordenada en “y” yraqueta, que es la variable que contiene la coordenada en la que actualmente está la raqueta). Si después hacemos seguido penerase y el procedimiento raqueta, lo que hacemos es poner en modo borrado la tortuga y como obligamos a trazar la raqueta, lo que hace es borrarla. Después aumentamos la coordenada en “y” de la raqueta con make “yraqueta yraqueta+1 (aumenta el valor de la variable yraqueta), luego nos vamos a la nueva posición con setxy -250 yraqueta y finalmente dibujamos la raqueta, para ello debemos decir que vuelva a pintar al mover la tortuga con penpaint y finalmente el procedimiento raqueta dibuja la raqueta.

En resumen, y a modo de esquema si dividimos este código:

if tecla = "q                                        à si pulsamos la tecla “q” entonces…
[                                                          à abrimos condicional
penup                                                à levanta la tortuga
setxy -250 yraqueta                         à veta a la posición actual de la raqueta
penerase                                          à modo borrar
raqueta                                              à traza (y borra) la raqueta donde estaba
make "yraqueta yraqueta+1           à el valor de yraqueta aumenta en uno
setxy -250 yraqueta             à vete a la nueva posición de la raqueta
penpaint                                            à modo dibujar
raqueta                                              à traza (y dibuja) la raqueta
]                                                          à fin del condicional


3.- Movimiento de la bola: la bola va a estar moviéndose en cada ciclo de ejecución del procedimiento frontón. Para ello seguimos las ecuaciones del movimiento lineal, es decir, r = r0 + v∙t. En esta ecuación, r es el vector de posición en un momento determinado, r0 el vector de posición en un momento anterior, v el vector de velocidades y t el tiempo transcurrido desde que se pasó de la posición r0 a r. Al ser vectores contemplamos la posibilidad de desplazamientos en varias dimensiones. En nuestro caso tenemos una pantalla plana y los movimientos serán en el plano, por lo que la dimensión de los vectores de posición y velocidades será dos y, por tanto, podemos reducir la ecuación vectorial a dos ecuaciones sencillas, una para el eje de cartesianas “x” y otra para el eje “y” de esta forma:

x = x0 + vx∙ t
y = y0 + vy∙t

en nuestro caso la variable de las coordenadas de la bola son, respectivamente, xbola e ybola y las de las velocidades de la bola xvelbola e yvelbola, por lo que estas ecuaciones anteriores adoptan este código:

make "xbola xbola+xvelbola
make "ybola ybola+yvelbola

En resumen, asignamos a la variable xbola (o ybola) el valor que tenía anteriormente y le sumamos la velocidad (el tiempo adoptamos el criterio de incrementos de uno en uno, por lo que al multiplicar por uno sencillamente no aparece en el código).

Para simular el movimiento de la bola no tenemos más que hacer lo mismo que hicimos para la raqueta, es decir, borrar la bola en la posición actual y dibujarla en la posición nueva. Se podría haber diseñado un procedimiento para el dibujo de la bola, pero en este caso no se hizo, así que esta simulación del movimiento de la bola sería así:

penup
setxy xbola ybola   ;voy a la posición de la bola
pendown
penerase   ;borrar
arc 360 5
arc 360 4
arc 360 3
make "xbola xbola+xvelbola  ;asignación de nueva posicion
make "ybola ybola+yvelbola
penpaint   ;volver a pintar
setxy xbola ybola
arc 360 5
arc 360 4
arc 360 3

Nos aparece un comando nuevo que es el comando arc que sirve para dibujar arcos indicando grados y radio. Lo usamos para dibujar la pelota con 3 circunferencias (360 grados hacen una circunferencia) de radios 3, 4 y 5 píxeles.

4.- Rebote de la bola: para hacer que la bola rebote no tenemos más que hacer uso de las ecuaciones de velocidad. Si una pelota se mueve hacia la derecha y llega a la pared de la derecha, debe rebotar. Esto podemos hacerlo cambiando el signo de la velocidad en el eje “x”, pues si iba hacia la derecha, era una velocidad positiva y como al rebotar se mueve hacia la izquierda, ahora la velocidad en el eje “x” debe ser negativa. Algo tan sencillo como cambiar el signo de las velocidades aplicando este razonamiento al resto de paredes, nos permite simular el rebote de la bola:

if xbola = 234 [make "xvelbola -xvelbola] ;rebote en la pared de la derecha
if ybola = 164 [make "yvelbola -yvelbola] ;rebote en la pared de arriba
if ybola = -83 [make "yvelbola -yvelbola] ; rebote en la pared de abajo

los valores 234, 164 y -83 son valores que se acomodan al dibujo del campo para simular que precisamente en estos valores deben rebotar (si cambiáramos el tamaño del campo estos valores deberían ajustarse).

5.- Ajustar la velocidad de juego antes y durante el mismo: una cuestión importante a tener en cuenta con el compilador es que el videojuego no irá exactamente a la misma velocidad en un ordenador o en otro, pues dependerá de la capacidad de proceso del mismo. Para poder ajustar la jugabilidad podemos definir una variable (en nuestro caso la variable velocidad) que servirá en primer lugar para dar un valor de inicio y así ajustar la velocidad a lo que queremos y, además, para poder ir variando el valor durante el juego y así hacerlo más rápido según transcurra para aumentar la dificultad.

En nuestro ejemplo, en el procedimiento inicio definimos make "velocidad 200, con lo que inicialmente la variable velocidad (que no es más que una indicación de pérdida de tiempo) vale 200.

El código para perder tiempo usado es el siguiente:

for [i 0 velocidad 1] [arc 360 5]  

este código está justo en el momento en el que dibuja la bola. arc 360 5 es una parte de dibujar la bola (tal y como vimos). En este caso vamos a perder tiempo inicialmente dibujando 201 veces una parte de la bola. Para ello usamos un bucle for que indica cuántas veces se ejecutará una instrucción. El primer corchete indica las veces y el modo en que se ejecutará la instrucción, ya que [i 0 velocidad 1] significa que defino una variable que se llama i que va a tomar valores desde 0 hasta el valor de la variable velocidad (que inicialmente valía 200) y que se va a ir incrementando de 1 en 1. La segunda parte [arc 360 5] indica el código que se va a repetir hasta que acabe el bucle for.

6.- Cuándo pierde el jugador: en el caso del frontón el código para que termine el juego es fácil: if xbola = -280 [final stop]. Este código lo único que mira es que si en cualquier momento la coordenada en x de la bola es -280 (la raqueta se dibujaba en -250), entonces es que algo ha pasado para que la bola esté demasiado a la izquierda del campo (en resumen no le habremos dado con la raqueta), por lo que sencillamente llama al procedimiento final (que ejecuta las instrucciones ya vistas en su procedimiento) y para la ejecución del programa con la instrucción stop.

7.- Calculando si la raqueta acierta a dar a la bola: existen varios procedimientos para discernir si la raqueta acierta a la bola. Como la raqueta sólo tiene movimiento en el eje “y”, su coordenada en “x” es fija, por lo que podemos hacer el cálculo cuando la coordenada en “x” de la bola sea la adecuada para que se pueda producir o no el acierto (en nuestro caso, por cuestiones de geometría lo haremos cuando la coordenada “x” de la bola valga -240).  Establecido esto, como la raqueta es un rectángulo y la bola una circunferencia, no tenemos más que calcular una distancia entre las componentes “y” de coordenadas del origen del rectángulo (variable yraqueta) y el centro de la circunferencia (variable ybola), y si esta diferencia es menor que un determinado valor (que la longitud de la raqueta, por ejemplo), establecer el rebote. Esta diferencia la elevaremos al cuadrado para garantizar que siempre comparamos números positivos. Veamos la siguiente figura:



Podemos usar la distancia (siempre sólo para la coordenada cartesiana “y”) entre el centro de la bola (azul) y el centro de la raqueta (rojo). Como el origen desde el que se dibuja la raqueta es el punto verde, esta distancia sería (ybola-(yraqueta+10)), que en el dibujo y para tres bolas distintas sería, respectivamente, d1, d2 y d3. Pues bien, una manera de calcular si la raqueta golpea a la bola sería calcular esta diferencia y si es menor que un cierto valor, la raqueta golpea, algo como if (ybola-(yraqueta+10)) < 15 [comandos]. Sin embargo, este ejemplo daría un falso positivo en el caso de la bola de la figura etiquetada con el número 1, pues d1 que es (ybola-(yraqueta+10)) sería un número negativo (d2 y d3 de la figura serían números positivos). Para evitar este problema elevamos esta distancia al cuadrado, pues al cuadrado cualquier número entero será mayor o igual que cero. Obviamente, habría que elevar al cuadrado también la distancia a comparar, por lo que 15 sería 225.

En resumen, el código escrito para decidir si la bola rebota o no es este:

if xbola = -240 [if ((ybola-(yraqueta+10))*(ybola-(yraqueta+10))) < 225 [make "xvelbola -xvelbola make "velocidad velocidad/1.5]]

en el que recogemos las explicaciones dadas y lo que hace en caso de rebote es cambiar el signo de la velocidad de la bola en la coordenada en “x” para que rebote (make “xvelbola –xvelbola) y aumentar la velocidad de juego al hacer que la pérdida de tiempo (que venía en la variable velocidad) disminuya al dividirlo por un número mayor que uno (make “velocidad velocidad/1.5).

8.- Evitando que la raqueta sobrepase el campo: para evitar que la raqueta sobrepase la altura del campo no tenemos más que incorporar un condicional a la forma en que la raqueta se mueve (visto en el punto 2). Veamos este código:

if yraqueta < 149 [if tecla = "q [penup setxy -250 yraqueta penerase raqueta make "yraqueta yraqueta+1 setxy -250 yraqueta penpaint raqueta]]

tenemos dos condicionales anidados, por lo que, como vemos, la tecla “q” sólo hará ascender la raqueta (todo el código a partir de if tecla... es el visto en el punto 2) si el valor de la coordenada “y” de la raqueta es menor que un cierto número (que obviamente se corresponde con el límite superior del campo).


Código final del Juego

;
; juego del fronton
; Raultecnologia
; ejemplo de aplicacion de programacion en LOGO con el programa UCBLogo
;

;
; el procedimiento campo sencillamente dibuja los limites del fronton
;
to campo
penup
setxy -250 -100
pendown
rt 90
fd 500
lt 90
fd 280
lt 90
fd 500
lt 90
fd 10
lt 90
fd 490
rt 90
fd 260
rt 90
fd 490
lt 90
fd 10
rt 180
penup
end

;
; el procedimiento raqueta dibuja la "raqueta", es decir, el rectangulo que movemos
;
to raqueta
fd 20
rt 90
fd 5
rt 90
fd 20
rt 90
fd 5
rt 90
end

;
; el procedimiento inicio establece una serie de parametros iniciales para el juego
;
to inicio
clearscreen
hideturtle
presentacion            ; llama al procedimiento de la pantalla de presentacion
clearscreen
pendown
penpaint
campo                      ; llama el procedimiento de dibujar el campo
make "yraqueta 0    ; posicion inicial de la raqueta
make "xbola 0          ; xbola, ybola son las coordenadas iniciales de la pelota
make "ybola 0
make "xvelbola 1     ; xvelbola, yvelbola son las velocidades iniciales de la pelota
make "yvelbola 1
make "velocidad 200  ; velocidad es una variable que usaremos para "perder tiempo"
penup
setxy xbola ybola
pendown
arc 360 5         ; dibujamos la bola
arc 360 4
arc 360 3
penup
setxy -250 yraqueta
pendown
raqueta            ; dibujamos la raqueta
make "tecla 0
fronton
end

;
; este procedimiento no es mas que una pantalla inicial con la informacion del juego
;
to presentacion
penup
setxy -100 110
setpencolor 4
label "Juego_del_Fronton
setxy -100 80
label "Teclas:_q_arriba_a_abajo
setxy -100 50
setpencolor 7
label "Evita_que_la_bola_salga_del_campo_de_juego
setxy -100 20
label "Con_cada_golpe_la_bola_aumenta_de_velocidad
setxy -100 -10
label "Pulsa_cualquier_tecla_para_empezar
make "var2 rc
end

;
; en el procedimiento fronton esta el alma del juego
;
to fronton
if keyp [make "tecla rc] ; si hay tecla pulsada, lee el teclado
;
; las siguientes instrucciones miran si podemos mover la raqueta y como lo hacemos
;
if yraqueta < 149 [if tecla = "q [penup setxy -250 yraqueta penerase raqueta make "yraqueta yraqueta+1 setxy -250 yraqueta penpaint raqueta]]
if yraqueta > -89 [if tecla = "a [penup setxy -250 yraqueta penerase raqueta make "yraqueta yraqueta-1 setxy -250 yraqueta penpaint raqueta]]
penup
setxy xbola ybola
pendown
penerase
arc 360 5
arc 360 4
arc 360 3
;
; ahora vienen las condiciones del choque con las paredes y la raqueta
;
if xbola = 234 [make "xvelbola -xvelbola]
if ybola = 164 [make "yvelbola -yvelbola]
if ybola = -83 [make "yvelbola -yvelbola]
if xbola = -240 [if ((ybola-(yraqueta+10))*(ybola-(yraqueta+10))) < 225 [make "xvelbola -xvelbola make "velocidad velocidad/1.5]]
if xbola = -280 [final stop]
make "xbola xbola+xvelbola
make "ybola ybola+yvelbola
penpaint
setxy xbola ybola
arc 360 5
arc 360 4
arc 360 3
for [i 0 velocidad 1] [arc 360 5]   ; esto sirve para perder tiempo
fronton
end

;
; el procedimiento final controla lo que ocurre cuando el juego termina
;
to final
clearscreen
penup
setxy -100 50
label "Has_perdido_la_partida
setxy -100 30
label "Escribe_inicio_para_volver_a_jugar
end