Compare commits
	
		
			10 Commits
		
	
	
		
			12319fc1ab
			...
			09f70223b8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 09f70223b8 | |||
| 45ed1c85c8 | |||
| 2b20582b87 | |||
| 6805e69509 | |||
| 9c5f39b669 | |||
| adb25e6ef6 | |||
| 6276f97cce | |||
| da8c64624f | |||
| e154a1fde9 | |||
| eb10933f4b | 
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-M.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-M.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-R.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Ubuntu-R.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.py
									
									
									
									
									
								
							| @@ -4,6 +4,7 @@ from src.game import Game | ||||
| def main(): | ||||
|     print("Welcome to Rally Racer !") | ||||
|     game: Game = Game() | ||||
|     game.mainloop() | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|   | ||||
							
								
								
									
										43
									
								
								src/camera.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/camera.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class Camera: | ||||
|     UNIT_RATIO = 150 | ||||
|  | ||||
|     def __init__(self) -> None: | ||||
|         self.pos: Vec = Vec() | ||||
|         self.up: Vec = Vec(0, -1) | ||||
|         self.size: Vec = Vec(600, 600) | ||||
|         self.zoom: float = 1 | ||||
|  | ||||
|     def set_pos(self, pos: Vec): | ||||
|         self.pos = pos | ||||
|  | ||||
|     def set_direction(self, up: Vec): | ||||
|         self.up = up.normalized | ||||
|  | ||||
|     def set_size(self, size: Vec): | ||||
|         self.size = size | ||||
|  | ||||
|     @property | ||||
|     def center(self) -> Vec: | ||||
|         return self.size / 2 | ||||
|  | ||||
|     def screen2world(self, screen_pos: Vec) -> Vec: | ||||
|         delta: Vec = screen_pos - self.center | ||||
|         delta /= self.zoom * self.UNIT_RATIO | ||||
|         dx: float = delta.x | ||||
|         dy: float = delta.y | ||||
|  | ||||
|         v1: Vec = self.up.perp * dx | ||||
|         v2: Vec = self.up * dy | ||||
|  | ||||
|         return self.pos + v1 + v2 | ||||
|  | ||||
|     def world2screen(self, world_pos: Vec) -> Vec: | ||||
|         delta: Vec = world_pos - self.pos | ||||
|         dy: float = -delta.dot(self.up) | ||||
|         dx: float = delta.dot(self.up.perp) | ||||
|         screen_delta: Vec = Vec(dx, dy) * self.zoom * self.UNIT_RATIO | ||||
|         screen_pos: Vec = self.center + screen_delta | ||||
|         return screen_pos | ||||
							
								
								
									
										96
									
								
								src/car.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/car.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| from math import radians | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.utils import segments_intersect | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class Car: | ||||
|     MAX_SPEED = 0.05 | ||||
|     MAX_BACK_SPEED = -0.025 | ||||
|     ROTATE_SPEED = radians(1) | ||||
|     COLOR = (230, 150, 80) | ||||
|     WIDTH = 0.4 | ||||
|     LENGTH = 0.6 | ||||
|     COLLISION_MARGIN = 0.4 | ||||
|  | ||||
|     def __init__(self, pos: Vec, direction: Vec) -> None: | ||||
|         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 | ||||
|  | ||||
|     def update(self): | ||||
|         if self.forward: | ||||
|             self.speed += 0.001 | ||||
|             self.speed = min(self.MAX_SPEED, self.speed) | ||||
|  | ||||
|         if self.backward: | ||||
|             self.speed -= 0.002 | ||||
|             self.speed = max(self.MAX_BACK_SPEED, self.speed) | ||||
|  | ||||
|         rotate_angle: float = 0 | ||||
|         if self.left: | ||||
|             rotate_angle -= self.ROTATE_SPEED | ||||
|         if self.right: | ||||
|             rotate_angle += self.ROTATE_SPEED | ||||
|  | ||||
|         # if self.backward: | ||||
|         #    rotate_angle *= -1 | ||||
|  | ||||
|         if rotate_angle != 0: | ||||
|             self.direction = self.direction.rotate(rotate_angle) | ||||
|  | ||||
|         self.speed *= 0.98 | ||||
|         if abs(self.speed) < 1e-8: | ||||
|             self.speed = 0 | ||||
|  | ||||
|         self.pos += self.direction * self.speed | ||||
|  | ||||
|     def render(self, surf: pygame.Surface, camera: Camera): | ||||
|         pts: list[Vec] = self.get_corners() | ||||
|         pts = [camera.world2screen(p) for p in pts] | ||||
|         pygame.draw.polygon(surf, self.COLOR, pts) | ||||
|  | ||||
|     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.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 | ||||
							
								
								
									
										89
									
								
								src/game.py
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								src/game.py
									
									
									
									
									
								
							| @@ -1,9 +1,94 @@ | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.car import Car | ||||
