Search code examples
pythonwebsocketpygame

Implement a WebSocket Server in PyGame to control objects via a HTML WebSocket Client


General idea

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.

Testing the WebSocket Server worked

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.

Adding the WebSocket Server to PyGame did not work

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 ...


Solution

  • 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.