commit ca656d1ca8491ed1ef3d639832eb4c74e70e6d06 Author: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Tue Mar 25 11:47:59 2025 -0500 Working on Lag Compensaion diff --git a/client.py b/client.py new file mode 100644 index 0000000..16e93ac --- /dev/null +++ b/client.py @@ -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() diff --git a/server.py b/server.py new file mode 100644 index 0000000..af02416 --- /dev/null +++ b/server.py @@ -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 client’s 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 client’s 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()