| from src.track import Track | ||||
| from src.utils import ROOT | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class Game: | ||||
|     DEFAULT_SIZE = (1280, 720) | ||||
|     BACKGROUND_COLOR = (80, 80, 80) | ||||
|     MAX_FPS = 60 | ||||
|     FPS_COLOR = (255, 0, 0) | ||||
|  | ||||
|     def __init__(self): | ||||
|     def __init__(self) -> None: | ||||
|         pygame.init() | ||||
|         self.win: pygame.Surface = pygame.display.set_mode(self.DEFAULT_SIZE) | ||||
|         self.win: pygame.Surface = pygame.display.set_mode( | ||||
|             self.DEFAULT_SIZE, pygame.RESIZABLE | ||||
|         ) | ||||
|         pygame.display.set_caption("Rally Racer") | ||||
|         self.running: bool = True | ||||
|         self.track: Track = Track.load("simple") | ||||
|         self.car: Car = Car(self.track.start_pos, self.track.start_dir) | ||||
|         self.camera: Camera = Camera() | ||||
|  | ||||
|         self.clock: pygame.time.Clock = pygame.time.Clock() | ||||
|         self.font: pygame.font.Font = pygame.font.Font( | ||||
|             str(ROOT / "assets" / "fonts" / "Ubuntu-M.ttf"), 20 | ||||
|         ) | ||||
|         self.show_fps: bool = True | ||||
|  | ||||
|     def mainloop(self): | ||||
|         while self.running: | ||||
|             self.process_pygame_events() | ||||
|             self.car.update() | ||||
|             self.car.check_collisions(self.track.get_collision_polygons()) | ||||
|             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() | ||||
|             elif event.type == pygame.VIDEORESIZE: | ||||
|                 self.camera.set_size(Vec(event.w, event.h)) | ||||
|             elif event.type == pygame.KEYDOWN: | ||||
|                 if event.key == pygame.K_ESCAPE: | ||||
|                     self.quit() | ||||
|                 else: | ||||
|                     self.on_key_down(event) | ||||
|             elif event.type == pygame.KEYUP: | ||||
|                 self.on_key_up(event) | ||||
|  | ||||
|     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) | ||||
|         if self.show_fps: | ||||
|             self.render_fps() | ||||
|  | ||||
|         pygame.display.flip() | ||||
|  | ||||
|     def on_key_down(self, event: pygame.event.Event): | ||||
|         if event.key == pygame.K_w: | ||||
|             self.car.forward = True | ||||
|         elif event.key == pygame.K_s: | ||||
|             self.car.backward = True | ||||
|         elif event.key == pygame.K_a: | ||||
|             self.car.left = True | ||||
|         elif event.key == pygame.K_d: | ||||
|             self.car.right = True | ||||
|  | ||||
|     def on_key_up(self, event: pygame.event.Event): | ||||
|         if event.key == pygame.K_w: | ||||
|             self.car.forward = False | ||||
|         elif event.key == pygame.K_s: | ||||
|             self.car.backward = False | ||||
|         elif event.key == pygame.K_a: | ||||
|             self.car.left = False | ||||
|         elif event.key == pygame.K_d: | ||||
|             self.car.right = False | ||||
|  | ||||
|     def render_fps(self): | ||||
|         txt: pygame.Surface = self.font.render( | ||||
|             f"{self.clock.get_fps():.1f}", True, self.FPS_COLOR | ||||
|         ) | ||||
|         self.win.blit(txt, (self.win.get_width() - txt.get_width(), 0)) | ||||
|   | ||||
							
								
								
									
										0
									
								
								src/objects/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/objects/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										66
									
								
								src/objects/road.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/objects/road.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.track_object import TrackObject, TrackObjectType | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class Road(TrackObject): | ||||
