I successfully configured a raspberry pi as an access point such that I can connect via WIFI with my mobile phone or laptop. Now, I would like to run PyGame on the raspberry pi, which is connected to a screen, and control objects in a game via a mobile phone or laptop that is connected to the raspberry pi.
In the following, I provide simple working examples first and then show what did not work.
To provide some content to the clients, I installed an nginx server on the raspberry pi. When I open a browser with the IP address of the raspberry pi (192.168.4.1), the index page appears.
Then, to test a WebSocket Server, I wrote a simple python script based on the websockets package:
import websockets
import asyncio
# handler processes the message and sends "Success" back to the client
async def handler(websocket, path):
async for message in websocket:
await processMsg(message)
await websocket.send("Success")
async def processMsg(message):
print(f"[Received]: {message}")
async def main():
async with websockets.serve(handler, "192.168.4.1", 6677):
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())
I tested the server by setting up an HTML page, which connects to the WebSocket Server via a Javascript file and implements a button to send a string to the server:
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Test</title>
<script type="text/javascript" src="static/client.js"></script>
</head>
<body>
<div>
<h2>WebSocket Test</h2>
<input type="button" name="send" value="Send Hello!" onClick="sendHello()">
</div>
</body>
</html>
and the client.js file:
let socket = new WebSocket("ws://192.168.4.1:6677/");
socket.onopen = function(e) {
console.log("[open] Connection established");
console.log("[send] Sending to server");
socket.send("Web connection established")
};
socket.onmessage = function(event) {
console.log(`[message] Data received from server: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
console.log('[close] Connection died!')
}
};
socket.onerror = function(error) {
console.log(`[error] ${error.message}`);
};
function sendHello() {
console.log("[send] Sending to server");
socket.send("Hello!");
};
With these simple example files I could successfully establish a persistent connection and exchange data between server and client.
To test the WebSocket Server with PyGame, I set up a simple game only displaying a blue circle:
# import and init pygame library
import pygame
pygame.init()
# screen dimensions
HEIGHT = 320
WIDTH = 480
# set up the drawing window
screen = pygame.display.set_mode([WIDTH,HEIGHT])
# run until the user asks to quit
running = True
while running:
# did the user close the window
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# fill the background with white
screen.fill((255,255,255))
# draw a solid blue circle in the center
pygame.draw.circle(screen, (0,0,255), (int(WIDTH/2),int(HEIGHT/2)), 30)
# flip the display
pygame.display.flip()
pygame.quit()
The game is running as it should, displaying a blue circle on a white background.
The problem is that when I want to add the WebSocket Server to the game, either one is blocking the execution of the other part, i.e., I can only run the game without an active server or I can run the WebSocket server but the game is not showing up. For example, putting the WebSocket server in front of the game loop like this
# WS server
async def echo(websocket, path):
async for message in websocket:
msg = message
print(f"[Received] {message}")
await websocket.send(msg)
async def server():
async with websockets.serve(echo, "192.168.4.1", 6677):
await asyncio.Future()
asyncio.ensure_future(server())
or including the game-loop inside the server:
async def server():
async with websockets.serve(echo, "192.168.4.1", 6677):
#await asyncio.Future()
# set up the drawing window
screen = pygame.display.set_mode([WIDTH,HEIGHT])
# run until the user asks to quit
running = True
while running:
#pygame code goes here
By an extensive Google search I figured out that PyGame and the asyncio package (websockets is based on asyncio) cannot simply work together as I have to somehow take care of, both, the asyncio-loop and the game-loop manually.
I hope someone can help me dealing with this problem ...
Thanks to a colleague of mine I figured out the solution to get Pygame running including a Websockets server and responding to buttons pressed on mobile clients. The important point is to run the Websockets server in a different thread and properly handle events.
Here is the code of server.py. I changed the IP address to local host (127.0.0.1) in order to test the code on my laptop.
import websockets
import asyncio
import pygame
IPADDRESS = "127.0.0.1"
PORT = 6677
EVENTTYPE = pygame.event.custom_type()
# handler processes the message and sends "Success" back to the client
async def handler(websocket, path):
async for message in websocket:
await processMsg(message)
await websocket.send("Success")
async def processMsg(message):
print(f"[Received]: {message}")
pygame.fastevent.post(pygame.event.Event(EVENTTYPE, message=message))
async def main(future):
async with websockets.serve(handler, IPADDRESS, PORT):
await future # run forever
if __name__ == "__main__":
asyncio.run(main())
The important part here is the method pygame.fastevent.post
to trigger a Pygame event.
Then, in game.py the server is initiated in a different thread:
# import and init pygame library
import threading
import asyncio
import pygame
import server
def start_server(loop, future):
loop.run_until_complete(server.main(future))
def stop_server(loop, future):
loop.call_soon_threadsafe(future.set_result, None)
loop = asyncio.get_event_loop()
future = loop.create_future()
thread = threading.Thread(target=start_server, args=(loop, future))
thread.start()
pygame.init()
pygame.fastevent.init()
# screen dimensions
HEIGHT = 320
WIDTH = 480
# set up the drawing window
screen = pygame.display.set_mode([WIDTH, HEIGHT])
color = pygame.Color('blue')
radius = 30
x = int(WIDTH/2)
# run until the user asks to quit
running = True
while running:
# did the user close the window
for event in pygame.fastevent.get():
if event.type == pygame.QUIT:
running = False
elif event.type == server.EVENTTYPE:
print(event.message)
color = pygame.Color('red')
x = (x + radius / 3) % (WIDTH - radius * 2) + radius
# fill the background with white
screen.fill((255,255,255))
# draw a solid blue circle in the center
pygame.draw.circle(screen, color, (x, int(HEIGHT/2)), radius)
# flip the display
pygame.display.flip()
print("Stoping event loop")
stop_server(loop, future)
print("Waiting for termination")
thread.join()
print("Shutdown pygame")
pygame.quit()
Be careful to properly close the thread running the Websockets Server when you want to exit Pygame!
Now, when you use the HTML file including the client.js script I posted in the question, the blue circle should change its color to red and move to the right every time you press the "Send hello" button.
I hope this helps everyone who is trying to setup an online multiplayer game using Pygame.