Razón Artificial

La ciencia y el arte de crear videojuegos

Arkanoid V – Creando la bola

Escrito por adrigm el 30 de septiembre de 2010 en Desarrollo Videojuegos, Noticias, Programación | 9 Comentarios.

Después de la pala llega el turno de la bolita, esa bolita con la que tendremos que destruir todos los ladrillos y evitar que se nos cuele debajo de la raqueta. Nosotros la programamos y ellas no putea, así es la vida.

Primero necesitamos una imagen que haga de bola, yo dejo la que uso a continuación, pero si no os gusta os hacéis una.

La clase Ball()

Antes que nada y como siempre creamos el archivo sp_ball.py y en el metemos los siguientes import que nos harán falta.

# -*- encoding: utf-8 -*-

import pygame
import config
import graphics
import math

El constructor

A continuación, pasamos a crear la clase y su constructor.

class Ball(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = graphics.load_image(config.sprites+"ball.png", True)
        self.rect = self.image.get_rect()
        self.rect.centery = config.height/2
        self.rect.centerx = (config.width-140)/2
        self.speed = [0.4, 0.4]

Muy parecido a la pala las diferencias, obviamente la imagen será la de la pelota y esta vez self.rect.centery vale config.height/2 o lo que es lo mismo que está justo en medio de la ventana.

como nuevo tenemos que self.speed es ahora una lista de dos valores, esto es así porque la bola al contrario que la pala se mueve tanto en el eje x como en el eje y.

El método update

En este método trabajaremos durante el juego bastante, pues será el encargado de detectar las colisiones de la bola con todos los elementos y actuar en consecuencia. De momento vamos a programarlo para que rebote contra las paredes y contra la pala que son los elementos que tenemos.

Dejo el método entero y lo explicamos paso a paso.

def update(self, time, pallet):
    self.rect.centerx += self.speed[0] * time
    self.rect.centery += self.speed[1] * time

    # Colisiones con las paredes.
    if self.rect.left <= 10:
        self.rect.left = 10
        self.speed[0] = -self.speed[0]
    if self.rect.right >= 490:
        self.rect.right = 490
        self.speed[0] = -self.speed[0]
    if self.rect.top <= 10:
        self.rect.top = 10
        self.speed[1] = -self.speed[1]
    if self.rect.bottom >= 470:
        self.rect.bottom = 470
        self.speed[1] = -self.speed[1]

    # Colisiones con la pala.
    if pygame.sprite.collide_rect(self, pallet):
        if self.rect.centerx <= pallet.rect.centerx:
            dist = pallet.rect.centerx - self.rect.centerx
            self.speed[0] = -math.sin(math.radians(dist))/1.25
        elif self.rect.centerx > pallet.rect.centerx:
            dist = self.rect.centerx - pallet.rect.centerx
            self.speed[0] = math.sin(math.radians(dist))/1.25
        self.speed[1] = -self.speed[1]

Como diría Jack el Destripador, vamos por partes. Lo primero es calcular la posición de la pelota con respecto a la velocidad y el tiempo como hacíamos con la pala solo que esta vez en dos ejes son las líneas 2 y línea 3 respectivamente.

Colisiones con las paredes

Solo con lo de arriba ya tendría movimiento nuestra pelota, pero ahora hay que hacer que rebote contra los bordes y contra la pala. Esto lo conseguimos jugando con los valores de la velocidad. Por ejemplo, si la pelota se mueve a la velocidad de 0.4 en el eje y significa que va hacia abajo ya que pongamos que la pelota tienen su posición y en el pixel 300 si transcurridos 20 milésimas de segundo calculamos cuando se ha movido sería 0.4*20 que daría 8 por lo que estaría en el pixel 308, por tanto se estaría moviendo hacia abajo (recuerda que la coordenada (0, 0) está en la esquina superior izquierda). Entonces cambiar el sentido de la velocidad bastaría con ponerle un signo menos delante, entonces tendríamos -8 y la posición sería 300-8=292 por lo que habría subido.

Esta explicación que parece obvia y es una cosa básica es buena tenerla en cuenta para los que anden despistados. Jugando con esto vamos a programar los rebotes contra la pared.

# Colisiones con las paredes.
    if self.rect.left <= 10:
        self.rect.left = 10
        self.speed[0] = -self.speed[0]
    if self.rect.right >= 490:
        self.rect.right = 490
        self.speed[0] = -self.speed[0]
    if self.rect.top <= 10:
        self.rect.top = 10
        self.speed[1] = -self.speed[1]
    if self.rect.bottom >= 470:
        self.rect.bottom = 470
        self.speed[1] = -self.speed[1]

Primero comprobamos si ha chocado con la parte izquieda, para ello comprobamos si la parte izquieda de la pelota (self.rect.left) es menor que 10 que es donde se encuentra el borde izquierdo. En caso de que así sea primero establecemos la parte izquieda de la pelota justo en 10. Esto lo hacemos por si llegara a vale 9 u 8 no se vea encima de la pered, recuerda que no dibujamos hasta el final. a continuación cambiamos la dirección de la velocidad del eje x.

Exactamente lo mismo se aplica a la parte derecha, la parte superior y la inferior. Solo que para la superior e inferior lo que variamos es la velocidad en el eje y. Como vemos la velocidad al chocar contra las paredes permanece constante y solo cambia la dirección de la misma.

Colisiones con la pala

Para la pala no nos vamos a limitar a cambiar simplemente la dirección de la bola, sino que vamos a darle más juego y según con la parte que golpeemos consigamos un efecto u otro. Lo que he hecho es que entre más con el extremo de la pala golpees más inclinada salga la pelota hacia el lado con el que le das y si le das con el centro justo de la raqueta salga vertical hacia arriba. Así el jugador tendrá control para decidir a que parte de la pantalla mandar la pelota.

La pala mide de ancho 60px, por lo que del centro a los extremos hay 30px exactos. Lo que vamos a hacer es calcular la diferencia que hay entre el centro de la pelota y el centro de la pala a la hora de golpear. La distancia podra ir desde 0 (cuando le da en el centro justo) hasta 30 (cuando le da con el extremo de la pala). Para obtener la velocidad nos basta con obtener el seno de la distancia resultante ya que el seno de 30 es 0.5 y el seno de 0 es 0 nos da que si le damos con el extemo de la pala saldria a una velocidad de 0.5 en el eje x y si le damos con el centro la velocidad en el eje x sería 0 por lo que saldría hacia arriba sin cambiar de dirección (recuerda que estamos variando el eje x, el eje parmanece intacto). 0.5 por las pruebas que he hecho es algo rápido para jugar bien por lo que es mejor que la velocidad máxima sea 0.4, esto lo conseguimos dicidiendo entre 1.25.

Así explicado puede que maree un poco, veamos el código.

# Colisiones con la pala.
    if pygame.sprite.collide_rect(self, pallet):
        if self.rect.centerx <= pallet.rect.centerx:
            dist = pallet.rect.centerx - self.rect.centerx
            self.speed[0] = -math.sin(math.radians(dist))/1.25
        elif self.rect.centerx > pallet.rect.centerx:
            dist = self.rect.centerx - pallet.rect.centerx
            self.speed[0] = math.sin(math.radians(dist))/1.25
        self.speed[1] = -self.speed[1]

Lo primero de todo es detectar si la pelota está colisionando con la pala. Para ello usamos las funciones que nos provee pygame.

if pygame.sprite.collide_rect(self, pallet):

Recibe dos sprites y devuelve verdadero si están colisionando en nuestro caso le pasamos la bola misma (self) y la pala (pallet) fijaos que la pala se la pasamos como parámetro el metodo update junto con time. Lamentablemente esta función solo comprueban si están colisionando y no en que punto esta colisionando, por lo que tendremos que hacerlo a mano.

Primero detectamos si está colisionando a la izquierda o a la derecha de la pala. Esto es importante para saber en que dirección mandamos la pelota, si la golpeamos con la parte izquierda de la pala la mandaremos hacia la izquierda y si la golpeamos con la derecha la mandaremos a la derecha. Comprobarlo es tan fácil como:

if self.rect.centerx <= pallet.rect.centerx:

Es decir, si el centro de la bola es menor o igual (el igual puede ir tanto a la derecha como a la izquieda ya que la velocidad en x valdrá 0 y da igual si es positiva o negativa) que el centro de la pala. En cuyo caso calculamos la distancia que separa el centro de la pala con el centro de la bola (nos dará un valor entre 0 y 30)

dist = pallet.rect.centerx - self.rect.centerx

Por último le damos el nuevo valor a la velocidad en el eje y que explicamos arriba.

self.speed[0] = -math.sin(math.radians(dist))/1.25

dist tiene un valor entre 0 y 30. La función seno de la biblioteca math recibe los parámetros en radianes así que convertimos dist a radianes con math.radians(dist). Luego atención a la división entre 1.25 que limita la velocidad un 25 por ciento para que no vaya tan rápido. También atención al signo – (menos) que hace que la dirección sea hacia la izquierda.

Para la derecha es exactamente lo mismo, pero sin el signo menos, haciendo que la pelota vaya hacia la derecha.

Por último y como final, también después de colisionar debemos cambiar la velocidad del eje y para que vaya en sentido contrario, lo hacemos con:

self.speed[1] = -self.speed[1]

El método draw

Como en todos los sprites no nos olvidemos de un método para dibujarlo, en este caso es idéntico al de la clase de la pala.

def draw(self, screen):
        screen.blit(self.image, self.rect)

Dejo el archivo entero, para el que se haya perdido.

# -*- encoding: utf-8 -*-

import pygame
import config
import graphics
import math

class Ball(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = graphics.load_image(config.sprites+"ball.png", True)
        self.rect = self.image.get_rect()
        self.rect.centery = config.height/2
        self.rect.centerx = (config.width-140)/2
        self.speed = [0.4, 0.4]

    def update(self, time, pallet):
        self.rect.centerx += self.speed[0] * time
        self.rect.centery += self.speed[1] * time

        # Colisiones con las paredes.
        if self.rect.left <= 10:
            self.rect.left = 10
            self.speed[0] = -self.speed[0]
        if self.rect.right >= 490:
            self.rect.right = 490
            self.speed[0] = -self.speed[0]
        if self.rect.top <= 10:
            self.rect.top = 10
            self.speed[1] = -self.speed[1]
        if self.rect.bottom >= 470:
            self.rect.bottom = 470
            self.speed[1] = -self.speed[1]

        # Colisiones con la pala.
        if pygame.sprite.collide_rect(self, pallet):
            if self.rect.centerx <= pallet.rect.centerx:
                dist = pallet.rect.centerx - self.rect.centerx
                self.speed[0] = -math.sin(math.radians(dist))/1.25
            elif self.rect.centerx > pallet.rect.centerx:
                dist = self.rect.centerx - pallet.rect.centerx
                self.speed[0] = math.sin(math.radians(dist))/1.25
            self.speed[1] = -self.speed[1]
        
    def draw(self, screen):
        screen.blit(self.image, self.rect)

Poniendo una bola en nuestro juego

Una vez creada, vamos a darle vida. Lo primero es crear una instancia en el constructor de SceneGame, como siempre.

self.ball = Ball()

A continuación solo debemos añadir el método update de la bola al método on_update de la escena, ya que al contrario que la pala, la bola no precisa de ningún evento para moverse.

self.ball.update(self.time, self.pallet)

Y por último en el método on_draw le decimos que se dibuje.

self.ball.draw(screen)

Con esto ya tenemos una pelota que rebota contra las paredes y la pala y con esta podemos dirigir hacia donde queremos mandarla. Dejo scene_game.py completo por si no lo véis.

# -*- encoding: utf-8 -*-

import pygame
import scene
import config
import graphics
import load_levels

from sp_pallet import Pallet
from sp_ball import Ball

class SceneGame(scene.Scene):
    """Escena inicial del juego, esta es la primera que se carga cuando inicia"""

    def __init__(self, director):
        scene.Scene.__init__(self, director)
        self.back = graphics.load_image(config.menus+"back_game.png")
        self.pallet = Pallet()
        self.ball = Ball()

    def on_update(self):
         self.time = self.director.time
         self.ball.update(self.time, self.pallet)

    def on_event(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.pallet.update(1, self.time)
        if keys[pygame.K_RIGHT]:
            self.pallet.update(2, self.time)

    def on_draw(self, screen):
        screen.blit(self.back, (0,0))
        self.pallet.draw(screen)
        self.ball.draw(screen)

9 Comentarios en "Arkanoid V – Creando la bola"

  1. Veo que vais avanzando lo que me parece perfecto. Esta serie de artículos ayudarán a muchos novatos a introducirse en el desarrollo de videojuegos.

    Personalmente hubiera gestionado las colisiones de forma externa a los elementos colisionables (Pala o Bola). El CollisionManager detectaría los elementos que colisionan y llamaría a métodos de estos objetos para que reaccionasen. Para mantener la simpleza (keep it simple) comprendo que os hayáis decantado por esta técnica.

    Saludos.

  2. adrigm dice:

    David Saltares, en principio solo va a ver colisiones de la bola o bien con las paredes, o con la pala o los ladrillos. Es decir, solo es la bola la que interactua con los demás objetos por eso decidí que fuera esta la que gestionara con quien colisionaba, pero cierto lo que dices para algo más complejo mejor hacerlo externo a los sprites.

  3. federico dice:

    Me gustaria felicitarte por tu esfuerzo en estos tutoriales, realmente estas ayudando a muchos novatos que quieren iniciarse en este mundo de desarrollo de videojuegos, has hecho un muy buen trabajo.

  4. Pedro Álvaro dice:

    Muchísimas felicidades por el tutorial. Llevaba un tiempo pegándole a pygame y debo decir que has conseguido que comprenda bastantes cosas que tenía dudosas. Muchas gracias.

    Y como seguro que un error tonto que he cometido lo ha hecho alguien más:
    * en el scene_game.py, def on_draw, recordar que los blits han de ponerse por orden de más abajo a más arriba:
    screen.blit(self.back, (0,0))
    self.pallet.draw(screen)
    self.ball.draw(screen)

    Lo dicho, muchas gracias. Te sigo.

  5. Kam dice:

    Me imagino que esto ya no lo vas a terminar… no?

  6. Juan dice:

    Muy buenos los tutoriales para empezar en este mundillo.Es un buen juego para comenzar. Realice hace tiempo un remake del arkanoid en java y pensé algunas cosas muy parecido a como lo explicaste en estos tutoriales.
    ¿Tienes pensado acabar los tutoriales?

    Un saludo

  7. Jos dice:

    Hola adgrim, excelentes tutoriales, quisiera saber si algún día que tengas el suficiente tiempo libre considerarías terminar esta magnifica serie de tutoriales sobre pygame, que a muchos novatos nos vienen como anillo al dedo porque no hay como la practica para aprender

  8. Ezequiel dice:

    No seguiras con los tutoriales? La verdad es que son buenisimos y muy faciles de entender!! Espero que sigas!! Saludos y gracias

  9. Muchas gracias Adrián por los tutoriales. Están genial explicados y, para alguien que se ha iniciado a programar hace dos meses, este tutorial es una bendición para animarse a programar jueguecillos chulos sin ser un experto programando.

    Supongo que como a todos nos gustaría que continuaran, pero lo cierto es que para lo que te hemos pagado (cero patatero) esto es “la reostia” (por si no se me entiende, que están geniales)

    Un abrazo fuerte!

Deja un comentario