Pong, el juego

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.

El juego de pong completo
Título: Pong.tar (215 clicks)
Leyenda: El juego de pong completo
Filename: pong-tar.gz
Size: 667 KB

Publicado en Programación Etiquetado con: , , , , , , , ,
3 Comentarios en “Pong, el juego
  1. jsbsan dice:

    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?

    • jguardon dice:

      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!

  2. aztk dice:

    Muy buen código, quedo bien lo de la música.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*