from __future__ import annotations from math import radians from typing import TYPE_CHECKING, Optional import pygame from src.camera import Camera from src.remote_controller import RemoteController from src.utils import get_segments_intersection, segments_intersect from src.vec import Vec if TYPE_CHECKING: from src.game import Game def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1) class Car: MAX_SPEED = 5 MAX_BACK_SPEED = -3 ROTATE_SPEED = 1 COLOR = (230, 150, 80) CTRL_COLOR = (80, 230, 150) WIDTH = 0.4 LENGTH = 0.6 COLLISION_MARGIN = 0.4 ACCELERATION = 2 FRICTION = 2.5 N_RAYS = 15 RAYS_FOV = 180 RAYS_MAX_DIST = 100 def __init__(self, game: Game, pos: Vec, direction: Vec) -> None: self.game: Game = game self.initial_pos: Vec = pos.copy() self.initial_dir: Vec = direction.copy() self.pos: Vec = pos self.direction: Vec = direction self.speed: float = 0 self.forward: bool = False self.backward: bool = False self.left: bool = False self.right: bool = False self.colliding: bool = False self.rays: list[float] = [0] * self.N_RAYS self.rays_end: list[Vec] = [Vec() for _ in range(self.N_RAYS)] self.controller: RemoteController = RemoteController(self.game, self) self.controller.start_server() def update(self, dt: float): if self.forward: self.speed += self.ACCELERATION * dt self.speed = min(self.MAX_SPEED, self.speed) if self.backward: self.speed -= self.ACCELERATION * 2 * dt self.speed = max(self.MAX_BACK_SPEED, self.speed) rotate_angle: float = 0 if self.left: rotate_angle -= self.ROTATE_SPEED * dt if self.right: rotate_angle += self.ROTATE_SPEED * dt # if self.backward: # rotate_angle *= -1 if rotate_angle != 0: self.direction = self.direction.rotate(rotate_angle) if not self.forward and not self.backward: fn = max if self.speed >= 0 else min self.speed -= sign(self.speed) * self.FRICTION * dt self.speed = fn(0, self.speed) if abs(self.speed) < 1e-4: self.speed = 0 self.pos += self.direction * self.speed * dt def render(self, surf: pygame.Surface, camera: Camera, show_raycasts: bool = False): if show_raycasts: pos: Vec = camera.world2screen(self.pos) for p in self.rays_end: pygame.draw.line(surf, (255, 0, 0), pos, camera.world2screen(p), 2) pts: list[Vec] = self.get_corners() pts = [camera.world2screen(p) for p in pts] pygame.draw.polygon(surf, self.COLOR, pts) if self.controller.is_connected: pygame.draw.circle( surf, self.CTRL_COLOR, camera.world2screen(self.pos), camera.size2screen(self.WIDTH / 4), ) def get_corners(self) -> list[Vec]: u: Vec = self.direction * self.LENGTH / 2 v: Vec = self.direction.perp * self.WIDTH / 2 pt: Vec = self.pos p1: Vec = pt + u + v p2: Vec = pt - u + v p3: Vec = pt - u - v p4: Vec = pt + u - v return [p1, p2, p3, p4] def check_collisions(self, polygons: list[list[Vec]]): self.cast_rays(polygons) self.colliding = False corners: list[Vec] = self.get_corners() sides: list[tuple[Vec, Vec]] = [ (corners[i], corners[(i + 1) % 4]) for i in range(4) ] for polygon in polygons: n_pts: int = len(polygon) for i in range(n_pts): pt1: Vec = polygon[i] pt2: Vec = polygon[(i + 1) % n_pts] d: Vec = pt2 - pt1 for s1, s2 in sides: if segments_intersect(s1, s2, pt1, pt2): self.colliding = True self.direction = d.normalized n: Vec = self.direction.perp dist: float = (self.pos - pt1).dot(n) if dist < 0: n *= -1 dist = -dist self.speed = 0 self.pos = self.pos + n * \ (self.COLLISION_MARGIN - dist) return def cast_rays(self, polygons: list[list[Vec]]): for i in range(self.N_RAYS): angle: float = radians( (i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV) p: Optional[Vec] = self.cast_ray(angle, polygons) self.rays[i] = self.RAYS_MAX_DIST if p is None else ( p - self.pos).mag() self.rays_end[i] = self.pos if p is None else p def cast_ray(self, angle: float, polygons: list[list[Vec]]) -> Optional[Vec]: v: Vec = self.direction.normalized.rotate(angle) segments: list[tuple[Vec, Vec]] = [] for polygon in polygons: n_pts: int = len(polygon) for i in range(n_pts): pt1: Vec = polygon[i] pt2: Vec = polygon[(i + 1) % n_pts] segments.append((pt1, pt2)) p1: Vec = self.pos p2: Vec = p1 + v * self.RAYS_MAX_DIST dist: float = self.RAYS_MAX_DIST closest: Optional[Vec] = None for q1, q2 in segments: p: Optional[Vec] = get_segments_intersection(p1, p2, q1, q2) if p is not None: d: float = (p - p1).mag() if d < dist: dist = d closest = p return closest def reset(self): self.pos = self.initial_pos.copy() self.direction = self.initial_dir.copy() self.speed = 0