Cómo desarrollar un juego para Android con un motor de juego actualizado en Python

Hace unas semanas me preguntaron si me gustaría ir a Kiwi Pycon (una conferencia de Python en Dunedin, Nueva Zelanda). Decidí que sería una experiencia interesante a pesar de que mi conocimiento de Python es un poco escaso. Como un proyecto después del trabajo, mi colega Yosan y yo decidimos armar un pequeño juego para ayudarnos a aprender el idioma. Este artículo cubre nuestras experiencias iniciales al poner todo junto.

El plan

Al igual que con otros idiomas que he aprendido, normalmente me gusta desarrollar una aplicación que implique un puñado de funciones, como leer archivos, redes, entradas de usuarios y visuales. Esto me obliga a familiarizarme con las bibliotecas y las funciones del lenguaje, lo que me pone al día de una manera que no sería posible volver a implementar algoritmos y completar proyectos de tutoría. También me obliga a comprender un poco sobre el entorno de Python con respecto a la instalación de dependencias y la creación de versiones.

Buscamos algunas bibliotecas relacionadas con la creación de juegos y la creación de redes y decidimos usar pygame, ya que parecía proporcionar una funcionalidad que eliminaría gran parte del tedio del desarrollo. También parecía que Python tenía una gama de bibliotecas para la creación de redes, por lo que decidimos resolverlo cuando llegamos a él.

Instalando Python

Python en sí fue relativamente fácil de instalar. Acabamos de sacar el instalador automático del sitio web y teníamos listo el tiempo de ejecución en un minuto.

Instalando Pygame

Pygame demostró ser un poco frustrante de instalar. Tomó varios intentos antes de que pudiéramos descargar el script e instalarlo de la manera correcta. Tuvimos que encontrar la versión correcta de la biblioteca (que coincidía con la versión de Python que habíamos instalado) en una lista de dependencias que no se encontró fácilmente, luego extraerla con la utilidad de instalación del paquete Python pip3.exe. Esto parecía más difícil de lo que debería haber sido, especialmente debido a la cantidad de versiones diferentes de la biblioteca y las pequeñas diferencias en lo que tendríamos que hacer si tuviéramos instalada una versión diferente de Python.

Finalmente, configuramos las cosas y buscamos un tutorial sobre cómo poner en marcha los conceptos básicos de un juego.

Dibujando un sprite

Lo primero que debe hacer al comenzar con algo gráfico es simplemente mostrar algo (o cualquier cosa) en la pantalla. Encontramos un montón de tutoriales de diversa complejidad sobre esto y, en base a sus ejemplos, surgió un ciclo de renderizado básico:

import pygame, sys
de pygame.locals import *

ANCHO = 400
ALTURA = 400

pantalla = pygame.display.set_mode ((WIDTH, HEIGHT))
pygame.display.set_caption (‘¡Hola, mundo!’)

clock = pygame.time.Clock ()

thing = pygame.image.load (‘images / TrashPanda / TrashPanda_front.png’)

x = 0
y = 0

mientras cierto:
para evento en pygame.event.get ():
if event.type == QUIT:
pygame.quit ()
sys.exit ()

clock.tick (30)
screen.fill ((0,0,0))
screen.blit (cosa, (x, y))
pygame.display.flip ()

Este código produjo esto:

Después de eso, nos enfocamos en capturar la entrada del usuario para mover el personaje. También creamos una clase para que el personaje jugador internalice parte de su lógica:

clase Minion:
def __init __ (self, x, y):
self.x = x
self.y = y
self.vx = 0
self.vy = 0
actualización de def (auto):
self.x + = self.vx
self.y + = self.vy
# Esto mantiene al personaje del jugador dentro de los límites de la pantalla
si self.x> WIDTH – 50:
self.x = WIDTH – 50
si self.x <0:
self.x = 0
si es self.y> ALTURA – 50:
self.y = ALTURA – 50
si self.y <0:
self.y = 0

def render (self):
screen.blit (cosa, (self.x, self.y))

La entrada del usuario fue capturada dentro del bucle del juego:

para evento en pygame.event.get ():
if event.type == QUIT:
pygame.quit ()
sys.exit ()
if event.type == KEYDOWN:
if event.key == K_LEFT: cc.vx = -10
if event.key == K_RIGHT: cc.vx = 10
if event.key == K_UP: cc.vy = -10
if event.key == K_DOWN: cc.vy = 10
if event.type == KEYUP:
if event.key == K_LEFT y cc.vx == -10: cc.vx = 0
if event.key == K_RIGHT y cc.vx == 10: cc.vx = 0
if event.key == K_UP y cc.vy == -10: cc.vy = 0
if event.key == K_DOWN y cc.vy == 10: cc.vy = 0

Y la posición del personaje fue actualizada y renderizada (también en el gameloop):

cc.update ()
cc.render ()

Ahora que teníamos funcionando el movimiento básico de los personajes, queríamos comenzar a construir algunas funciones simples para varios jugadores.

Decidimos un modelo de transferencia de datos muy simple:

  • Los clientes se conectarían al servidor y luego transmitirían continuamente la posición de su propio personaje
  • El servidor luego transmitiría la ubicación de todos los caracteres a todos los clientes

Decidimos usar sockets TCP ya que manejan cosas como conexiones y desconexiones más fácil que UDP. Además, esta no es exactamente una aplicación crítica de rendimiento.

Logramos encontrar un buen artículo sobre escritura de servidores asíncronos en Python aquí.

El código básico del servidor comenzó así:

zócalo de importación
importar asyncore
importar al azar
pepinillo de importación
tiempo de importación

BUFFERSIZE = 512

saliente = []

# lógica adicional aquí …

clase MainServer (asyncore.dispatcher):
def __init __ (self, port):
asyncore.dispatcher .__ init __ (self)
self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
self.bind ((”, puerto))
self.listen (10)

def handle_accept (self):
conn, addr = self.accept ()
print (‘Dirección de conexión:’ + addr [0] + “” + str (addr [1]))
outgoing.append (conn)
playerid = random.randint (1000, 1000000)
playerminion = Minion (playerid)
minionmap [playerid] = playerminion
conn.send (pickle.dumps ([‘id update’, playerid]))
SecondaryServer (conn)

clase SecondaryServer (asyncore.dispatcher_with_send):
def handle_read (self):
recivedData = self.recv (BUFFERSIZE)
si se recibeData:
updateWorld (recivedData)
más: self.close ()

Servidor principal (4321)
asyncore.loop ()

Esto define un MainServer responsable de aceptar nuevas conexiones TCP para las cuales crea un SecondaryServer. Los servidores secundarios manejan todos los datos entrantes de cada cliente. Cuando se recibe un paquete entrante, los datos se pasan a updateWorld. Esto se define a continuación:

clase Minion:
def __init __ (self, ownerid):
self.x = 50
self.y = 50
self.ownerid = ownerid

minionmap = {}

def updateWorld (mensaje):
arr = pickle.loads (mensaje)
jugadorid = arr [1]
x = arr [2]
y = arr [3]

if playerid == 0: return

minionmap [playerid] .x = x
minionmap [playerid] .y = y

eliminar = []

para yo en saliente:
actualización = [‘ubicaciones del jugador’]

para clave, valor en minionmap.items ():
update.append ([value.ownerid, value.x, value.y])

tratar:
i.send (pickle.dumps (actualización))
excepto Excepción:
remove.append (i)
continuar

para r en eliminar:
outgoing.remove (r)

updateWorld es simplemente responsable de actualizar el diccionario que contiene la ubicación del personaje de cada jugador. Luego transmite las posiciones a cada jugador serializando sus posiciones como una matriz de matrices.

Ahora que el cliente fue construido, podríamos implementar la lógica en el cliente para enviar y recibir actualizaciones. Cuando se inicia el juego, agregamos algo de lógica para iniciar un socket simple y conectarnos a una dirección de servidor. Opcionalmente, toma una dirección IP especificada por la línea de comando, pero de lo contrario se conecta a localhost:

serverAddr = ‘127.0.0.1’
si len (sys.argv) == 2:
serverAddr = sys.argv [1]
s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect ((serverAddr, 4321))

Luego agregamos algo de lógica al inicio del bucle del juego para leer desde el socket. Utilizamos el paquete ‘ select’ para leer los paquetes entrantes del socket solo cuando tenían datos. Si hubiéramos usado ‘ socket.recv’, el gameloop se detendría si el socket no tuviera ningún paquete para leer. El uso de ‘select’ permite que el gameloop continúe ejecutándose incluso si no hay nada que leer:

ins, outs, ex = select.select ([s], [], [], 0)
para inm in ins:
gameEvent = pickle.loads (inm.recv (BUFFERSIZE))
if gameEvent [0] == ‘id update’:
playerid = gameEvent [1]
print (playerid)
if gameEvent [0] == ‘ubicaciones de jugadores’:
gameEvent.pop (0)
secuaces = []
para minion en gameEvent:
if minion [0]! = playerid:
minions.append (Minion (minion [1], minion [2], minion [0]))

El código anterior manejó dos de las cargas útiles serializadas que el servidor podría producir.

1. El paquete inicial que contiene el identificador asignado al servidor de jugadores

El cliente lo utiliza para identificarse con el servidor en todas las actualizaciones de posición. También solía ignorar sus propios datos de jugador que el servidor transmite para que no haya una versión sombreada del personaje del jugador.

2. La carga útil de la ubicación del jugador

Contiene un conjunto de matrices que contienen identificadores de jugadores y posiciones de personajes. Cuando esto se recupera, los objetos Minion existentes se borran y se crean nuevos objetos Minion para cada uno de los transmitidos.

Los otros Minions se representan en el bucle del juego:

para m en minions:
m.render ()

Lo último que tuvimos que hacer fue agregar un código al cliente para decirle al servidor la posición del jugador. Esto se hizo agregando una transmisión al final del gameloop para serializar la posición actual de los jugadores usando ‘ pickle ‘, y luego enviando este bytestream al servidor:

ge = [‘actualización de posición’, playerid, cc.x, cc.y]
s.send (pickle.dumps (ge))

Una vez que esto se completara, los jugadores conectados al mismo servidor podían ver a los otros jugadores moverse.

Se implementaron algunas actualizaciones adicionales, como mostrar diferentes avatares basados ​​en el ID del jugador.

Cuando terminó, la iteración actual con dos jugadores se veía así:

El código completo para el cliente y el servidor está disponible aquí.

Por supuesto, siempre hay espacio para el desarrollo y la mejora. Si este artículo le pareció interesante, infórmenos en los comentarios a continuación.

Sugerir

☞ Zero to Hero con Python Professional Python Programmer Bundle

☞ El Mega Curso de Python: Crea 10 aplicaciones de Python

☞ Tutorial de programación PYTHON para principiantes: aprenda en 3 horas

☞ El último tutorial de programación de Python

☞ Comienza a aprender Unity3d haciendo 5 juegos desde cero

☞ Dominar el desarrollo de juegos HTML5

use kivy @Kivy: Python Framework multiplataforma para NUI