Desarrollo de videojuegos multiplataforma con Python
Como crear videojuegos con el Intérprete de Python "Brython" y el Framework para JavaScript "Phaser3"
INTRODUCCIÓN
Como jugador de videojuegos pensé que había llegado la hora de aprender a crear los míos propios, y como entusiasta de Python lo ideal para mí sería programarlos en este lenguaje.
Python tiene muy buenas librerías para crear videojuegos (pygame, pyglet, arcade, kivent...) pero lo ideal sería crear juegos con un framework para todas las plataformas, es decir, quería crear juegos que fuesen tanto para escritorio como para el navegador y dispositivos móviles.
Sin duda la mejor opción hubiese sido descartar Python como lenguaje para este propósito, y en su lugar optar por JavaScript, ya que con este es posible crear aplicaciones y juegos para el navegador, y es posible crear aplicaciones móviles y de escritorio cuya interfaz gráfica sea un Webview.
Sin embargo no disfruto nada programando en JavaScript y por el contrario me encanta Python.
Tenía que hacer algo...
No todo estaba perdido...
EL JUEGO PONG
El juego que muestro a continuación ha sido programado completamente en Python y se ejecuta del lado del cliente en el navegador web:
[ Cuando un jugador puntúe, pulsa la tecla ENTER para lanzar la bola de nuevo ]
¿Como es posible?
Es posible gracias a Brython, el intérprete de Python para el navegador escrito en JavaScript.
He utilizado el Framework Phaser3, que es un framework para el desarrollo de videojuegos con JavaScript, pero el juego ha sido escrito completamente con código Python.
PREPARANDO EL JUEGO
Antes de comenzar a programar el juego con Python tenemos que desplegar un servidor y escribir un poco de html y css
- Crea una carpeta llamada "juego" y en su interior crea tres archivos: index.html, index.css y game.py
- Inicia un servidor web local desde un terminal en la carpeta "juego": python -m http.server
- Abre localhost:8000 en un navegador web.
index.html
El contenido del archivo index.html debe ser el siguiente:
<DOCTYPE html>
<html lang="es">
<head>
<meta charset='utf-8'/>
<!-- Cargamos Phaser y Brython -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/brython-dev/brython/www/src/brython.js"></script>
<!-- Cargamos la hoja de estilos -->
<link rel="stylesheet" href="index.css"/>
</head>
<!-- Iniciamos Brython al cargarse el cuerpo del documento -->
<body onload="brython();">
<!-- Definimos el contenedor del juego -->
<div id="gamebox"></div>
<!-- Cargamos el juego -->
<script type="text/python3" src="game.py"></script>
</body>
</html>
Dentro de la etiqueta head cargamos Phaser y Brython, a continuación cargamos la hoja de estilos index.css:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/brython-dev/brython/www/src/brython.js"></script> <link rel="stylesheet" href="index.css"/>
Iniciamos Brython cuando el cuerpo del documento esté cargado:
<body onload="brython();">
Dentro de la etiqueta body definimos la etiqueta html que será el contenedor del juego:
<div id="gamebox"></div>
Y finalmente, también dentro de body, cargamos el script del juego que será ejecutado por Brython:
<script type="text/python3" src="game.py"></script>
index.css
Damos estilo al contenedor de nuestro juego, el div con id="gamebox":
#gamebox{
width:100vh;
height:100vh;
display:flex;
justify-content:center;
align-items:center;
}
Hemos establecido que #gamebox
ocupe todo el ancho y alto de la pantalla, y que su contenido se encuentre centrado tanto horizontal como verticalmente (así el juego renderizado estará en el centro de la pantalla).
game.py
Escribiremos nuestro juego en el archivo game.py, pero antes vamos a familiarizarnos un poco con Phaser desde el punto de vista de Brython.
PHASER EN BRYTHON
En Brython es posible acceder a los atributos del objeto global de javascript (window) mediante el objeto browser.window
. de modo que para acceder a Phaser desde Brython haremos lo siguiente:
from browser import window
Phaser = window.Phaser
Phaser tiene una Clase Game que se utiliza para iniciar el juego y una Clase Scene para crear las distintas escenas del juego. Un Phaser.Game
manejará una o varias Phaser.Scene
.
Cada una de las Phaser.Scene
debe tener al menos cuatro métodos que se ejecutan secuencialmente al iniciar la escena: init, preload, create y update. El método update
es el bucle que dará vida al juego.
Podríamos implementar un juego de la siguiente forma:
from browser import window
Phaser = window.Phaser
def init(*args):
print('init ejecutado')
def preload(*args):
print('preload ejecutado')
def create(*args):
print('create ejecutado')
def update(time, delta):
print('update ejecutado')
# DEFINIMOS LA ESCENA
scene = dict(
init=init,
preload=preload,
create=create,
update=update
)
# INICIAMOS EL JUEGO
Phaser.Game.new(dict(
width=640,
height=480,
type=Phaser.CANVAS,
parent='gamebox',
scene=[scene]
))
Nuestro juego (Phaser.Game
) unicamente maneja una Phaser.Scene
a la que hemos identificado como scene:
Phaser.Game.new
- Creación del juego. Recibe un diccionario como parámetro (dict
ó{}
). Esto es así porque Brython trasforma los diccionarios python en literales de objetos javascript, es decir, realmente estamos pasando como parámetro un objeto javascript.En este diccionario especificamos las propiedades del juego, en este caso: width, height, type, parent y scene.
type puede ser
Phaser.CANVAS
,Phaser.WEBGL
oPhaser.AUTO
.
parent es el elemento html sobre el que se va a renderizar el juego.
scene es la lista de escenas que manejará el juego. Si el juego solamente cuenta con una escena la lista puede omitirse:scene=scene
scene
- Definición de la escena. Puede ser un diccionario (que brython trasformará en un objeto javscript) en el que agrupamos las cuatro funciones que deben definirse en toda escena de Phaser: init, preload, create y update, o un objetoPhaser.Scene
, de manera que podríamos haber definido nuestra scene de la siguiente forma:scene = Phaser.Scene.new('scene') scene.active = True scene.init = init scene.preload = preload scene.create = create scene.update = update
En este caso la diferencia es que al crear la escena hemos especificado su atributo key (
'scene'
), que será necesario para obtener la escena desde otras escenas del juego. Además hemos especificadoscene.active = True
, que es importante cuando el juego maneja varias escenas y por ejemplo queremos mostrar solamente algunas, o todas al mismo tiempo. Además de esta manera puedes prescindir de la variable this de javascript para referirte a la escena.ME EXPLICO: Si defines la escena con un diccionario necesitas alguna manera de referirte a la propia escena desde las funciones init, preload, create y update para acceder a los atributos y métodos que implementa Phaser y así poder desarrollar el juego, y la única manera sería utilizar this, ya que en este caso
scene
es un diccionario con el quePhaser.Game
creará unaPhaser.Scene
, pero realmente scene no es la escena.Si creas la escena mediante
scene = Phaser.Scene.new('scene')
, en lugar de this puedes utilizar scene, ya que en este casoscene
si es la escena.
Caso 1: Creación de la Escena con un diccionario (no recomendado)
En este caso es necesario utilizar this para referirse a las escena dentro de sus propios métodos init, preload, create y update (y otros que tú definas). En Brython podemos obtener el objeto al que apunta this dentro del método (el objeto propietario de la función en términos de javascript) mediante el uso del módulo javascript de brython:
from browser import window
import javascript # NO OLVIDES IMPORTAR EL MÓDULO
Phaser = window.Phaser
def init(*args):
this = javascript.this()
# utiliza "this" para referirte a la escena y utilizar sus métodos
def preload(*args):
this = javascript.this()
# utiliza "this" para referirte a la escena y utilizar sus métodos
def create(*args):
this = javascript.this()
# utiliza "this" para referirte a la escena y utilizar sus métodos
def update(time, delta):
this = javascript.this()
# utiliza "this" para referirte a la escena y utilizar sus métodos
scene = dict(
init=init,
preload=preload,
create=create,
update=update
)
# scene no es una Phaser.Scene, sino un diccionario que utilizará Phaser.Game
# para crear una Phaser.Scene, por lo que necesitamos acceder a la Phaser.Scene
# mediante javascript.this() dentro de sus propios métodos oara acceder
# a los atributos y métodos de la escena y así desarrollar el videojuego.
# INICIAMOS EL JUEGO
Phaser.Game.new(dict(
width=640,
height=480,
type=Phaser.CANVAS,
parent='gamebox',
scene=[scene]
))
Caso 2: Creación de la escena con Phaser.Scene (recomendado, pero hay una forma mejor)
En este caso no necesitamos usar this, ya que la variable scene
apunta directamente a la Phaser.Scene
, es decir, scene
es la escena:
from browser import window
Phaser = window.Phaser
def init(*args):
# en lugar de "this" utiliza "scene" para referirte a la escena
pass
def preload(*args):
# en lugar de "this" utiliza "scene" para referirte a la escena
pass
def create(*args):
# en lugar de "this" utiliza "scene" para referirte a la escena
pass
def update(time, delta):
# en lugar de "this" utiliza "scene" para referirte a la escena
pass
scene = Phaser.Scene.new('scene')
scene.active = True
scene.init = init
scene.preload = preload
scene.create = create
scene.update = update
# scene es una Phaser.Scene
# INICIAMOS EL JUEGO
Phaser.Game.new(dict(
width=640,
height=480,
type=Phaser.CANVAS,
parent='gamebox',
scene=[scene]
))
El this de javascript no funciona igual que el self de python, por lo que recomiendo evitar usarlo.
Pero lo suyo sería utilizar clases Python que hereden de Phaser.Scene, ¿no?
Efectivamente, si no utilizamos clases y herencia crearemos un código dificil de mantener. Lo que hemos visto hasta ahora puede valer para crear juegos pequeños, pero sin utilizar clases y herencia será muy difícil crear algo mas que pequeños ejemplos como el pong de esta página.
La mala noticia es que no es posible crear clases python que hereden de clases javascript.
Estuve a punto de tirar de surrender, pero...
¡descubrí la Herencia Fake!
HERENCIA FAKE
Prepárate porque vamos a crear una clase que nos va a permitir trabajar con Phaser como si se tratase de una librería Python.
Estuve un par de días estrujándome la cabeza para dar en el clavo. Presta atención a la definición de la Clase Scene:
class Scene:
def __init__(self, active=True):
self.this = Phaser.Scene.new(self.__class__.__name__)
self.this.active = active
self.this.init = self.init
self.this.preload = self.preload
self.this.create = self.create
self.this.update = self.update
def __getattr__(self, attr):
if attr not in self.__dict__:
return self.this[attr]
def __setattr__(self, attr, value):
self.__dict__[attr] = value
self.this[attr] = value
def __call__(self):
return self.this
A partir de esta clase crearás las escenas de tu juego mediante herencia: Ahora es como si self fuese this y te voy a explicar por qué:
Observa el método __init__
de la Clase Scene que definimos previamente:
def __init__(self, active=True):
self.this = Phaser.Scene.new(self.__class__.__name__)
self.this.active = active
self.this.init = self.init
self.this.preload = self.preload
self.this.create = self.create
self.this.update = self.update
Al instanciar un objeto de la Clase Scene, el método __init__
crea el atributo self.this
, que es una Phaser.Scene
cuyo nombre es el de la propia Clase Scene (o el de la clase que herede de Scene):
self.this = Phaser.Scene.new(self.__class__.__name__)
A continuación indicamos si la escena self.this es o no activa, siendo el valor por defecto True
. Finalmente asignamos los métodos init, preload, create y update de la Clase Scene (o de la clase que herede de Scene) a la escena self.this.
La magia necesaria para utilizar self como si fuese self.this se encuentra en la definición de los métodos especiales __getattr__
y __setattr__
:
__getattr__
- Define como se comporta la instancia de clase cuando se le solicita acceso a un atributo arbitrario:def __getattr__(self, attr): if attr not in self.__dict__: return self.this[attr]
Todos los atributos de una instancia de clase se encuentran definidos en su atributo
__dict__
:print(self.attr)
es lo mismo queprint(self.__dict__['attr'])
.Lo que hemos definido es que cuando un atributo (
attr
) no se encuentre enself.__dict__
la función devuelvaself.this[attr]
. Cuando no se cumple esa condición,__getattr__
devuelveself.__dict__[attr]
, pero esto no es necesario especificarlo ya que si__getattr__
no encuentra una sentenciareturn
actúa como cabría esperar por defecto.De este manera podemos acceder a los atributos y métodos de la escena refiriéndonos a self, lo que resulta cómodo porque a veces necesitaremos acceder a atributos y métodos predefinidos y otras veces a los que creemos nosotros mismos. No tenemos que preocuparnos de si los almacenamos en self o en self.this.
__setattr__
- Define como se comporta la instancia de clase cuando debe asignar un valor a un atributo arbitrario:def __setattr__(self, attr, value): self.__dict__[attr] = value self.this[attr] = value
Cuando una instancia de clase asigna un valor a uno de sus atributos (por ejemplo:
self.num = 1
), por defecto se ejecutaself.__dict__['num'] = 1
. Si el atributo no existía se crea insitu.Lo que hemos definido es que siempre que se asigne un valor a un atributo de self se genere un atributo con el mismo nombre en self.this, y que este apunte al mismo objeto que el correspondiente atributo en self.
Así, si queremos establecer un nuevo valor para un atributo de self.this, podremos hacerlos simplemente refiriéndonos a self.
Finalmente, definimos el método especial __call__
para que al invocar a la instancia de la clase nos devuelva a self.this
:
def __call__(self):
return self.this
Ya puedes crear una escena mediante "herencia fake" siendo self la instancia de la clase pero además funcionando como la escena en sí misma:
class MainScene(Scene):
def __init__(self, active=True):
Scene.__init__(self, active)
def init(self, *args):
pass
def preload(self, *args):
pass
def create(self, *args):
pass
def update(self, time, delta):
pass
Finalmente, la base de código en game.py para crear nuestro juego es la siguiente:
from browser import window
Phaser = window.Phaser
class Scene:
def __init__(self, active=True):
self.this = Phaser.Scene.new(self.__class__.__name__)
self.this.active = active
self.this.init = self.init
self.this.preload = self.preload
self.this.create = self.create
self.this.update = self.update
def __getattr__(self, attr):
if attr not in self.__dict__:
return self.this[attr]
def __setattr__(self, attr, value):
self.__dict__[attr] = value
self.this[attr] = value
def __call__(self):
return self.this
class MainScene(Scene):
def __init__(self, active=True):
Scene.__init__(self, active)
def init(self, *args):
pass
def preload(self, *args):
pass
def create(self, *args):
pass
def update(self, time, delta):
pass
Phaser.Game.new(dict(
width=640,
height=480,
type=Phaser.CANVAS,
parent='gamebox',
scene=[MainScene()()]
))
En resumen, hemos creado una Clase Scene para crear nuevas clases que hereden de esta.
Definimos una Clase MainScene que hereda de Scene en la que definimos los métodos init, preload, create y update.
Fíjate en como instanciamos a MainScene:
MainScene()()
Con los primeros paréntesis ()
se crea un objeto python de la clase MainScene, y con los segundos se devuelve a self.this, que es la Phaser.Scene
.
Ya estamos listos para crear el Pong...
DESARROLLO DEL "Juego Pong" CON HERENCIA FAKE
Método init:
Definimos algunas constantes que utilizaremos posteriormente
def init(self, *args):
self.MIN_X = 0
self.MAX_X = self.sys.game.config.width
self.MIN_Y = 0
self.MAX_Y = self.sys.game.config.height
self.CENTER_X = self.MAX_X/2
self.CENTER_Y = self.MAX_Y/2
self.KEYCODES = Phaser.Input.Keyboard.KeyCodes
En self.sys.game.config
hay definidas algunas variables como width y height que utilizaremos para definir los límites del juego y así limitar el espacio sobre el que se mueve la bola.
Almacenamos Phaser.Input.Keyboard.KeyCodes
en self.KEYCODES
simplemente por comodidad a la hora de definir acciones desencadenadas al pulsar ciertas teclas (el movimiento de las palas).
Puedes inspeccionar los atributos y métodos de la escena en la consola del navegador mediante:
console.log = window.console.log
console.log(self)
Método preload:
Cargamos los sprites del juego, que se encuentran en la carpeta "assets".
def preload(self, *args):
self.load.path = './assets/'
for i in 'P1', 'P2', 'BOLA', 'SEPARADOR':
self.load.image(i, f'{i}.png')
P1 y P2 son los sprites que utilizaremos para dibujar las palas de los jugadores 1 y 2 respectivamente. BOLA y SEPARADOR se describen por si solos.
La función self.load.image
permite cargar sprites en la escena. El primer argumento que toma es el nombre del sprite, y el segundo el path de la imágen del sprite.
Aunque las imágenes se encuentran en la carpeta "assets", omitimos a assets al indicar el path de las imágenes como segundo parámetro de self.load.image
porque previamente hemos indicado que la carpeta de trabajo es "assets" mediante la función self.load.path
.
Método create:
Mientras que en el método preload hemos cargados los sprites del juego, en el método create vamos a añadir estos sprites precargados a la escena. Es en create donde hacemos visibles a nuestros sprites.
Las instrucciones del método create son algo mas complejas que las de los métodos init y preload, de modo que vamos a definir la función poco a poco.
Añadir el sprite separador a la escena y posicionarlo en el centro de esta:
def create(self, *args): # SEPARADOR self.add.image(self.CENTER_X, self.CENTER_Y, 'SEPARADOR')
Hemos utilizado la función
self.add.image
para añadir el sprite. Esta función recibe tres parámetros: el primero es su posición en el eje x, el segundo su posición en el eje y y el tercero el nombre interno del sprite.self.CENTER_X
yself.CENTER_Y
son constantes que previamente habíamos definido en el métodoself.init
- El nombre interno del sprite es
"SEPARADOR"
porque así lo indicamos previamente en el métodoself.preload
Añadir el sprite bola a la escena y posicionarlo en el centro de esta:
Podríamos hacerlo del mismo modo que lo hicimos para el separador:
def create(self, *args): self.add.image(self.CENTER_X, self.CENTER_Y, 'SEPARADOR') # BOLA self.BOLA = self.add.image(self.CENTER_X, self.CENTER_Y, 'BOLA')
La única diferencia es que hemos almacenado una referencia al sprite en la variable
self.BOLA
para utilizarla posteriormente.Sin embargo, en este caso no debemos utilizar
self.add.image
para añadir el sprite, sinoself.physics.add.image
, ya que tenemos que agregas físicas a la bola como veremos a continuación.def create(self, *args): self.add.image(self.CENTER_X, self.CENTER_Y, 'SEPARADOR') # BOLA self.BOLA = self.physics.add.image(self.CENTER_X, self.CENTER_Y, 'BOLA')
Esto funcionará porque previamente habíamos habilitado las físicas en
Phaser.Game
:physics={'default': 'arcade'}
Definir los límites de colisión de la escena:
Lo haremos antes de añadir la bola mediante
self.physics.world.setBoundsCollision
:def create(self, *args): self.add.image(self.CENTER_X, self.CENTER_Y, 'SEPARADOR') self.physics.world.setBoundsCollision(False, False, True, True) # <-- LÍMITES DE COLISIÓN: ARRIBA Y ABAJO DEL CANVAS self.BOLA = self.physics.add.image(self.CENTER_X, self.CENTER_Y, 'BOLA')
Esta función recibe 4 valores booleanos:
self.physics.world.setBoundsCollision(izquierda, derecha, arriba, abajo)
Hemos indicado que en la escena los objetos puedan chocar con los límites de arriba y abajo, pero no con los de izquierda y derecha.
Definir las físicas de la bola:
def create(self, *args): self.add.image(self.CENTER_X, self.CENTER_Y, 'SEPARADOR') self.physics.world.setBoundsCollision(False, False, True, True) self.BOLA = self.physics.add.image(self.CENTER_X, self.CENTER_Y, 'BOLA') # FÍSICAS APLICADAS A LA BOLA self.BOLA.setCollideWorldBounds(True) # QUE LA BOLA REBOTE AL CHOCAR CON LAS PAREDES self.BOLA.setBounce(1) # QUE AL CHOCAR REBOTE CON LA MISMA INTENSIDAD self.BOLA.setVelocityX(-300) # QUE LA BOLA SE MUEVA A -300 DE VELOCIDAD EN EL EJE X (A LA IZQUIERDA)
setCollideWorkdBound(True)
- Indicamos que la bola debe rebotas al chocar con las pareces. Solamente rebotará con las paredes de arriba y abajo, tal como definimos anteriormente.setBounce(1)
- La bola debe rebotar al chocar con una pared con la misma intensidad con la que chocó.setVelocityX(-300)
- Al iniciarse el juego la bola se moverá en el eje X a una velocidad de -300, lo que significa que irá hacia la izquierda.
Añadir las palas/players:
La función create queda finalmente así:
def create(self, *args): self.add.image(self.CENTER_X, self.CENTER_Y, 'SEPARADOR') self.physics.world.setBoundsCollision(False, False, True, True) self.BOLA = self.physics.add.image(self.CENTER_X, self.CENTER_Y, 'BOLA') self.BOLA.setCollideWorldBounds(True) self.BOLA.setBounce(1) self.BOLA.setVelocityX(-300) # PALAS/PLAYERS self.P1 = self.crear_pala(name='P1', position=(self.MIN_X+30, self.CENTER_Y), keys=('W', 'S')) self.P2 = self.crear_pala(name='P2', position=(self.MAX_X-30, self.CENTER_Y), keys=('UP', 'DOWN'))
La función
self.crear_pala
no es una función de Phaser, sino que vamos a crearla nosotros a continuación. Fíjate en como hemos creado los spritesself.P1
yself.P2
: Indicamos el nombre del sprite, su posición y las teclas del teclado con las que el sprite va a manejarse.
Método crear_pala:
El método self.crear_pala
debe devolver un sprite que representa a una pala/player. Su definición es la siguiente:
def crear_pala(self, name, position, keys):
sprite = self.add.sprite(position[0], position[1], name)
self.physics.world.enable(sprite)
sprite.body.immovable = True
sprite.body.setCollideWorldBounds(True)
self.physics.add.collider(self.BOLA, sprite, lambda *args: self.BOLA.setVelocityY(Phaser.Math.Between(-120, 120)), None, self)
up, down = keys
sprite.KEY_UP = self.input.keyboard.addKey(self.KEYCODES[up])
sprite.KEY_DOWN = self.input.keyboard.addKey(self.KEYCODES[down])
return sprite
Analicemos el código de esta función:
sprite = self.add.sprite(position[0], position[1], name)
self.add.sprite
nos permite añadir un sprite a la escena. El primer argumento que recibe es su posición en el eje x, el segundo su posición en el eje y, y el tercero es su type (yo lo he llamado name en vez de type porque en python type es una función predefinida).
self.physics.world.enable(sprite)
Hemos aplicado las físicas definidas en la escena sobre el sprite
sprite.body.immovable = True
sprite.body
se refiere al cuerpo del sprite, es decir, al objeto que representa su significado físico. En otras palabras, body es el atributo del sprite donde se definen sus propiedades físicas.
Si no establecemos sprite.body.immovable = True
, al chocar la bola contra la pala se llevará a la pala por delante. Comprueba lo que sucede si omites esta sentencia o si la estableces en False.
sprite.body.setCollideWorldBounds(True)
Hemos indicado que el sprite debe detectar los límites del mundo, es decir, los límites de la escena/canvas.
self.physics.add.collider(self.BOLA, sprite, lambda *args: self.BOLA.setVelocityY(Phaser.Math.Between(-120, 120)), None, self)
Añadimos un colisionador a las fisicas de la escena. Cuando self.BOLA
y sprite
colisionen se ejecutará la función:
lambda *args: self.BOLA.setVelocityY(Phaser.Math.Between(-120, 120))
'''
Que podriamos haber definido así:
def colision(*args):
self.BOLA.setVelocityY(Phaser.Math.Between(-120, 120))
'''
Al colisionar la bola con una pala, a la bola se le asignará una velocidad en el eje Y comprendida entre los valores -120 y 120.
Mientras no colisionen no se ejecutará ninguna función: None
.
EL último argumento es self
, es decir, la propia escena. Recuerda por un momento que en realidad la escena es self.this
, y que gracias a la herencia fake puedes olvidarte de esto.
EL hecho de que el último argumento sea self
puede parecer redundante, sin embargo es así porque la función Phaser.Scene.physics.add.collider
podría utilizarse para definir una colisión en una escena desde otra escena.
Analicemos el tramo final de la función:
up, down = keys
sprite.KEY_UP = self.input.keyboard.addKey(self.KEYCODES[up])
sprite.KEY_DOWN = self.input.keyboard.addKey(self.KEYCODES[down])
return sprite
Hemos definido las teclas KEY_UP y KEY_DOWN para el sprite mediante la función
self.input.keyboard.addKey
.self.KEYCODES
es una referencia aPhaser.Input.Keyboard.KeyCodes
que definimos previamente en el método init.
Método update:
El método update es el bucle que da vida al juego. Lo primero que vamos a hacer es comprobar la posición de la bola y devolverla al centro si esta ha salido por alguna de las pareces (izquierda o derecha). Posteriormente definiremos lo que debe suceder al pulsar las teclas KEY_UP y KEY_DOWN de los sprites self.P1
y self.P2
(las palas/players).
Reposicionar la bola en el centro si sale de los limites de la escena:
Recordemos que ya habíamos definido los límites de la escena en el eje X en el método init. Solamente indicamos los del eje X porque en el método create establecimos que si la bola choca con las pareces de arriba y abajo debe rebotar y no salir de los límites.
Por tanto, las condiciones que determinan si la bola ha salido del tablero son las siguientes:
- self.BOLA.x > self.MAX_X - En este caso la bola ha salido por la derecha.
- self.BOLA.x < self.MIN_X - En este caso la bola ha salido por la izquierda.
Nuestra función update comienza así:
def update(self, time, delta): if self.BOLA.x > self.MAX_X or self.BOLA.x < self.MIN_X: self.BOLA.setPosition(self.CENTER_X, self.CENTER_Y)
Lo que significa que "si la bola ha salido por la derecha o por la izquierda, establecemos la posición de la bola en el centro del tablero". La función
self.BOLA.setPosition
nos permite indicar las coordenadas x e y.self.CENTER_X
yself.CENTER_Y
fueron definidas en el método init.
Mover las palas al pulsar sus teclas asociadas:
Como tenemos dos palas (
self.P1
yself.P2
) y la funcionalidad de ambas debe ser la misma, vamos a definir esta funcionalidad en un bucle for que itere sobre ambas:def update(self, time, delta): if self.BOLA.x > self.MAX_X or self.BOLA.x < self.MIN_X: self.BOLA.setPosition(self.CENTER_X, self.CENTER_Y) # PALAS for pala in self.P1, self.P2: if pala.KEY_DOWN.isDown: pala.body.setVelocityY(300) elif pala.KEY_UP.isDown: pala.body.setVelocityY(-300) else: pala.body.setVelocityY(0)
Simplemente hemos indicado que para cada una de las palas, si está pulsada la tecla KEY_DOWN, la velocidad de la pala en el eje Y es 300 (la pala se mueve hacia abajo). Si por el contrario la tecla pulsada es KEY_UP, la velocidad de la pala en el eje Y es -300, (la pala se mueve hacia arriba).
Si ninguna de las dos teclas está siendo pulsada, la velocidad de la pala en el eje Y es 0 y la pala se detiene.
game.py finalizado
from browser import window
Phaser = window.Phaser
class Scene:
def __init__(self, active=True):
self.this = Phaser.Scene.new(self.__class__.__name__)
self.this.active = active
self.this.init = self.init
self.this.preload = self.preload
self.this.create = self.create
self.this.update = self.update
def __getattr__(self, attr):
if attr not in self.__dict__:
return self.this[attr]
def __setattr__(self, attr, value):
self.__dict__[attr] = value
self.this[attr] = value
def __call__(self):
return self.this
class MainScene(Scene):
def __init__(self, active=True):
Scene.__init__(self, active)
def crear_pala(self, name, position, keys):
sprite = self.add.sprite(position[0], position[1], name)
self.physics.world.enable(sprite)
sprite.body.immovable = True
sprite.body.setCollideWorldBounds(True)
self.physics.add.collider(self.BOLA, sprite, lambda *args: self.BOLA.setVelocityY(Phaser.Math.Between(-120, 120)), None, self)
up, down = keys
sprite.KEY_UP = self.input.keyboard.addKey(self.KEYCODES[up])
sprite.KEY_DOWN = self.input.keyboard.addKey(self.KEYCODES[down])
return sprite
def init(self, *args):
self.MIN_X = 0
self.MAX_X = self.sys.game.config.width
self.MIN_Y = 0
self.MAX_Y = self.sys.game.config.height
self.CENTER_X = self.MAX_X/2
self.CENTER_Y = self.MAX_Y/2
self.KEYCODES = Phaser.Input.Keyboard.KeyCodes
def preload(self, *args):
self.load.path = './assets/'
for i in 'P1', 'P2', 'BOLA', 'SEPARADOR':
self.load.image(i, f'{i}.png')
def create(self, *args):
self.add.image(self.CENTER_X, self.CENTER_Y, 'SEPARADOR')
self.physics.world.setBoundsCollision(False, False, True, True)
self.BOLA = self.physics.add.image(self.CENTER_X, self.CENTER_Y, 'BOLA')
self.BOLA.setCollideWorldBounds(True)
self.BOLA.setBounce(1)
self.BOLA.setVelocityX(-300)
self.P1 = self.crear_pala(name='P1', position=(self.MIN_X+30, self.CENTER_Y), keys=('W', 'S'))
self.P2 = self.crear_pala(name='P2', position=(self.MAX_X-30, self.CENTER_Y), keys=('UP', 'DOWN'))
def update(self, time, delta):
if self.BOLA.x > self.MAX_X or self.BOLA.x < self.MIN_X:
self.BOLA.setPosition(self.CENTER_X, self.CENTER_Y)
for pala in self.P1, self.P2:
if pala.KEY_DOWN.isDown:
pala.body.setVelocityY(300)
elif pala.KEY_UP.isDown:
pala.body.setVelocityY(-300)
else:
pala.body.setVelocityY(0)
Phaser.Game.new(dict(
width=640,
height=480,
type=Phaser.CANVAS,
parent='gamebox',
scene=[MainScene()()]
))