Tutorial Pyxel 5: Puntuación y reseteo

Pyxel

Introducción

Hola, y bienvenido/a/e a esta quinta entrega de mi tutorial de programación de juegos con Python y Pyxel.

En esta entrega vamos a añadir el sistema de puntuación del juego, así como que, tras puntuar, la bola y palas vuelvan a su posición de inicio a la espera de iniciar la nueva partida

Entregas anteriores

Empezando

Lo primero que vamos a hacer es añadir 2 nuevas variables constantes al juego, y que representaran dos estados del juego: STATE_INIT (estado por defecto y tras puntuar) y STATE_PLAYING (jugando)

Para ello, añadimos las siguientes líneas tras SCREEN_W y SCREEN_H:

STATE_INIT = 1 # Inicio del juego y cuando se ha marcado un punto
STATE_PLAYING = 2 # Jugando

El motivo de usar variables constantes para los diversos estados del juego es que, si en algún momento queremos cambiar sus valores, o añadir nuevos valores, no tenemos que estar cambiando todas las líneas que comparen, o asignen, dicho valor, además de que son más fáciles de recordar, mejora la legibilidad del código, y evitar errores en el futuro por equivocarnos de valor en algún momento.

Reseteando posiciones

Ahora vamos ha añadir tanto a Ball como Paddle un método al que llamaremos reset y que en ambos casos reseteara a la posición inicial la bola y las palas, y qué irá al final de cada clase.

Empecemos por Paddle. Primero vamos a añadir el nuevo método:

1
2
3
4
class Paddle():
    def reset(self):
        self.pos_y = (SCREEN_H - self.height) // 2
        self.center = self.pos_y + (self.height // 2)

Como vemos hace lo mismo que cuando creamos una nueva estancia de la clase, que es, primero posicionar la pala en el centro del eje vertical, y luego guardar la posición del centro de la pala.

Ahora vamos a hacer lo mismo en Ball:

1
2
3
4
5
6
class Ball():
   def reset(self):
        self.pos_x = (SCREEN_W - 4) // 2
        self.pos_y = (SCREEN_H - 4) // 2
        self.speed_x = 0
        self.speed_y = 0

En este caso también reiniciamos la velocidad de la bola en ambos ejes.

Detectando cuando se puntúa

Ahora vamos a detectar cuando la bola se sale de la pantalla por la izquierda o la derecha, que es cuando el jugador contrario gana un punto.

Lo primero que vamos a hacer es eliminar las siguientes líneas que están en Ball.update() y que hacen que cambie su dirección cuando choca con los bordes derecho e izquierdo:

1
2
3
4
if self.pos_x - 6 <= 0:
    self.speed_x = self.speed
elif self.pos_x + 6 >= SCREEN_W:
    self.speed_x = self.speed * -1

Ahora en Pyng.__init() vamos a añadir una nueva propiedad, state, que es donde iremos almacenando los estados del juego:

self.state = STATE_INIT

Ahora vamos a reemplazar el código del método Pyng.update() por lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Pyng():
    def update(self):
        if self.state == STATE_PLAYING:
            if pyxel.btn(pyxel.KEY_Q):
                self.paddle1.move('up')
            elif pyxel.btn(pyxel.KEY_A):
                self.paddle1.move('down')
            else:
                self.paddle1.move(None)

            if self.paddle1.checkCollide(self.ball):
                self.ball.changeDir('right')

            if self.paddle2.checkCollide(self.ball):
                self.ball.changeDir('left')

            if self.ball.pos_y < self.paddle2.center:
                self.paddle2.move('up')
            elif self.ball.pos_y > self.paddle2.center:
                self.paddle2.move('down')

            if self.ball.pos_x + (self.ball.radius * 2) <= 0:
                self.p2_score += 1
                self.state = STATE_INIT
            elif self.ball.pos_x >= SCREEN_W:
                self.p1_score += 1
                self.state = STATE_INIT
        elif self.state == STATE_INIT:
            self.ball.reset()
            self.paddle1.reset()
            self.paddle2.reset()

            if pyxel.btn(pyxel.KEY_SPACE):
                self.ball.initMove()
                self.state = STATE_PLAYING

        self.ball.update()

Lo primero que hacemos es comprobar si el estado actual del juego es STATE_PLAYING, o sea, que se está jugando, y si es así, se ejecuta el código que teníamos hasta ahora dentro del método.

Lo más importante dentro de esa sentencia es lo siguiente:

1
2
3
4
5
6
if self.ball.pos_x - self.ball.radius <= 0:
    self.p2_score += 1
    self.state = STATE_INIT
elif self.ball.pos_x + self.ball.radius >= SCREEN_W:
    self.p1_score += 1
    self.state = STATE_INIT

Primero comprobamos si la pelota ya ha sobrepasado el borde izquierdo, y si es así, sumamos un punto al jugador 2, y si no es así, se comprueba si la bola salió por el lado derecho, dándole un punto al jugador 1. Además, en ambos caso se cambia el estado del juego al estado inicial.

Veamos el resto del código:

1
2
3
4
5
6
7
8
9
10
11
12
class Pyng():
    def update(self):
        if self.state == STATE_PLAYING:
        ...
        elif self.state == STATE_INIT:
            self.ball.reset()
            self.paddle1.reset()
            self.paddle2.reset()

            if pyxel.btn(pyxel.KEY_SPACE):
                self.ball.initMove()
                self.state = STATE_PLAYING

Cuando el estado pasa al estado inicial, lo primero que hacemos es resetear la posición de las palas y la bola. Luego tenemos una sentencia donde se comprueba si estando en este estado se pulsa la tecla espacio, y si es así, se inicia el juego.

Si ejecutamos el código debería de pasar como en la imagen tras anotar un punto:

Añadiendo el marcador

Es hora de ir mostrando en pantalla los puntos obtenidos. Para ello vamos a hacer el uso del método pyxel.text:

pyxel.text(x, y, texto, color)

Al final de Pyng.draw() añadimos las siguientes líneas

1
2
pyxel.text((SCREEN_W // 2 - 8), 8, str(self.p1_score), 7)
pyxel.text((SCREEN_W // 2 + 8), 8, str(self.p2_score), 7)

La puntuación del jugador estará situado en el centro de la pantalla, menos 8 píxeles, mientras que la del juego estará 8 píxeles del centro, además ambos tendran un margen superior de 8 píxeles. Pueden estar en cualquier posición, pero para mí así es mejor visualmente, aunque esto puede cambiar en futuras entregas.

El juego se verá así:

Código completo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
113
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
139
140
141
142
143
144
145
146
147
import pyxel
from random import choice


SCREEN_W = 320
SCREEN_H = 240
STATE_INIT = 1 # Inicio del juego y cuando se ha marcado un punto
STATE_PLAYING = 2 # Jugando
STATE_PAUSE = 3 # Juego en pausa


class Paddle():
    def __init__(self, pos_x, width, height):
        self.pos_x = pos_x
        self.width = width
        self.height = height
        self.pos_y = (SCREEN_H - self.height) // 2
        self.center = self.pos_y + (self.height // 2)
        self.speed = 3
        self.color = 7

    def draw(self):
        pyxel.rect(self.pos_x, self.pos_y, self.width, self.height, self.color)

    def move(self, direction):
        if direction == 'up' and self.pos_y > 0:
            self.pos_y += self.speed * -1
        elif direction == 'down' and self.pos_y + self.height < SCREEN_H:
            self.pos_y += self.speed

        self.center = self.pos_y + (self.height // 2)

    def checkCollide(self, ball):
        if (
            (ball.pos_x + 4 >= self.pos_x and ball.pos_x - 4 <= self.pos_x + self.width) and
            (ball.pos_y + 4 >= self.pos_y and ball.pos_y - 4 <= self.pos_y + self.height)
        ):
            return True
        
        return False

    def reset(self):
        self.pos_y = (SCREEN_H - self.height) // 2
        self.center = self.pos_y + (self.height // 2)

    
class Ball():
    def __init__(self):
        self.pos_x = (SCREEN_W - 2) // 2
        self.pos_y = (SCREEN_H - 2) // 2
        self.speed_x = 0
        self.speed_y = 0
        self.speed = 4
        self.radius = 4
        self.color = 7

    def changeDir(self, direction):
        self.speed_x = self.speed if direction == 'right' else self.speed * -1

    def initMove(self):
        x_move = choice(['left', 'right'])
        self.speed_x = self.speed if x_move == 'right' else self.speed * -1

        y_move = choice(['up', 'down'])
        self.speed_y = self.speed if y_move == 'down' else self.speed * -1

    def draw(self):
        pyxel.circ(self.pos_x, self.pos_y, self.radius, self.color)

    def reset(self):
        self.pos_x = (SCREEN_W - 4) // 2
        self.pos_y = (SCREEN_H - 4) // 2
        self.speed_x = 0
        self.speed_y = 0

    def update(self):
        # Comprobamos si la bola choca contra la parte superior o inferior de la pantalla
        if self.pos_y - 6 <= 0:
            self.speed_y = self.speed
        elif self.pos_y + 6 >= SCREEN_H:
            self.speed_y = self.speed * -1

        # Y finalmente movemos la bola
        self.pos_x += self.speed_x
        self.pos_y += self.speed_y


class Pyng():
    def __init__(self):
        self.paddle1 = Paddle(8, 8, 48)
        self.paddle2 = Paddle(SCREEN_W - 16, 8, 48)
        self.ball = Ball()
        self.ball.initMove()
        self.p1_score = 0
        self.p2_score = 0
        self.state = STATE_INIT

        pyxel.init(SCREEN_W, SCREEN_H, 'Pyng')
        pyxel.run(self.update, self.draw)

    def draw(self):
        pyxel.cls(0)
        self.paddle1.draw()
        self.paddle2.draw()
        self.ball.draw()
        pyxel.text((SCREEN_W // 2 - 8), 8, str(self.p1_score), 7)
        pyxel.text((SCREEN_W // 2 + 8), 8, str(self.p2_score), 7)

    def update(self):
        if self.state == STATE_PLAYING:
            if pyxel.btn(pyxel.KEY_Q):
                self.paddle1.move('up')
            elif pyxel.btn(pyxel.KEY_A):
                self.paddle1.move('down')
            else:
                self.paddle1.move(None)

            if self.paddle1.checkCollide(self.ball):
                self.ball.changeDir('right')

            if self.paddle2.checkCollide(self.ball):
                self.ball.changeDir('left')

            if self.ball.pos_y < self.paddle2.center:
                self.paddle2.move('up')
            elif self.ball.pos_y > self.paddle2.center:
                self.paddle2.move('down')

            if self.ball.pos_x + self.ball.radius <= 0:
                self.p2_score += 1
                self.state = STATE_INIT
            elif self.ball.pos_x + self.ball.radius >= SCREEN_W:
                self.p1_score += 1
                self.state = STATE_INIT
        elif self.state == STATE_INIT:
            self.ball.reset()
            self.paddle1.reset()
            self.paddle2.reset()

            if pyxel.btn(pyxel.KEY_SPACE):
                self.ball.initMove()
                self.state = STATE_PLAYING

        self.ball.update()


Pyng()

Y esto es todo por el momento. En la siguiente entrega vamos a ir acabando el juego, mejorando algunas mecanicas y añadiendo pantallas de inicio y game over.

Recuerda que el código fuente de cada entrega del tutorial lo tienes disponible en este repositorio. Hasta pronto

Share: