Working on Lag Compensaion

This commit is contained in:
OusmBlueNinja 2025-03-25 11:47:59 -05:00
commit ca656d1ca8
2 changed files with 375 additions and 0 deletions

226
client.py Normal file
View File

@ -0,0 +1,226 @@
import socket
import threading
import json
import pygame
import sys
import time
HOST = '127.0.0.1'
PORT = 65432
MOVE_SPEED = 200 # pixels per second
FAKE_LATENCY = 0.1 # seconds of artificial latency for both sending and receiving
# Global state
players = {} # Server-authoritative state: client_id -> {"username", "x", "y"}
my_client_id = None # Our unique client id from the server
# Local predicted state for the local player.
predicted_state = {"x": 100, "y": 100, "keys": set(), "username": "Guest"}
ping = 0 # measured ping in seconds
def listen_to_server(sock):
"""
Listen for incoming messages from the server and update local state.
Also performs reconciliation by snapping to the server state when no keys are pressed.
Simulates network latency on incoming messages.
"""
global my_client_id, players, predicted_state, ping
file_obj = sock.makefile('r')
while True:
try:
line = file_obj.readline()
if not line:
break
# Simulate fake latency for incoming messages.
time.sleep(FAKE_LATENCY)
msg = json.loads(line)
event_name = msg.get('event')
payload = msg.get('payload')
if event_name == "self_id":
if my_client_id is None:
my_client_id = payload.get("client_id")
print(f"[INFO] My client ID: {my_client_id}")
elif event_name == "client_connect":
print(f"[INFO] Client connected: {payload.get('client_id')}")
elif event_name == "client_disconnect":
client_id = payload.get('client_id')
print(f"[INFO] Client disconnected: {client_id}")
if client_id in players:
del players[client_id]
elif event_name == "state_update":
# Update our players dictionary from the server.
players = payload.get("players", {})
# If we have an update for our own player, reconcile.
if my_client_id and my_client_id in players:
server_x = players[my_client_id].get("x", 100)
server_y = players[my_client_id].get("y", 100)
if not predicted_state["keys"]:
# Snap immediately if no movement is occurring.
predicted_state["x"] = server_x
predicted_state["y"] = server_y
else:
# If keys are pressed, nudge the predicted state gradually.
correction_factor = 0.1 # 10% correction per update while moving.
predicted_state["x"] += (server_x - predicted_state["x"]) * correction_factor
predicted_state["y"] += (server_y - predicted_state["y"]) * correction_factor
elif event_name == "pong":
sent_time = payload.get('timestamp')
if sent_time:
round_trip = time.time() - sent_time
ping = round_trip
print(f"[PING] {ping*1000:.0f} ms")
except Exception as e:
print(f"[ERROR] {e}")
break
def send_message(sock, msg_dict):
"""
Send a JSON message to the server with fake latency.
"""
time.sleep(FAKE_LATENCY) # simulate network latency on outgoing messages
message = json.dumps(msg_dict) + "\n"
sock.sendall(message.encode())
def ping_loop(sock):
"""
Periodically send ping messages to measure latency.
"""
while True:
ts = time.time()
send_message(sock, {'type': 'ping', 'timestamp': ts})
time.sleep(1)
def main():
global my_client_id, predicted_state
username = input("Enter your username: ")
predicted_state["username"] = username
# Connect to the server.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
# Start threads to listen to the server and to send pings.
threading.Thread(target=listen_to_server, args=(sock,), daemon=True).start()
threading.Thread(target=ping_loop, args=(sock,), daemon=True).start()
# Register for needed events.
default_events = ["client_connect", "client_disconnect", "state_update"]
for event in default_events:
send_message(sock, {'type': 'register_event', 'event': event})
print(f"[INFO] Registered for event: {event}")
# Inform the server of our chosen username.
send_message(sock, {'type': 'set_username', 'username': username})
# Initialize Pygame.
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Pygame Client - Multiplayer (Ghost vs Client)")
clock = pygame.time.Clock()
# Define colors.
ghost_color = (0, 255, 0) # Green for the server (ghost) state.
client_color = (0, 0, 255) # Blue for the client (predicted) state.
other_color = (255, 0, 0) # Red for other players.
# Local set to track pressed keys.
local_keys = set()
while True:
dt = clock.tick(60) / 1000.0 # Frame time (aiming for 60 FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
key_name = None
if event.key == pygame.K_LEFT:
key_name = "left"
elif event.key == pygame.K_RIGHT:
key_name = "right"
elif event.key == pygame.K_UP:
key_name = "up"
elif event.key == pygame.K_DOWN:
key_name = "down"
if key_name and key_name not in local_keys:
local_keys.add(key_name)
predicted_state["keys"].add(key_name)
send_message(sock, {'type': 'keydown', 'key': key_name})
elif event.type == pygame.KEYUP:
key_name = None
if event.key == pygame.K_LEFT:
key_name = "left"
elif event.key == pygame.K_RIGHT:
key_name = "right"
elif event.key == pygame.K_UP:
key_name = "up"
elif event.key == pygame.K_DOWN:
key_name = "down"
if key_name and key_name in local_keys:
local_keys.remove(key_name)
if key_name in predicted_state["keys"]:
predicted_state["keys"].remove(key_name)
send_message(sock, {'type': 'keyup', 'key': key_name})
# Update the predicted state based on locally pressed keys.
dx, dy = 0, 0
if "left" in predicted_state["keys"]:
dx -= MOVE_SPEED * dt
if "right" in predicted_state["keys"]:
dx += MOVE_SPEED * dt
if "up" in predicted_state["keys"]:
dy -= MOVE_SPEED * dt
if "down" in predicted_state["keys"]:
dy += MOVE_SPEED * dt
predicted_state["x"] += dx
predicted_state["y"] += dy
# Get ghost (server) position without interpolation.
if my_client_id and my_client_id in players:
ghost_x = players[my_client_id].get("x", 100)
ghost_y = players[my_client_id].get("y", 100)
else:
ghost_x, ghost_y = predicted_state["x"], predicted_state["y"]
# Render the scene.
screen.fill((0, 0, 0))
# Draw remote players (using server state).
for cid, data in players.items():
if cid == my_client_id:
continue
x = int(data.get("x", 100))
y = int(data.get("y", 100))
uname = data.get("username", "Guest")
pygame.draw.circle(screen, other_color, (x, y), 20)
font = pygame.font.SysFont(None, 24)
text_surface = font.render(uname, True, (255, 255, 255))
text_rect = text_surface.get_rect(center=(x, y - 30))
screen.blit(text_surface, text_rect)
# Draw the ghost (server-authoritative) position as an outlined circle.
font = pygame.font.SysFont(None, 24)
ghost_text = font.render("Ghost", True, (255, 255, 255))
ghost_rect = ghost_text.get_rect(center=(int(ghost_x), int(ghost_y) - 30))
pygame.draw.circle(screen, ghost_color, (int(ghost_x), int(ghost_y)), 20, 2)
screen.blit(ghost_text, ghost_rect)
# Draw the client-predicted position as the actual player (filled circle).
pred_x = int(predicted_state.get("x", 100))
pred_y = int(predicted_state.get("y", 100))
font = pygame.font.SysFont(None, 24)
client_text = font.render(predicted_state.get("username", "Guest"), True, (255, 255, 255))
client_rect = client_text.get_rect(center=(pred_x, pred_y - 30))
pygame.draw.circle(screen, client_color, (pred_x, pred_y), 20)
screen.blit(client_text, client_rect)
# Display ping on the screen.
ping_font = pygame.font.SysFont(None, 24)
ping_surface = ping_font.render(f"Ping: {int(ping*1000)} ms", True, (255, 255, 0))
screen.blit(ping_surface, (10, 10))
pygame.display.flip()
if __name__ == "__main__":
main()

149
server.py Normal file
View File

@ -0,0 +1,149 @@
import socket
import threading
import json
import time
import uuid
HOST = '127.0.0.1'
PORT = 65432
# Global dictionaries for event subscriptions, client IDs, and game state.
event_subscriptions = {} # event_name -> list of client sockets
client_ids = {} # conn -> client_id
client_states = {} # conn -> {"client_id", "username", "x", "y", "keys"}
lock = threading.Lock()
MOVE_SPEED = 200 # pixels per second
def handle_client(conn, addr):
client_id = str(uuid.uuid4())
with lock:
client_ids[conn] = client_id
# Initialize state: default username "Guest", starting position, and an empty set of pressed keys.
client_states[conn] = {"client_id": client_id, "username": "Guest", "x": 100, "y": 100, "keys": set()}
print(f"[NEW CONNECTION] {addr} connected as {client_id}")
# Send a dedicated "self_id" event so the client knows its unique id.
try:
msg = json.dumps({'event': 'self_id', 'payload': {'client_id': client_id}}) + "\n"
conn.sendall(msg.encode())
except Exception as e:
print(f"[ERROR] sending self_id: {e}")
# Broadcast that a new client connected.
send_event("client_connect", {"client_id": client_id})
try:
file_obj = conn.makefile('r')
for line in file_obj:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
msg_type = msg.get('type')
if msg_type == 'register_event':
event_name = msg.get('event')
with lock:
event_subscriptions.setdefault(event_name, []).append(conn)
print(f"[REGISTER] {client_id} subscribed to '{event_name}'")
elif msg_type == 'set_username':
username = msg.get('username', 'Guest')
with lock:
if conn in client_states:
client_states[conn]['username'] = username
print(f"[USERNAME] {client_id} set username to '{username}'")
elif msg_type == 'keydown':
key = msg.get('key')
with lock:
if conn in client_states:
client_states[conn]['keys'].add(key)
print(f"[KEYDOWN] {client_id} key '{key}' pressed")
elif msg_type == 'keyup':
key = msg.get('key')
with lock:
if conn in client_states and key in client_states[conn]['keys']:
client_states[conn]['keys'].remove(key)
print(f"[KEYUP] {client_id} key '{key}' released")
elif msg_type == 'ping':
# Respond immediately with a pong, echoing the timestamp.
timestamp = msg.get('timestamp')
response = json.dumps({'event': 'pong', 'payload': {'timestamp': timestamp}}) + "\n"
conn.sendall(response.encode())
# Additional message types can be handled here.
except json.JSONDecodeError:
print(f"[ERROR] Invalid JSON from {client_id}: {line}")
except Exception as e:
print(f"[ERROR] Exception with {client_id}: {e}")
finally:
with lock:
for subs in event_subscriptions.values():
if conn in subs:
subs.remove(conn)
client_ids.pop(conn, None)
client_states.pop(conn, None)
print(f"[DISCONNECT] {client_id} disconnected.")
send_event("client_disconnect", {"client_id": client_id})
conn.close()
def send_event(event_name, payload):
"""
Broadcast an event to all clients subscribed to the given event.
"""
message = json.dumps({'event': event_name, 'payload': payload}) + "\n"
with lock:
receivers = event_subscriptions.get(event_name, [])
for client in receivers[:]: # iterate over a copy to allow removals
try:
client.sendall(message.encode())
except Exception as e:
print(f"[ERROR] Failed to send to client, removing: {e}")
receivers.remove(client)
def game_loop():
"""
Main game loop: update each clients position based on currently pressed keys,
and broadcast the overall game state (including server time) to all clients.
"""
dt = 0.1 #100 ms;
while True:
time.sleep(dt)
with lock:
# Update each client's position.
for state in client_states.values():
dx, dy = 0, 0
if 'left' in state['keys']:
dx -= MOVE_SPEED * dt
if 'right' in state['keys']:
dx += MOVE_SPEED * dt
if 'up' in state['keys']:
dy -= MOVE_SPEED * dt
if 'down' in state['keys']:
dy += MOVE_SPEED * dt
state['x'] += dx
state['y'] += dy
# Build payload: include current server time and each clients state.
players_payload = {}
for state in client_states.values():
players_payload[state['client_id']] = {
"username": state['username'],
"x": state['x'],
"y": state['y']
}
payload = {"server_time": time.time(), "players": players_payload}
send_event("state_update", payload)
def start_server():
print("[STARTING] Server is starting...")
threading.Thread(target=game_loop, daemon=True).start()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print(f"[LISTENING] Server is listening on {HOST}:{PORT}")
while True:
conn, addr = s.accept()
threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()
if __name__ == "__main__":
start_server()