Compare commits

..

3 Commits

Author SHA1 Message Date
91e93759e8 feat: add reset shortcut 2025-10-20 00:07:40 +02:00
04ac674982 feat: add raycasts 2025-10-19 02:46:38 +02:00
8ca15eaa78 feat: add strips on road 2025-10-19 02:18:50 +02:00
5 changed files with 156 additions and 10 deletions

View File

@@ -1,9 +1,10 @@
from math import radians
from typing import Optional
import pygame
from src.camera import Camera
from src.utils import segments_intersect
from src.utils import get_segments_intersection, segments_intersect
from src.vec import Vec
@@ -12,14 +13,17 @@ sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1)
class Car:
MAX_SPEED = 5
MAX_BACK_SPEED = -2
MAX_BACK_SPEED = -3
ROTATE_SPEED = 1
COLOR = (230, 150, 80)
WIDTH = 0.4
LENGTH = 0.6
COLLISION_MARGIN = 0.4
ACCELERATION = 2
FRICTION = 3
FRICTION = 2.5
N_RAYS = 15
RAYS_FOV = 180
RAYS_MAX_DIST = 100
def __init__(self, pos: Vec, direction: Vec) -> None:
self.pos: Vec = pos
@@ -31,6 +35,9 @@ class Car:
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)]
def update(self, dt: float):
if self.forward:
self.speed += self.ACCELERATION * dt
@@ -53,14 +60,21 @@ class Car:
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):
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)
@@ -76,6 +90,8 @@ class Car:
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]] = [
@@ -101,3 +117,35 @@ class Car:
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

View File

@@ -31,6 +31,7 @@ class Game:
)
self.show_fps: bool = True
self.show_speed: bool = True
self.show_raycasts: bool = True
def mainloop(self):
while self.running:
@@ -38,13 +39,11 @@ class Game:
self.process_pygame_events()
self.car.update(dt)
self.car.check_collisions(self.track.get_collision_polygons())
self.update_camera()
self.render()
self.clock.tick(60)
def process_pygame_events(self):
self.camera.set_pos(self.car.pos)
self.camera.set_direction(self.car.direction)
self.camera.set_size(Vec(*self.win.get_size()))
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.quit()
@@ -58,13 +57,18 @@ class Game:
elif event.type == pygame.KEYUP:
self.on_key_up(event)
def update_camera(self):
self.camera.set_pos(self.car.pos)
self.camera.set_direction(self.car.direction)
self.camera.set_size(Vec(*self.win.get_size()))
def quit(self):
self.running = False
def render(self):
self.win.fill(self.BACKGROUND_COLOR)
self.track.render(self.win, self.camera)
self.car.render(self.win, self.camera)
self.car.render(self.win, self.camera, self.show_raycasts)
if self.show_fps:
self.render_fps()
if self.show_speed:
@@ -85,6 +89,10 @@ class Game:
self.show_fps = not self.show_fps
elif event.key == pygame.K_v:
self.show_speed = not self.show_speed
elif event.key == pygame.K_c:
self.show_raycasts = not self.show_raycasts
elif event.key == pygame.K_r:
self.reset()
def on_key_up(self, event: pygame.event.Event):
if event.key == pygame.K_w:
@@ -125,3 +133,8 @@ class Game:
pts2.append((ox + r2 * dx, oy + r2 * dy))
pygame.draw.polygon(self.win, (200, 200, 200), pts1 + pts2[::-1])
def reset(self):
self.car.pos = self.track.start_pos
self.car.direction = self.track.start_dir
self.car.speed = 0

View File

@@ -10,9 +10,14 @@ from src.vec import Vec
class Road(TrackObject):
type = TrackObjectType.Road
STRIP_LENGTH = 0.5
STRIP_GAP = 0.5
def __init__(self, pts: list[RoadPoint]) -> None:
super().__init__()
self.pts: list[RoadPoint] = pts
self.strips: list[tuple[Vec, Vec]] = []
self.compute_strips()
@classmethod
def load(cls, data: dict) -> Road:
@@ -40,6 +45,15 @@ class Road(TrackObject):
pygame.draw.lines(surf, (255, 255, 255), True, side1)
pygame.draw.lines(surf, (255, 255, 255), True, side2)
for p1, p2 in self.strips:
pygame.draw.line(
surf,
(255, 255, 255),
camera.world2screen(p1),
camera.world2screen(p2),
6,
)
def get_collision_polygons(self) -> list[list[Vec]]:
side1: list[Vec] = []
side2: list[Vec] = []
@@ -49,9 +63,50 @@ class Road(TrackObject):
p3: Vec = p1 - pt.normal * pt.width
side1.append(p2)
side2.append(p3)
return [side1, side2]
def compute_strips(self):
n: int = len(self.pts)
vecs: list[Vec] = [
self.pts[(i + 1) % n].pos - pt.pos for i, pt in enumerate(self.pts)
]
lengths: list[float] = [v.mag() for v in vecs]
cum_sums: list[float] = [0]
for l in lengths:
cum_sums.append(cum_sums[-1] + l)
self.strips = []
total_length: float = sum(lengths)
def get_pt(length: float) -> tuple[int, float]:
length %= total_length
for i, cs in list(enumerate(cum_sums))[::-1]:
if cs <= length:
return (i, (length - cs) / lengths[i])
raise ValueError()
l0: float = 0
while l0 < total_length:
l1: float = l0 + self.STRIP_LENGTH
i0, t0 = get_pt(l0)
i1, t1 = get_pt(l1)
p0: Vec = self.pts[i0].pos + vecs[i0] * t0
p1: Vec = self.pts[i1].pos + vecs[i1] * t1
if i0 == i1:
self.strips.append((p0, p1))
elif (i0 + 1) % n == i1:
pm: Vec = self.pts[i1].pos
self.strips.append((p0, pm))
self.strips.append((pm, p1))
else:
self.strips.append((p0, self.pts[(i0 + 1) % n].pos))
i = (i0 + 1) % n
while i != i1:
i2 = (i + 1) % n
self.strips.append((self.pts[i].pos, self.pts[i2].pos))
i = i2
self.strips.append((self.pts[i1].pos, p1))
l0 = l1 + self.STRIP_GAP
class RoadPoint:
def __init__(self, pos: Vec, normal: Vec, width: float) -> None:

View File

@@ -1,9 +1,9 @@
import os
from pathlib import Path
from typing import Optional
from src.vec import Vec
ROOT = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))
@@ -32,3 +32,30 @@ def segments_intersect(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> bool:
return True
return False
def get_segments_intersection(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> Optional[Vec]:
da: Vec = a2 - a1
db: Vec = b2 - b1
dp: Vec = a1 - b1
dap: Vec = da.perp
denom: float = dap.dot(db)
if abs(denom) < 1e-9:
o1: float = da.cross(-dp)
if abs(o1) < 1e-9:
for p in [b1, b2]:
if p.within(a1, a2):
return p
for p in [a1, a2]:
if p.within(b1, b2):
return p
return None
return None
num: float = dap.dot(dp)
t: float = num / denom
intersection: Vec = b1 + db * t
if intersection.within(a1, a2) and intersection.within(b1, b2):
return intersection
return None

View File

@@ -24,6 +24,9 @@ class Vec:
def __truediv__(self, value: float) -> Vec:
return Vec(self.x / value, self.y / value)
def __neg__(self) -> Vec:
return Vec(-self.x, -self.y)
def dot(self, other: Vec) -> float:
return self.x * other.x + self.y * other.y