|     type = TrackObjectType.Road | ||||
|  | ||||
|     def __init__(self, pts: list[RoadPoint]) -> None: | ||||
|         super().__init__() | ||||
|         self.pts: list[RoadPoint] = pts | ||||
|  | ||||
|     @classmethod | ||||
|     def load(cls, data: dict) -> Road: | ||||
|         return Road([RoadPoint.load(pt) for pt in data["pts"]]) | ||||
|  | ||||
|     def render(self, surf: pygame.Surface, camera: Camera): | ||||
|         side1: list[Vec] = [] | ||||
|         side2: list[Vec] = [] | ||||
|  | ||||
|         for i, pt in enumerate(self.pts): | ||||
|             p1: Vec = pt.pos | ||||
|             p2: Vec = p1 + pt.normal * pt.width | ||||
|             p3: Vec = p1 - pt.normal * pt.width | ||||
|             side1.append(camera.world2screen(p2)) | ||||
|             side2.append(camera.world2screen(p3)) | ||||
|             col: tuple[float, float, float] = (i * 10 + 150, 100, 100) | ||||
|             pygame.draw.circle(surf, col, camera.world2screen(p1), 5) | ||||
|  | ||||
|         n: int = len(self.pts) | ||||
|         for i in range(n): | ||||
|             pygame.draw.polygon( | ||||
|                 surf, | ||||
|                 (100, 100, 100), | ||||
|                 [side1[i], side1[(i + 1) % n], side2[(i + 1) % n], side2[i]], | ||||
|             ) | ||||
|  | ||||
|         pygame.draw.lines(surf, (255, 255, 255), True, side1) | ||||
|         pygame.draw.lines(surf, (255, 255, 255), True, side2) | ||||
|  | ||||
|     def get_collision_polygons(self) -> list[list[Vec]]: | ||||
|         side1: list[Vec] = [] | ||||
|         side2: list[Vec] = [] | ||||
|         for pt in self.pts: | ||||
|             p1: Vec = pt.pos | ||||
|             p2: Vec = p1 + pt.normal * pt.width | ||||
|             p3: Vec = p1 - pt.normal * pt.width | ||||
|             side1.append(p2) | ||||
|             side2.append(p3) | ||||
|  | ||||
|         return [side1, side2] | ||||
|  | ||||
|  | ||||
| class RoadPoint: | ||||
|     def __init__(self, pos: Vec, normal: Vec, width: float) -> None: | ||||
|         self.pos: Vec = pos | ||||
|         self.normal: Vec = normal.normalized | ||||
|         self.width: float = width | ||||
|  | ||||
|     @staticmethod | ||||
|     def load(data: list[float]) -> RoadPoint: | ||||
|         return RoadPoint(Vec(data[0], data[1]), Vec(data[2], data[3]), data[4]) | ||||
							
								
								
									
										54
									
								
								src/track.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/track.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import json | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| from src.camera import Camera | ||||
| from src.track_object import TrackObject | ||||
| from src.utils import ROOT | ||||
| from src.vec import Vec | ||||
|  | ||||
| TrackObject.init() | ||||
|  | ||||
|  | ||||
| class Track: | ||||
|     TRACKS_DIRECTORY = ROOT / "assets" / "tracks" | ||||
|  | ||||
|     def __init__(self, id: str, name: str, start_pos: Vec, start_dir: Vec) -> None: | ||||
|         self.id: str = id | ||||
|         self.name: str = name | ||||
|         self.start_pos: Vec = start_pos | ||||
|         self.start_dir: Vec = start_dir | ||||
|         self.objects: list[TrackObject] = [] | ||||
|         self.load_objects() | ||||
|  | ||||
|     @staticmethod | ||||
|     def load(name: str) -> Track: | ||||
|         with open(Track.TRACKS_DIRECTORY / name / "meta.json", "r") as f: | ||||
|             meta: dict = json.load(f) | ||||
|  | ||||
|         return Track( | ||||
|             name, | ||||
|             meta["name"], | ||||
|             Vec(*meta["start"]["pos"]), | ||||
|             Vec(*meta["start"]["direction"]), | ||||
|         ) | ||||
|  | ||||
|     def load_objects(self): | ||||
|         with open(Track.TRACKS_DIRECTORY / self.id / "track.json", "r") as f: | ||||
|             data: list = json.load(f) | ||||
|  | ||||
|         self.objects = [] | ||||
|         for obj_data in data: | ||||
|             self.objects.append(TrackObject.load(obj_data)) | ||||
|  | ||||
|     def render(self, surf: pygame.Surface, camera: Camera): | ||||
|         for object in self.objects: | ||||
|             object.render(surf, camera) | ||||
|  | ||||
|     def get_collision_polygons(self) -> list[list[Vec]]: | ||||
|         polygons: list[list[Vec]] = [] | ||||
|         for obj in self.objects: | ||||
|             polygons.extend(obj.get_collision_polygons()) | ||||
|         return polygons | ||||
							
								
								
									
										46
									
								
								src/track_object.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/track_object.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import importlib | ||||
