182 lines
5.8 KiB
Python
182 lines
5.8 KiB
Python
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
|