This commit is contained in:
OusmBlueNinja 2025-03-31 10:14:04 -05:00
parent ea58b8e46f
commit 9b6defebe8
2 changed files with 164 additions and 286 deletions

229
client.py
View File

@ -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__":

221
server.py
View File

@ -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
}