From 9b6defebe8e1c1dbb1a35ae0c99cb4f4dc4f19d4 Mon Sep 17 00:00:00 2001 From: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:14:04 -0500 Subject: [PATCH] Car game --- client.py | 229 ++++++++++++++++++++++++------------------------------ server.py | 221 +++++++++++++++------------------------------------- 2 files changed, 164 insertions(+), 286 deletions(-) diff --git a/client.py b/client.py index 69241b0..a43c81a 100644 --- a/client.py +++ b/client.py @@ -8,12 +8,18 @@ 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 + +# Racing physics constants. +ACCELERATION = 300.0 # pixels per second^2 +BRAKE_DECELERATION = 400.0 # pixels per second^2 +FRICTION = 200.0 # pixels per second^2 +TURN_SPEED = math.radians(120) # radians per second (e.g., 120° per second) +MAX_SPEED = 400.0 # maximum forward speed +MIN_SPEED = -200.0 # maximum reverse speed + +# Dimensions for the car. +CAR_WIDTH = 40 +CAR_HEIGHT = 20 class Event: def __init__(self, name, payload): @@ -43,23 +49,22 @@ def recv_event(sock): return pickle.loads(data) # 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. +players = {} # client_id -> {"username", "x", "y", "angle", "speed"} my_client_id = None # Our unique client id from the server. +# Predicted state for our own car. predicted_state = { "x": 100, "y": 100, + "angle": 0, # Facing right initially. + "speed": 0, "keys": set(), - "username": "Guest", - "mouse_x": 0, - "mouse_y": 0 + "username": "Guest" } ping = 0 # measured ping in seconds 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, bullets, grenades, map_obstacles + global my_client_id, players, predicted_state, ping, server_details, map_obstacles while True: try: event = recv_event(sock) @@ -81,9 +86,8 @@ def listen_to_server(sock): if cid in players: del players[cid] elif event.name == "state_update": + # Expect players to have "x", "y", "angle", "speed", "username". 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"] @@ -92,14 +96,20 @@ def listen_to_server(sock): 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) + server_angle = players[my_client_id].get("angle", 0) + server_speed = players[my_client_id].get("speed", 0) tickrate = server_details.get("tickrate", 64) correction_factor = 0.1 * (64 / tickrate) if not predicted_state["keys"]: predicted_state["x"] = server_x predicted_state["y"] = server_y + predicted_state["angle"] = server_angle + predicted_state["speed"] = server_speed else: predicted_state["x"] += (server_x - predicted_state["x"]) * correction_factor predicted_state["y"] += (server_y - predicted_state["y"]) * correction_factor + predicted_state["angle"] += (server_angle - predicted_state["angle"]) * correction_factor + predicted_state["speed"] += (server_speed - predicted_state["speed"]) * correction_factor elif event.name == "pong": sent_time = event.payload.get("timestamp") if sent_time: @@ -119,6 +129,26 @@ def ping_loop(sock): send_message(sock, Event("ping", {"timestamp": ts})) time.sleep(1) +def draw_car(surface, x, y, angle, color): + # Define a simple rectangle for the car. + half_w = CAR_WIDTH / 2 + half_h = CAR_HEIGHT / 2 + # Corners of the rectangle before rotation. + corners = [ + (-half_w, -half_h), + (half_w, -half_h), + (half_w, half_h), + (-half_w, half_h) + ] + rotated = [] + cos_a = math.cos(angle) + sin_a = math.sin(angle) + for cx, cy in corners: + rx = cx * cos_a - cy * sin_a + ry = cx * sin_a + cy * cos_a + rotated.append((int(x + rx), int(y + ry))) + pygame.draw.polygon(surface, color, rotated) + def main(): global my_client_id, predicted_state username = input("Enter your username: ") @@ -139,13 +169,13 @@ def main(): pygame.init() screen = pygame.display.set_mode((800, 600)) - pygame.display.set_caption("Top Down Shooter Multiplayer") + pygame.display.set_caption("Top Down Racing Multiplayer") clock = pygame.time.Clock() local_keys = set() while True: - dt = clock.tick(60) / 1000.0 + dt = clock.tick(60) / 1000.0 # Delta time in seconds for ev in pygame.event.get(): if ev.type == pygame.QUIT: @@ -161,13 +191,6 @@ 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) @@ -187,35 +210,41 @@ def main(): if key_name in predicted_state["keys"]: predicted_state["keys"].remove(key_name) send_message(sock, Event("keyup", {"key": key_name})) - elif ev.type == pygame.MOUSEMOTION: - mouse_x, mouse_y = ev.pos - 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 = 0 - dy = 0 - if "left" in predicted_state["keys"]: - dx -= MOVE_SPEED * dt - if "right" in predicted_state["keys"]: - dx += MOVE_SPEED * dt + # Racing physics update. + # Acceleration/Braking. 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 + predicted_state["speed"] += ACCELERATION * dt + elif "down" in predicted_state["keys"]: + predicted_state["speed"] -= BRAKE_DECELERATION * dt + else: + # Apply friction to gradually slow the car. + if predicted_state["speed"] > 0: + predicted_state["speed"] -= FRICTION * dt + if predicted_state["speed"] < 0: + predicted_state["speed"] = 0 + elif predicted_state["speed"] < 0: + predicted_state["speed"] += FRICTION * dt + if predicted_state["speed"] > 0: + predicted_state["speed"] = 0 - # Camera follows local player. + # Clamp speed. + if predicted_state["speed"] > MAX_SPEED: + predicted_state["speed"] = MAX_SPEED + if predicted_state["speed"] < MIN_SPEED: + predicted_state["speed"] = MIN_SPEED + + # Steering. + if "left" in predicted_state["keys"]: + predicted_state["angle"] -= TURN_SPEED * dt + if "right" in predicted_state["keys"]: + predicted_state["angle"] += TURN_SPEED * dt + + # Update position based on current speed and angle. + predicted_state["x"] += predicted_state["speed"] * math.cos(predicted_state["angle"]) * dt + predicted_state["y"] += predicted_state["speed"] * math.sin(predicted_state["angle"]) * dt + + # Camera follows the local car. local_x = predicted_state["x"] local_y = predicted_state["y"] screen_width, screen_height = 800, 600 @@ -224,107 +253,51 @@ def main(): # Drawing. screen.fill((30, 30, 30)) # Background. - # Draw map obstacles. + # Draw map obstacles (if any). for obs in map_obstacles: - rect = pygame.Rect(obs["x"] - camera_offset[0], obs["y"] - camera_offset[1], obs["width"], obs["height"]) + 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. + # Draw players as cars. for cid, data in players.items(): - # Get player position and health. + # Get car position and orientation. px = data.get("x", 100) py = data.get("y", 100) - health = data.get("health", 100) + angle = data.get("angle", 0) uname = data.get("username", "Guest") - # Determine aiming direction. + # For our own car, use the predicted state. if cid == my_client_id: - aim_dx = predicted_state["mouse_x"] - local_x - aim_dy = predicted_state["mouse_y"] - local_y + px = predicted_state["x"] + py = predicted_state["y"] + angle = predicted_state["angle"] + color = (0, 0, 255) # Blue for local player. 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. + color = (255, 0, 0) # Red for others. + draw_x = px - camera_offset[0] + draw_y = py - camera_offset[1] + draw_car(screen, draw_x, draw_y, angle, color) + # Draw username above the car. 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)) + screen.blit(name_surface, (draw_x - name_surface.get_width() // 2, draw_y - 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. + # Display ping and HUD info. ping_font = pygame.font.SysFont(None, 24) - ping_surface = ping_font.render(f"Ping: {int(ping*1000)} ms", True, (255, 255, 0)) + ping_surface = ping_font.render(f"Ping: {int(ping * 1000)} ms", True, (255, 255, 0)) screen.blit(ping_surface, (10, 10)) - # 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", "?") tickrate = server_details.get("tickrate", 64) hud_text = hud_font.render( - f"Users: {total_users}/{max_users} | Client ID: {my_client_id or 'N/A'} | Tickrate: {tickrate} tps", + f"Speed: {int(predicted_state['speed'])} px/s | Users: {total_users}/{max_users} | Client ID: {my_client_id or 'N/A'} | Tickrate: {tickrate} tps", True, (255, 255, 255) ) screen.blit(hud_text, (10, 40)) + pygame.display.flip() if __name__ == "__main__": diff --git a/server.py b/server.py index e407671..361ba07 100644 --- a/server.py +++ b/server.py @@ -7,20 +7,19 @@ import math HOST = '127.0.0.1' PORT = 65432 -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 +# Racing physics constants. +ACCELERATION = 300.0 # pixels per second^2 (forward) +BRAKE_DECELERATION = 400.0 # pixels per second^2 (when braking) +FRICTION = 200.0 # pixels per second^2 (natural deceleration) +TURN_SPEED = math.radians(120) # radians per second (steering) +MAX_SPEED = 400.0 # maximum forward speed (pixels/sec) +MIN_SPEED = -200.0 # maximum reverse speed (pixels/sec) -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) +# Player appearance (for collision). +PLAYER_RADIUS = 20 # Define map obstacles. map_obstacles = [ @@ -34,12 +33,7 @@ def circle_rect_collision(cx, cy, radius, rect): 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) + return (dx * dx + dy * dy) < (radius * radius) class Event: def __init__(self, name, payload): @@ -71,12 +65,10 @@ def recv_event(sock): # 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", "health"} +# Each client state now stores position, angle, speed, and keys. +client_states = {} # conn -> {"client_id", "username", "x", "y", "angle", "speed", "keys"} 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: @@ -86,10 +78,9 @@ def handle_client(conn, addr): "username": "Guest", "x": 100, "y": 100, - "keys": set(), - "mouse_x": 0, - "mouse_y": 0, - "health": 100 + "angle": 0, # Facing right initially. + "speed": 0, + "keys": set() } print(f"[NEW CONNECTION] {addr} connected as {client_id}") @@ -101,7 +92,7 @@ def handle_client(conn, addr): "max_users": MAX_USERS, "server_ip": HOST, "server_port": PORT, - "server_name": "My Multiplayer Shooter Server", + "server_name": "My Top Down Racing Server", "map": map_obstacles } send_event(conn, Event("server_info", server_info)) @@ -138,61 +129,10 @@ def handle_client(conn, addr): 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 event.name == "mouse_move": - x = event.payload.get('x', 0) - y = event.payload.get('y', 0) - with lock: - if conn in client_states: - client_states[conn]['mouse_x'] = x - client_states[conn]['mouse_y'] = 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})) + # Any other events (e.g., mouse_move, shoot) are ignored for racing. except Exception as e: print(f"[ERROR] Exception with {client_ids.get(conn, 'unknown')}: {e}") finally: @@ -218,105 +158,70 @@ def broadcast_event(event_name, payload): receivers.remove(client) def game_loop(): - global bullets, grenades dt = 1 / TICKRATE while True: time.sleep(dt) with lock: - # Update player positions with axis separation and collision. + # Update each player's state using racing physics. for state in client_states.values(): - # 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 + # Acceleration or braking. if 'up' in state['keys']: - dy -= MOVE_SPEED * dt - if 'down' in state['keys']: - dy += MOVE_SPEED * dt - 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) + state['speed'] += ACCELERATION * dt + elif 'down' in state['keys']: + state['speed'] -= BRAKE_DECELERATION * dt else: - gren["duration"] -= dt - if gren["duration"] > 0: - new_grenades.append(gren) - grenades = new_grenades + # Apply friction when no acceleration is active. + if state['speed'] > 0: + state['speed'] -= FRICTION * dt + if state['speed'] < 0: + state['speed'] = 0 + elif state['speed'] < 0: + state['speed'] += FRICTION * dt + if state['speed'] > 0: + state['speed'] = 0 - # Build payload. + # Clamp speed. + if state['speed'] > MAX_SPEED: + state['speed'] = MAX_SPEED + if state['speed'] < MIN_SPEED: + state['speed'] = MIN_SPEED + + # Steering. + if 'left' in state['keys']: + state['angle'] -= TURN_SPEED * dt + if 'right' in state['keys']: + state['angle'] += TURN_SPEED * dt + + # Compute new position. + new_x = state['x'] + state['speed'] * math.cos(state['angle']) * dt + new_y = state['y'] + state['speed'] * math.sin(state['angle']) * dt + + # Check collision with obstacles. + collision = False + for obs in map_obstacles: + if circle_rect_collision(new_x, new_y, PLAYER_RADIUS, obs): + collision = True + break + if not collision: + state['x'] = new_x + state['y'] = new_y + else: + # On collision, stop the car. + state['speed'] = 0 + + # Build payload to send to all clients. players_payload = {} for state in client_states.values(): players_payload[state['client_id']] = { "username": state['username'], "x": state['x'], "y": state['y'], - "mouse_x": state.get('mouse_x', 0), - "mouse_y": state.get('mouse_y', 0), - "health": state.get('health', 100) + "angle": state['angle'], + "speed": state['speed'] } total_users = len(client_states) payload = { "players": players_payload, - "bullets": bullets, - "grenades": grenades, "total_users": total_users, "max_users": MAX_USERS }