Hola de nuevo.
En la entrada anterior se ha explicado cómo comenzar a programar el juego Pong, vimos qué variables y constantes son necesarias y dimos algún consejo de buenas prácticas. Vimos también los eventos de teclado para que cada jugador disponga de dos teclas para mover su pala. Hoy vamos a terminar el código para que el juego funcione y nos permita medir nuestra pericia con algún compañero o amigo.
Si recapitulamos a entradas anteriores donde se explicaba la forma de animar objetos en un DrawingArea, recordaremos que usábamos un Timer con un delay de 16ms, que equivale mas o menos a 60 fps. El evento del timer se dispara 60 veces por segundo refrescando el DrawingArea que a su vez dispara su evento Draw. Todo el código que situemos dentro del evento Draw se ejecutará 60 veces por segundo, código que en su mayoría son rutinas y cálculos de dibujo, dando la sensación de animación. Veamos el código completo de ese evento y después lo analizamos:
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | Public Sub DrawingArea1_Draw() Paint.AntiAlias = True ' dibujamos el campo de juego Paint.Brush = Paint.Color(Color.White) Paint.LineWidth = 1 Paint.MoveTo(CANVAS_WIDTH / 2, 0) Paint.LineTo(CANVAS_WIDTH / 2, CANVAS_HEIGHT) Paint.Arc(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, 150) Paint.Stroke ' dibujamos la bola Paint.Arc($ball_pos[0], $ball_pos[1], BALL_RADIUS) Paint.Fill() ' dibujamos las palas Paint.LineWidth = PADDLE_WIDTH Paint.MoveTo($paddle1_pos[0], $paddle1_pos[1]) Paint.LineTo($paddle1_pos[0], $paddle1_pos[1] + PADDLE_HEIGHT) Paint.Stroke Paint.MoveTo($paddle2_pos[0], $paddle2_pos[1]) Paint.LineTo($paddle2_pos[0], $paddle2_pos[1] + PADDLE_HEIGHT) Paint.Stroke ' actualizamos posicion de la bola $ball_pos[0] += $ball_vel[0] $ball_pos[1] += $ball_vel[1] ' actualizamos posicion de las palas $paddle1_pos[1] += $paddle1_vel[1] $paddle2_pos[1] += $paddle2_vel[1] ' límites superior e inferior de las palas If $paddle1_pos[1] < 0 Then $paddle1_pos[1] = 0 If $paddle1_pos[1] > CANVAS_HEIGHT - PADDLE_HEIGHT Then $paddle1_pos[1] = CANVAS_HEIGHT - PADDLE_HEIGHT If $paddle2_pos[1] < 0 Then $paddle2_pos[1] = 0 If $paddle2_pos[1] > CANVAS_HEIGHT - PADDLE_HEIGHT Then $paddle2_pos[1] = CANVAS_HEIGHT - PADDLE_HEIGHT ' comprobamos colisiones con las paredes If $ball_pos[1] <= BALL_RADIUS Or If $ball_pos[1] >= CANVAS_HEIGHT - BALL_RADIUS - 1 Then $ball_vel[1] *= -1 Endif ' comprobamos colision con la pala izquierda If $ball_pos[0] <= BALL_RADIUS + PADDLE_WIDTH And $ball_pos[1] >= $paddle1_pos[1] And $ball_pos[1] <= $paddle1_pos[1] + PADDLE_HEIGHT Then $ball_vel[0] *= ACC Else If $ball_pos[0] < PADDLE_WIDTH Then ball_init(True) $score2 += 1 Endif ' comprobamos colision con la pala derecha If $ball_pos[0] >= CANVAS_WIDTH - BALL_RADIUS - PADDLE_WIDTH And $ball_pos[1] >= $paddle2_pos[1] And $ball_pos[1] <= $paddle2_pos[1] + PADDLE_HEIGHT Then $ball_vel[0] *= ACC Else If $ball_pos[0] > CANVAS_WIDTH - PADDLE_WIDTH Then ball_init() $score1 += 1 Endif ' actualizamos puntuaciones Paint.Font.Name = "Arial" Paint.Font.Size = 24 Paint.Text($score1, CANVAS_WIDTH / 4, 40) Paint.Text($score2, (CANVAS_WIDTH / 4) * 3, 40) Paint.Fill End |
En el primer bloque de código (líneas 40 a 47) estamos indicando que el dibujo ha de hacerse en modo suavizado o antialiasing, para que no aparezcan dientes de sierra en las líneas curvas u oblicuas. Indicamos el color y el grosor del trazo, en este caso blanco de 1px y seguidamente dibujamos las líneas que conformarán el campo. Aunque una mesa de ping-pong no lleva un círculo central como los campos de fútbol, decidí dibujarlo por motivos didácticos. El método Paint.MoveTo(x,y) lo que hace es colocar el puntero invisible de dibujo en el punto dado, es decir, que será el punto inicial para la siguiente instrucción. Paint.LineTo(x,y) es la que realmente pinta la línea desde el punto anterior hasta el punto que le pasemos como argumento y además sitúa el cursor invisible en el punto dado para la siguiente operación. Es nuestra responsabilidad indicar al programa en siguientes instrucciones si deseamos partir de ese punto o no, empleando el método .MoveTo(x,y). En las líneas 44 y 45 dibujamos una línea vertical en la mitad exacta del dibujo para delimitar el terreno de juego.
Sin embargo, esto no es aplicable cuando dibujamos un círculo o arco con el método Paint.Arc(). Éste método admite seis argumentos, los tres últimos opcionales, definidos como sigue:
Static Sub Arc ( XC As Float, YC As Float, Radius As Float [ , Angle As Float, Length As Float, Pie As Boolean ] )
Para pintar un círculo basta con establecer los tres primeros que son X, Y como punto de origen y el radio. Si lo que queremos es pintar un sector circular para un gráfico de tarta, por ejemplo, usaremos también el resto de argumentos. Finalmente mediante Paint.Stroke() pintamos realmente las líneas o figuras definidas anteriormente y se resetean la ruta o los puntos definidos con anterioridad.
En las líneas 50 y 51 hacemos lo mismo para pintar la pelota, con la salvedad de que en lugar de ser un círculo vacío, éste va relleno de color. Esto lo conseguimos usando Paint.Fill().
Desde la linea 54 a la 61 lo que hacemos es pintar las palas a ambos lados de la pantalla. En realidad las palas son dos líneas verticales de 10px de anchura, definida mediante Paint.LineWidth.
Desde la línea 63 a la 69 actualizamos la posición de la pelota y de las palas. Recordad la forma de hacerlo, que es sumando el vector de velocidad a la posición actual en cada ciclo. Por eso, si os dais cuenta, todas las variables son globales a nivel de clase (o formulario) y almacenarán valores diferentes en cada ciclo de dibujo, es decir, 60 veces en un segundo. Las constantes también son globales a nivel de clase, pero como ya hemos explicado, sus valores no cambian nunca.
Para evitar que al mover las palas éstas se salgan de la pantalla por arriba o por abajo, hay que limitar su movimiento vertical. Esto es lo que hacemos en el fragmento comprendido entre las líneas 72 a 76.
En el siguiente bloque de código (79 a 81) comprobamos la colisión con las paredes superior e inferior y en caso de producirse, provocamos la reflexión de la trayectoria en el eje Y simplemente cambiando de signo el valor de velocidad, como ya vimos en anteriores entradas.
El bloque siguiente, líneas 83 a 103, contiene la lógica para averiguar si la pelota ha colisionado con la pala, en cuyo caso haremos que rebote igual que hemos explicado antes, pero con cada rebote incrementamos la velocidad en un 10% para añadir dificultad al juego. Para ello debemos calcular la posición exacta de cada pala en cada momento y compararla con la de la pelota es ese momento. Si el punto más cercano de la pelota (el centro de la misma más o menos el radio, según hacia qué lado vaya) coincide con algún punto de la pala, entonces rebotamos la bola. En caso de que la bola alcance el fondo, sobrepasada la línea más interior de las palas, se considera gol y se actualizan los marcadores convenientemente. Se reinicia el juego, sacando la pelota en dirección al jugador que marcó el tanto.
A partir de la línea 106 hasta el final de la rutina, actualizamos el texto de los marcadores pintando en las posiciones calculadas matemáticamente dividiendo el ancho del juego en cuatro partes. Dibujamos en el primer cuarto y en el tercer cuarto los marcadores para cada jugador.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | Public Sub btnStart_Click() Randomize ball_init(Int(Rnd(0, 2))) btnStart.Enabled = False End Public Sub btnStop_Click() $ball_vel = [0, 0] $ball_pos = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2] btnStart.Enabled = True End Public Sub btnReset_Click() Randomize ball_init(Int(Rnd(0, 2))) $score1 = 0 $score2 = 0 btnStart.Enabled = False End |
Finalmente añadimos un poco de lógica a los controles externos que son botones para comenzar, detener y resetear el juego a valores iniciales. Para el saque inicial, tomamos un valor aleatorio que devuelve un entero 0 ó 1, que pasaremos al método ball_init(bool) que acepta un valor opcional booleano como parámetro. En gambas el valor 0 resuelve como False, mientras que 1 es True, por tanto, el resultado es que la pelota saldrá aleatoriamente hacia la derecha o la izquierda pero siempre con un ángulo por encima de la horizontal.
Esto es todo el código relevante para construir el juego completamente funcional. En muy pocas líneas y con controles estándar hemos sido capaces de crear un juego sencillo que nos mantendrá entretenidos un buen rato.
En el programa que he desarrollado, he incluido sonidos usando el componente gb.sdl.sound y la posibilidad de jugar contra la máquina, un solo jugador, aunque el algoritmo no es ninguna ciencia. Simplemente la pala izquierda sigue la posición ‘Y’ de la pelota, por lo que sería imposible ganar contra la máquina… 😉
Espero que hayáis aprendido algo nuevo, esa era la intención. Os dejo la descarga del proyecto completo en gambas más abajo. En próximas entradas estudiaremos los principios de aceleración y fricción y acabaremos haciendo un juego sencillo de naves y asteroides con muchas explosiones!! Hasta entonces, un saludo.
Seria interesante hacer este proyecto siguiendo el paradigma de Programación Orientada a Objetos…
Lo primero que se me ocurre es el objeto balón, con las propiedades velocidad, coordenadas, y sus métodos «posición de inicio», «choque». Aunque también puede ver otro objeto «paleta», que también tiene el método choque con el balón, y choque con las paredes…
Quizás para este ejemplo que es simple, no merezca la pena… pero si puede ser más fácil «adaptarlo» y entender mejor como se relacionarian las clases (partida, tablero, paletas, jugadores, balon, y no se si se me escapa alguna más), ¿no te parece?
El ejemplo es demasiado simple para eso. Lo único que pretende es mostrar el uso del drawing area como base para el dibujo y animación. Pero en los siguientes ejemplos, después de explicar conceptos como velocidad angular y aceleración y fricción, veremos un juego de asteroides «orientado a objetos». Pero eso será un poco más adelante… Gracias por comentar!
Muy buen código, quedo bien lo de la música.