| import pkgutil | ||||
| from enum import StrEnum | ||||
| from typing import Optional, Self | ||||
|  | ||||
| import pygame | ||||
|  | ||||
| import src.objects | ||||
| from src.camera import Camera | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| class TrackObjectType(StrEnum): | ||||
|     Road = "road" | ||||
|  | ||||
|     Unknown = "unknown" | ||||
|  | ||||
|  | ||||
| class TrackObject: | ||||
|     REGISTRY = {} | ||||
|     type: TrackObjectType = TrackObjectType.Unknown | ||||
|  | ||||
|     @staticmethod | ||||
|     def init(): | ||||
|         package = src.objects | ||||
|         for _, modname, _ in pkgutil.walk_packages( | ||||
|             package.__path__, package.__name__ + "." | ||||
|         ): | ||||
|             importlib.import_module(modname) | ||||
|  | ||||
|     def __init_subclass__(cls, **kwargs) -> None: | ||||
|         super().__init_subclass__(**kwargs) | ||||
|         TrackObject.REGISTRY[cls.type] = cls | ||||
|  | ||||
|     @classmethod | ||||
|     def load(cls, data: dict) -> Self: | ||||
|         obj_type: Optional[TrackObjectType] = data.get("type") | ||||
|         if obj_type not in cls.REGISTRY: | ||||
|             raise ValueError(f"Unknown object tyoe: {obj_type}") | ||||
|         return cls.REGISTRY[obj_type].load(data) | ||||
|  | ||||
|     def render(self, surf: pygame.Surface, camera: Camera): | ||||
|         pass | ||||
|  | ||||
|     def get_collision_polygons(self) -> list[list[Vec]]: | ||||
|         return [] | ||||
							
								
								
									
										34
									
								
								src/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import os | ||||
| from pathlib import Path | ||||
|  | ||||
| from src.vec import Vec | ||||
|  | ||||
|  | ||||
| ROOT = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))) | ||||
|  | ||||
|  | ||||
| def orientation(a: Vec, b: Vec, c: Vec) -> float: | ||||
|     return (b - a).cross(c - a) | ||||
|  | ||||
|  | ||||
| def segments_intersect(a1: Vec, a2: Vec, b1: Vec, b2: Vec) -> bool: | ||||
|     o1 = orientation(a1, a2, b1) | ||||
|     o2 = orientation(a1, a2, b2) | ||||
|     o3 = orientation(b1, b2, a1) | ||||
|     o4 = orientation(b1, b2, a2) | ||||
|  | ||||
|     # General case: segments straddle each other | ||||
|     if (o1 * o2 < 0) and (o3 * o4 < 0): | ||||
|         return True | ||||
|  | ||||
|     # Special cases: Collinear overlaps | ||||
|     if o1 == 0 and b1.within(a1, a2): | ||||
|         return True | ||||
|     if o2 == 0 and b2.within(a1, a2): | ||||
|         return True | ||||
|     if o3 == 0 and a1.within(b1, b2): | ||||
|         return True | ||||
|     if o4 == 0 and a2.within(b1, b2): | ||||
|         return True | ||||
|  | ||||
|     return False | ||||
							
								
								
									
										13
									
								
								src/vec.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/vec.py
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from math import sqrt | ||||
| from math import cos, sin, sqrt | ||||
|  | ||||
|  | ||||
| class Vec: | ||||
| @@ -55,3 +55,14 @@ class Vec: | ||||
|  | ||||
|     def __repr__(self) -> str: | ||||
|         return f"Vec({self.x}, {self.y})" | ||||
|  | ||||
|     def rotate(self, angle: float) -> Vec: | ||||
|         return Vec( | ||||
|             cos(angle) * self.x - sin(angle) * self.y, | ||||
|             sin(angle) * self.x + cos(angle) * self.y, | ||||
|         ) | ||||
|  | ||||
|     def within(self, p1: Vec, p2: Vec) -> bool: | ||||
|         x1, x2 = min(p1.x, p2.x), max(p1.x, p2.x) | ||||
|         y1, y2 = min(p1.y, p2.y), max(p1.y, p2.y) | ||||
|         return (x1 <= self.x <= x2) and (y1 <= self.y <= y2) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user