Repod is designed to integrate seamlessly with synchronous game loops (like Pygame or Pyglet) by running networking in background daemon threads. This keeps your main game loop simple and responsive.
Server Background Threads
Use start_background() to run a server in a daemon thread while your main thread handles game logic:
from repod import Server, Channel
import time
class GameChannel(Channel):
def Network_input(self, data: dict) -> None:
# Process player input
self.server.game_state.update_player(self, data)
class GameServer(Server):
channel_class = GameChannel
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.game_state = GameState()
# Start server in background
server = GameServer(host="0.0.0.0", port=5071)
server_thread = server.start_background()
print(f"Server running on background thread: {server_thread.name}")
# Main game loop continues
while True:
server.game_state.update()
# Broadcast state to all clients
server.send_to_all({
"action": "game_state",
"data": server.game_state.serialize()
})
time.sleep(0.016) # ~60 FPS
Thread Properties
The thread returned by start_background() is a daemon thread:
server_thread = server.start_background()
print(f"Daemon: {server_thread.daemon}") # True
print(f"Alive: {server_thread.is_alive()}") # True
print(f"Name: {server_thread.name}") # Thread-N
Daemon threads automatically terminate when the main program exits, so you don’t need explicit cleanup in most cases.
Client Background Threads
Clients automatically run networking in a background thread when you call connect():
from repod import ConnectionListener
import time
import pygame
class GameClient(ConnectionListener):
def __init__(self):
self.game_state = None
def Network_game_state(self, data: dict) -> None:
# Updates from background thread
self.game_state = data["data"]
# Initialize client
client = GameClient()
client.connect("localhost", 5071) # Starts background thread
# Initialize Pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
# Main game loop (synchronous)
running = True
while running:
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Process network messages
client.pump()
# Send player input to server
keys = pygame.key.get_pressed()
client.send({
"action": "input",
"left": keys[pygame.K_LEFT],
"right": keys[pygame.K_RIGHT]
})
# Render (using state from background thread)
if client.game_state:
render_game(screen, client.game_state)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Host & Client (Peer-to-Peer)
Run both a server and a client in the same process for “Host Game” scenarios:
from repod import Server, Channel, ConnectionListener
import time
class GameChannel(Channel):
def Network_move(self, data: dict) -> None:
# Relay moves to all clients
self.server.send_to_all(data)
class GameServer(Server):
channel_class = GameChannel
class GameClient(ConnectionListener):
def Network_move(self, data: dict) -> None:
# Update local state
print(f"Player moved: {data}")
# Start server in background
server = GameServer(host="0.0.0.0", port=5071)
server.start_background()
print("Server started in background")
# Connect as a client
client = GameClient()
client.connect("localhost", 5071)
print("Connected as client")
# Main game loop
while True:
client.pump()
# Send periodic updates
client.send({"action": "move", "x": 100, "y": 200})
time.sleep(0.1)
This pattern is perfect for peer-to-peer multiplayer where one player hosts the game. The host runs both a server (for other players) and a client (to play themselves).
Thread Safety
Repod’s API is designed to be thread-safe for cross-thread calls:
Safe Operations
These operations are safe to call from any thread:
# From main thread while server runs in background
server.send_to_all({"action": "update"})
# From main thread while client runs in background
client.send({"action": "ping"})
# Send from Channel to client
channel.send({"action": "response"})
Internal Mechanisms
Repod uses thread-safe queues and asyncio’s call_soon_threadsafe():
# In channel.py (excerpt)
if loop is not None and loop.is_running():
try:
loop.call_soon_threadsafe(self._send_queue.put_nowait, outgoing)
except RuntimeError:
return 0
else:
self._send_queue.put_nowait(outgoing)
While send() operations are thread-safe, avoid accessing internal attributes like _send_queue or _receive_queue directly from multiple threads unless you know what you’re doing.
Game Loop Integration
Pygame Example
import pygame
from repod import ConnectionListener
class GameClient(ConnectionListener):
def __init__(self):
self.player_pos = {"x": 400, "y": 300}
def Network_update(self, data: dict) -> None:
self.player_pos = data["pos"]
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
client = GameClient()
client.connect("localhost", 5071)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Network update (processes messages from background thread)
client.pump()
# Game logic
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
client.send({"action": "jump"})
# Rendering
screen.fill((0, 0, 0))
pygame.draw.circle(screen, (255, 0, 0),
(client.player_pos["x"], client.player_pos["y"]), 20)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Pyglet Example
import pyglet
from repod import ConnectionListener
class GameClient(ConnectionListener):
def __init__(self):
self.entities = []
def Network_entities(self, data: dict) -> None:
self.entities = data["entities"]
window = pyglet.window.Window(800, 600)
client = GameClient()
client.connect("localhost", 5071)
@window.event
def on_draw():
window.clear()
for entity in client.entities:
# Draw entities
pass
def update(dt):
# Process network messages
client.pump()
# Send input
client.send({"action": "input", "time": dt})
pyglet.clock.schedule_interval(update, 1/60.0)
pyglet.app.run()
Custom Game Loop
import time
from repod import ConnectionListener, Server, Channel
class GameChannel(Channel):
def Network_action(self, data: dict) -> None:
# Process player actions
pass
class GameServer(Server):
channel_class = GameChannel
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tick = 0
class GameClient(ConnectionListener):
def Network_tick(self, data: dict) -> None:
print(f"Server tick: {data['tick']}")
# For a dedicated server
server = GameServer(host="0.0.0.0", port=5071)
server_thread = server.start_background()
last_time = time.time()
while True:
current_time = time.time()
delta = current_time - last_time
last_time = current_time
# Update game logic at fixed timestep
server.tick += 1
server.send_to_all({"action": "tick", "tick": server.tick})
# Sleep for fixed timestep (e.g., 20 FPS server)
time.sleep(0.05)
Error Handling in Background Threads
Errors in background threads are logged but won’t crash your main program:
from repod import Channel
class GameChannel(Channel):
def on_error(self, error: Exception) -> None:
"""Called when an error occurs in the network thread."""
print(f"Network error: {error}")
# Log to file, show to user, etc.
For clients, handle errors via the Network_error handler:
class GameClient(ConnectionListener):
def Network_error(self, data: dict) -> None:
error_msg = data.get("error", "Unknown error")
print(f"Connection error: {error_msg}")
# Show error dialog, attempt reconnect, etc.
Since background threads run as daemons, uncaught exceptions won’t prevent the main program from exiting. Always implement on_error handlers to log issues during development.
Best Practices
Keep handlers fast
Network handlers run in the main thread (after pump()), so keep them quick to avoid blocking your game loop:def Network_large_update(self, data: dict) -> None:
# BAD: Heavy processing in handler
self.process_heavy_data(data) # Blocks game loop!
# GOOD: Queue for later processing
self.update_queue.append(data)
Call pump() regularly
Call pump() at least once per frame to minimize input lag:while running:
client.pump() # First thing in loop
# ... rest of game logic
Handle disconnections gracefully
Always implement disconnect handlers:def Network_disconnected(self, data: dict) -> None:
print("Lost connection to server")
self.running = False
# Show "Disconnected" screen
Use background servers for hosting
When players host games, use start_background() so the host can play too:# Host starts server
server = GameServer()
server.start_background()
# Host also connects as client
client = GameClient()
client.connect("localhost", 5071)
For dedicated servers (no game loop), use server.launch() instead of start_background(). This blocks and runs the server in the main thread.