From ea58b8e46f5f127025342104e33085fcd8571d5b Mon Sep 17 00:00:00 2001 From: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Wed, 26 Mar 2025 18:35:34 -0500 Subject: [PATCH] Added some utils --- client.py | 179 +++++++++++++++++++++++++++++++++++++--------------- server.py | 185 +++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 294 insertions(+), 70 deletions(-) diff --git a/client.py b/client.py index 0d5d791..69241b0 100644 --- a/client.py +++ b/client.py @@ -4,10 +4,16 @@ import pickle import pygame import sys import time +import math HOST = '127.0.0.1' PORT = 65432 MOVE_SPEED = 200 # pixels per second +GUN_LENGTH = 30 +PLAYER_RADIUS = 20 +BULLET_RADIUS = 4 +FLASH_EFFECT_RADIUS = 200 +FLASH_DURATION = 1.0 # must match server value class Event: def __init__(self, name, payload): @@ -36,9 +42,11 @@ def recv_event(sock): data = recvall(sock, msg_len) return pickle.loads(data) -# Global state -players = {} # Server-authoritative state: client_id -> {"username", "x", "y", "mouse_x", "mouse_y"} -my_client_id = None # Our unique client id from the server +# Global state. +players = {} # client_id -> {"username", "x", "y", "mouse_x", "mouse_y", "health"} +bullets = [] # List of bullet dicts from server. +grenades = [] # List of grenade dicts. +my_client_id = None # Our unique client id from the server. predicted_state = { "x": 100, "y": 100, "keys": set(), @@ -47,10 +55,11 @@ predicted_state = { "mouse_y": 0 } ping = 0 # measured ping in seconds -server_details = {} # Updated from server_info and state_update events +server_details = {} # Updated from server_info and state_update events. +map_obstacles = [] # Map obstacles from server info. def listen_to_server(sock): - global my_client_id, players, predicted_state, ping, server_details + global my_client_id, players, predicted_state, ping, server_details, bullets, grenades, map_obstacles while True: try: event = recv_event(sock) @@ -62,6 +71,7 @@ def listen_to_server(sock): print(f"[INFO] My client ID: {my_client_id}") elif event.name == "server_info": server_details = event.payload + map_obstacles = server_details.get("map", []) print(f"[INFO] Received server info: {server_details}") elif event.name == "client_connect": print(f"[INFO] Client connected: {event.payload.get('client_id')}") @@ -72,6 +82,8 @@ def listen_to_server(sock): del players[cid] elif event.name == "state_update": players = event.payload.get("players", {}) + bullets = event.payload.get("bullets", []) + grenades = event.payload.get("grenades", []) # Update dynamic server details. if "total_users" in event.payload: server_details["total_users"] = event.payload["total_users"] @@ -127,17 +139,14 @@ def main(): pygame.init() screen = pygame.display.set_mode((800, 600)) - pygame.display.set_caption("Pygame Client - Multiplayer (Real vs Fake)") + pygame.display.set_caption("Top Down Shooter Multiplayer") clock = pygame.time.Clock() - ghost_color = (0, 255, 0) # Ghost: server state (outlined) - client_color = (0, 0, 255) # Client: predicted state (filled) - other_color = (255, 0, 0) # Remote players - remote_mouse_color = (255, 255, 0) # Color for remote mouse pointer (yellow) local_keys = set() while True: dt = clock.tick(60) / 1000.0 + for ev in pygame.event.get(): if ev.type == pygame.QUIT: pygame.quit() @@ -152,6 +161,13 @@ def main(): key_name = "up" elif ev.key == pygame.K_DOWN: key_name = "down" + # Grenade throw keys. + elif ev.key == pygame.K_q: + send_message(sock, Event("throw_smoke", {})) + print("[ACTION] Smoke grenade thrown") + elif ev.key == pygame.K_e: + send_message(sock, Event("throw_flash", {})) + print("[ACTION] Flash grenade thrown") if key_name and key_name not in local_keys: local_keys.add(key_name) predicted_state["keys"].add(key_name) @@ -176,9 +192,18 @@ def main(): predicted_state["mouse_x"] = mouse_x predicted_state["mouse_y"] = mouse_y send_message(sock, Event("mouse_move", {"x": mouse_x, "y": mouse_y})) - + elif ev.type == pygame.MOUSEBUTTONDOWN: + if ev.button == 1: # Left click: shoot + mouse_x, mouse_y = ev.pos + dx = mouse_x - predicted_state["x"] + dy = mouse_y - predicted_state["y"] + angle = math.atan2(dy, dx) + send_message(sock, Event("shoot", {"angle": angle})) + print("[ACTION] Shoot event sent") + # Update predicted state locally. - dx, dy = 0, 0 + dx = 0 + dy = 0 if "left" in predicted_state["keys"]: dx -= MOVE_SPEED * dt if "right" in predicted_state["keys"]: @@ -190,50 +215,106 @@ def main(): predicted_state["x"] += dx predicted_state["y"] += dy - # For our own player, the ghost (server) position is used as the "real" state. - 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"] + # Camera follows local player. + local_x = predicted_state["x"] + local_y = predicted_state["y"] + screen_width, screen_height = 800, 600 + camera_offset = (local_x - screen_width // 2, local_y - screen_height // 2) - screen.fill((0, 0, 0)) - # Draw remote players (only real state). + # Drawing. + screen.fill((30, 30, 30)) # Background. + + # Draw map obstacles. + for obs in map_obstacles: + rect = pygame.Rect(obs["x"] - camera_offset[0], obs["y"] - camera_offset[1], obs["width"], obs["height"]) + pygame.draw.rect(screen, (100, 100, 100), rect) + + # Draw bullets. + for bullet in bullets: + bx = int(bullet.get("x", 0) - camera_offset[0]) + by = int(bullet.get("y", 0) - camera_offset[1]) + pygame.draw.circle(screen, (255, 255, 255), (bx, by), BULLET_RADIUS) + + # Draw grenades. + for gren in grenades: + gx = int(gren.get("x", 0) - camera_offset[0]) + gy = int(gren.get("y", 0) - camera_offset[1]) + gtype = gren.get("type", "") + if gren.get("fuse", 0) > 0: + pygame.draw.circle(screen, (200, 200, 200), (gx, gy), 6) + else: + if gtype == "smoke": + # Draw semi-transparent smoke flood fill. + smoke_radius = 80 + smoke_surface = pygame.Surface((smoke_radius*2, smoke_radius*2), pygame.SRCALPHA) + pygame.draw.circle(smoke_surface, (100, 100, 100, 150), (smoke_radius, smoke_radius), smoke_radius) + screen.blit(smoke_surface, (gx - smoke_radius, gy - smoke_radius)) + elif gtype == "flash": + pygame.draw.circle(screen, (255, 255, 0), (gx, gy), 30) + + # Draw players. for cid, data in players.items(): - if cid == my_client_id: - continue - x = int(data.get("x", 100)) - y = int(data.get("y", 100)) + # Get player position and health. + px = data.get("x", 100) + py = data.get("y", 100) + health = data.get("health", 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 remote mouse pointer if available. - if "mouse_x" in data and "mouse_y" in data: - mx = int(data.get("mouse_x", 0)) - my = int(data.get("mouse_y", 0)) - pygame.draw.circle(screen, remote_mouse_color, (mx, my), 5) - # Draw our ghost (server-authoritative) 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 our client-predicted player as a 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) + # Determine aiming direction. + if cid == my_client_id: + aim_dx = predicted_state["mouse_x"] - local_x + aim_dy = predicted_state["mouse_y"] - local_y + else: + aim_dx = data.get("mouse_x", 0) - px + aim_dy = data.get("mouse_y", 0) - py + aim_angle = math.atan2(aim_dy, aim_dx) + # Adjust drawing positions by camera offset. + draw_x = int(px - camera_offset[0]) + draw_y = int(py - camera_offset[1]) + # Draw player body. + if cid == my_client_id: + color = (0, 0, 255) + else: + color = (255, 0, 0) + pygame.draw.circle(screen, color, (draw_x, draw_y), PLAYER_RADIUS) + # Draw face (an eye) indicating aim. + eye_offset = 10 + eye_x = draw_x + int(eye_offset * math.cos(aim_angle)) + eye_y = draw_y + int(eye_offset * math.sin(aim_angle)) + pygame.draw.circle(screen, (255, 255, 255), (eye_x, eye_y), 4) + # Draw health bar above player. + bar_width = 40 + bar_height = 5 + health_ratio = health / 100 + health_bar_back = pygame.Rect(draw_x - bar_width//2, draw_y - PLAYER_RADIUS - 15, bar_width, bar_height) + health_bar_front = pygame.Rect(draw_x - bar_width//2, draw_y - PLAYER_RADIUS - 15, int(bar_width * health_ratio), bar_height) + pygame.draw.rect(screen, (100, 100, 100), health_bar_back) + pygame.draw.rect(screen, (0, 255, 0), health_bar_front) + # Draw username. + font = pygame.font.SysFont(None, 20) + name_surface = font.render(uname, True, (255, 255, 255)) + screen.blit(name_surface, (draw_x - name_surface.get_width()//2, draw_y - PLAYER_RADIUS - 30)) + + # Flash effect: if a flash grenade is active and the local player is nearby, overlay white. + flash_alpha = 0 + for gren in grenades: + if gren.get("type") == "flash" and gren.get("fuse", 0) <= 0: + gx = gren.get("x", 0) + gy = gren.get("y", 0) + dist = math.hypot(local_x - gx, local_y - gy) + if dist < FLASH_EFFECT_RADIUS: + intensity = gren.get("duration", 0) / FLASH_DURATION + flash_alpha = max(flash_alpha, int(255 * intensity)) + if flash_alpha > 0: + flash_overlay = pygame.Surface((screen_width, screen_height)) + flash_overlay.fill((255, 255, 255)) + flash_overlay.set_alpha(flash_alpha) + screen.blit(flash_overlay, (0, 0)) + # Display ping. 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)) - # Display HUD info: total users, client ID, and tickrate. + # Display HUD info. hud_font = pygame.font.SysFont(None, 24) total_users = server_details.get("total_users", len(players)) max_users = server_details.get("max_users", "?") diff --git a/server.py b/server.py index 69bf562..e407671 100644 --- a/server.py +++ b/server.py @@ -3,13 +3,44 @@ import threading import pickle import time import uuid +import math HOST = '127.0.0.1' PORT = 65432 -MOVE_SPEED = 200 # pixels per second -TICKRATE = 64 # ticks per second +MOVE_SPEED = 200 # pixels per second for players +TICKRATE = 128 # ticks per second (sub-tick simulation) MAX_USERS = 100 +BULLET_SPEED = 400 # pixels per second +BULLET_LIFETIME = 2.0 # seconds +BULLET_DAMAGE = 25 # damage per bullet +PLAYER_RADIUS = 20 +BULLET_RADIUS = 4 + +SMOKE_GRENADE_FUSE = 0.5 # seconds before smoke activates +SMOKE_DURATION = 5.0 # active smoke duration (seconds) +FLASH_GRENADE_FUSE = 0.3 # seconds before flash activates +FLASH_DURATION = 1.0 # active flash duration (seconds) + +# Define map obstacles. +map_obstacles = [ + {"x": 200, "y": 150, "width": 100, "height": 300}, + {"x": 500, "y": 100, "width": 50, "height": 400}, + {"x": 100, "y": 500, "width": 600, "height": 50}, +] + +def circle_rect_collision(cx, cy, radius, rect): + closest_x = max(rect["x"], min(cx, rect["x"] + rect["width"])) + closest_y = max(rect["y"], min(cy, rect["y"] + rect["height"])) + dx = cx - closest_x + dy = cy - closest_y + return (dx*dx + dy*dy) < (radius*radius) + +def circle_circle_collision(x1, y1, r1, x2, y2, r2): + dx = x1 - x2 + dy = y1 - y2 + return (dx*dx + dy*dy) < ((r1 + r2) ** 2) + class Event: def __init__(self, name, payload): self.name = name @@ -37,17 +68,19 @@ def recv_event(sock): data = recvall(sock, msg_len) return pickle.loads(data) -# Global dictionaries +# Global dictionaries. event_subscriptions = {} # event name -> list of client sockets client_ids = {} # conn -> client_id -client_states = {} # conn -> {"client_id", "username", "x", "y", "keys", "mouse_x", "mouse_y"} +client_states = {} # conn -> {"client_id", "username", "x", "y", "keys", "mouse_x", "mouse_y", "health"} lock = threading.Lock() +bullets = [] # List of dicts: {"x", "y", "dx", "dy", "life", "shooter"} +grenades = [] # List of dicts: {"type": "smoke" or "flash", "x", "y", "fuse", "duration"} + def handle_client(conn, addr): client_id = str(uuid.uuid4()) with lock: client_ids[conn] = client_id - # Initialize state for new client. client_states[conn] = { "client_id": client_id, "username": "Guest", @@ -55,27 +88,26 @@ def handle_client(conn, addr): "y": 100, "keys": set(), "mouse_x": 0, - "mouse_y": 0 + "mouse_y": 0, + "health": 100 } print(f"[NEW CONNECTION] {addr} connected as {client_id}") try: - # Send self_id so the client knows its unique id. send_event(conn, Event("self_id", {"client_id": client_id})) - # Send server info. server_info = { "tickrate": TICKRATE, "total_users": len(client_states), "max_users": MAX_USERS, "server_ip": HOST, "server_port": PORT, - "server_name": "My Multiplayer Server" + "server_name": "My Multiplayer Shooter Server", + "map": map_obstacles } send_event(conn, Event("server_info", server_info)) except Exception as e: print(f"[ERROR] sending self_id/server_info: {e}") - # Broadcast that a new client connected. broadcast_event("client_connect", {"client_id": client_id}) try: @@ -113,14 +145,56 @@ def handle_client(conn, addr): if conn in client_states: client_states[conn]['mouse_x'] = x client_states[conn]['mouse_y'] = y - # Optionally log mouse moves. - # print(f"[MOUSE_MOVE] {client_id} moved mouse to ({x}, {y})") + elif event.name == "shoot": + angle = event.payload.get("angle") + with lock: + if conn in client_states: + state = client_states[conn] + bx = state["x"] + by = state["y"] + dx = math.cos(angle) * BULLET_SPEED + dy = math.sin(angle) * BULLET_SPEED + bullet = { + "x": bx, + "y": by, + "dx": dx, + "dy": dy, + "life": BULLET_LIFETIME, + "shooter": client_ids[conn] + } + bullets.append(bullet) + print(f"[SHOOT] {client_ids[conn]} fired a bullet") + elif event.name == "throw_smoke": + with lock: + if conn in client_states: + state = client_states[conn] + grenade = { + "type": "smoke", + "x": state["x"], + "y": state["y"], + "fuse": SMOKE_GRENADE_FUSE, + "duration": SMOKE_DURATION + } + grenades.append(grenade) + print(f"[GRENADE] {client_ids[conn]} threw a smoke grenade") + elif event.name == "throw_flash": + with lock: + if conn in client_states: + state = client_states[conn] + grenade = { + "type": "flash", + "x": state["x"], + "y": state["y"], + "fuse": FLASH_GRENADE_FUSE, + "duration": FLASH_DURATION + } + grenades.append(grenade) + print(f"[GRENADE] {client_ids[conn]} threw a flash grenade") elif event.name == "ping": timestamp = event.payload.get('timestamp') send_event(conn, Event("pong", {"timestamp": timestamp})) - # You can handle additional events here. except Exception as e: - print(f"[ERROR] Exception with {client_id}: {e}") + print(f"[ERROR] Exception with {client_ids.get(conn, 'unknown')}: {e}") finally: with lock: for subs in event_subscriptions.values(): @@ -144,24 +218,90 @@ def broadcast_event(event_name, payload): receivers.remove(client) def game_loop(): - dt = 1 / TICKRATE # Tick interval + global bullets, grenades + dt = 1 / TICKRATE while True: time.sleep(dt) with lock: - # Update each client's position based on pressed keys. + # Update player positions with axis separation and collision. for state in client_states.values(): - dx, dy = 0, 0 + # Horizontal movement. + dx = 0 if 'left' in state['keys']: dx -= MOVE_SPEED * dt if 'right' in state['keys']: dx += MOVE_SPEED * dt + new_x = state['x'] + dx + colliding = False + for obs in map_obstacles: + if circle_rect_collision(new_x, state['y'], PLAYER_RADIUS, obs): + colliding = True + break + if not colliding: + state['x'] = new_x + # Vertical movement. + dy = 0 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 for state_update. + new_y = state['y'] + dy + colliding = False + for obs in map_obstacles: + if circle_rect_collision(state['x'], new_y, PLAYER_RADIUS, obs): + colliding = True + break + if not colliding: + state['y'] = new_y + + # Update bullets. + new_bullets = [] + for bullet in bullets: + bullet["x"] += bullet["dx"] * dt + bullet["y"] += bullet["dy"] * dt + bullet["life"] -= dt + if bullet["life"] <= 0: + continue + # Check collision with obstacles. + hit_obstacle = False + for obs in map_obstacles: + if circle_rect_collision(bullet["x"], bullet["y"], BULLET_RADIUS, obs): + hit_obstacle = True + break + if hit_obstacle: + continue + # Check collision with players. + hit_player = False + for state in client_states.values(): + if state["client_id"] == bullet["shooter"]: + continue + if circle_circle_collision(bullet["x"], bullet["y"], BULLET_RADIUS, state["x"], state["y"], PLAYER_RADIUS): + state["health"] -= BULLET_DAMAGE + if state["health"] <= 0: + # Respawn the player. + state["health"] = 100 + state["x"] = 100 + state["y"] = 100 + hit_player = True + break + if hit_player: + continue + new_bullets.append(bullet) + bullets = new_bullets + + # Update grenades. + new_grenades = [] + for gren in grenades: + if gren["fuse"] > 0: + gren["fuse"] -= dt + new_grenades.append(gren) + else: + gren["duration"] -= dt + if gren["duration"] > 0: + new_grenades.append(gren) + grenades = new_grenades + + # Build payload. players_payload = {} for state in client_states.values(): players_payload[state['client_id']] = { @@ -169,11 +309,14 @@ def game_loop(): "x": state['x'], "y": state['y'], "mouse_x": state.get('mouse_x', 0), - "mouse_y": state.get('mouse_y', 0) + "mouse_y": state.get('mouse_y', 0), + "health": state.get('health', 100) } total_users = len(client_states) payload = { "players": players_payload, + "bullets": bullets, + "grenades": grenades, "total_users": total_users, "max_users": MAX_USERS }