Загрузка данных
import math
import random
import sys
import pygame
# =============================================================================
# НАСТРОЙКИ ЭКРАНА И КОНСТАНТЫ ПСЕВДО-3D
# =============================================================================
SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768
HALF_WIDTH = SCREEN_WIDTH // 2
HALF_HEIGHT = SCREEN_HEIGHT // 2
FPS = 60
ROAD_WIDTH = 2000 # Полная ширина половины дороги (от центра до края)
SEGMENT_LENGTH = 200 # Длина одного сегмента полотна
CAMERA_HEIGHT = 1000 # Высота камеры над полотном
DRAW_DISTANCE = 240 # Дальность прорисовки (в сегментах)
FOV = 60 # Поле зрения
CAMERA_DEPTH = 1.0 / math.tan((FOV / 2.0) * math.pi / 180.0)
# Цветовая палитра трассы
COLOR_SKY = (100, 145, 230)
COLOR_MOUNTAINS_FAR = (70, 90, 150)
COLOR_MOUNTAINS_NEAR = (50, 70, 120)
ROAD_LIGHT = {
'road': (105, 105, 105),
'grass': (16, 160, 16),
'rumble': (240, 240, 240),
'lane': (255, 255, 255)
}
ROAD_DARK = {
'road': (97, 97, 97),
'grass': (0, 135, 0),
'rumble': (210, 20, 20),
'lane': (97, 97, 97)
}
# =============================================================================
# ГЕНЕРАТОР ЗВУКОВ (8-bit Retro Synth)
# =============================================================================
class SoundEngine:
"""Генерация звуковых эффектов без внешних wav-файлов"""
def __init__(self):
self.enabled = False
try:
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)
self.enabled = True
self.engine_channel = pygame.mixer.Channel(0)
self.crash_sound = self._create_noise_sound(0.4)
self.checkpoint_sound = self._create_tone_sound(660, 0.25)
except Exception:
self.enabled = False
def _create_tone_sound(self, freq, duration):
sample_rate = 22050
n_samples = int(sample_rate * duration)
buf = bytearray()
for i in range(n_samples):
t = float(i) / sample_rate
val = int(32767.0 * 0.3 * math.sin(2.0 * math.pi * freq * t))
buf += val.to_bytes(2, byteorder='little', signed=True)
buf += val.to_bytes(2, byteorder='little', signed=True)
return pygame.mixer.Sound(buffer=bytes(buf))
def _create_noise_sound(self, duration):
sample_rate = 22050
n_samples = int(sample_rate * duration)
buf = bytearray()
for _ in range(n_samples):
val = random.randint(-12000, 12000)
buf += val.to_bytes(2, byteorder='little', signed=True)
buf += val.to_bytes(2, byteorder='little', signed=True)
return pygame.mixer.Sound(buffer=bytes(buf))
def play_crash(self):
if self.enabled:
self.crash_sound.play()
def play_checkpoint(self):
if self.enabled:
self.checkpoint_sound.play()
# =============================================================================
# МАТЕМАТИКА И ПРОЕКЦИЯ СЕГМЕНТОВ
# =============================================================================
class Segment:
def __init__(self, index):
self.index = index
self.p1 = {'world': [0, 0, 0], 'screen': [0, 0, 0], 'scale': 0}
self.p2 = {'world': [0, 0, 0], 'screen': [0, 0, 0], 'scale': 0}
self.curve = 0.0
self.clip = SCREEN_HEIGHT
self.sprite = None # 'tree', 'billboard', 'sign'
self.sprite_x = 0.0 # смещение от полотна (-2.0 .. +2.0)
self.cars = []
self.color = ROAD_DARK if (index // 3) % 2 else ROAD_LIGHT
def project(self, p, camera_x, camera_y, camera_z):
world_x, world_y, world_z = p['world']
# Смещение относительно камеры
cam_x = world_x - camera_x
cam_y = world_y - camera_y
cam_z = world_z - camera_z
if cam_z <= 0.001:
cam_z = 0.001
scale = CAMERA_DEPTH / cam_z
p['scale'] = scale
# Перспективное проецирование на экран
screen_x = int(HALF_WIDTH + (scale * cam_x * HALF_WIDTH))
screen_y = int(HALF_HEIGHT - (scale * cam_y * HALF_HEIGHT))
screen_w = int(scale * ROAD_WIDTH * HALF_WIDTH)
p['screen'] = [screen_x, screen_y, screen_w]
# =============================================================================
# ИИ И ТРАНСПОРТ ТРАФИКА
# =============================================================================
class TrafficCar:
def __init__(self, z, offset, speed, color):
self.z = z
self.offset = offset # -0.7 .. +0.7
self.speed = speed
self.percent = 0.0
self.color = color
self.width = 650 # Реалистичная ширина в игровых единицах
self.height = 360
def update(self, dt, track_length):
self.z = (self.z + self.speed * dt) % track_length
self.percent = (self.z % SEGMENT_LENGTH) / SEGMENT_LENGTH
# =============================================================================
# ГЛАВНЫЙ ИГРОВОЙ ДВИЖОК
# =============================================================================
class RetroHighwayRacer:
def __init__(self):
pygame.init()
pygame.font.init()
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Outrun 3D: Fixed Engine")
self.clock = pygame.time.Clock()
self.sound = SoundEngine()
self.font_speed = pygame.font.SysFont("impact", 44)
self.font_hud = pygame.font.SysFont("monospace", 22, bold=True)
self.font_msg = pygame.font.SysFont("impact", 64)
# Трасса
self.segments = []
self.track_length = 0
self.build_track()
# Игрок
self.player_x = 0.0 # -1.0 (левый край дороги) .. 1.0 (правый)
self.player_z = 0.0 # Дистанция по трассе
self.speed = 0.0
self.max_speed = 12500.0 # Макс скорость в единицах Z в секунду
self.accel = self.max_speed / 4.0
self.breaking = -self.max_speed * 1.5
self.decel = -self.max_speed / 5.0
self.offroad_decel = -self.max_speed / 1.2
self.centrifugal = 3.2
# Состояние игры
self.lap_time = 0.0
self.best_lap = 0.0
self.time_left = 35.0
self.game_over = False
self.message = "GO!"
self.message_timer = 2.0
# Трафик и окружение
self.traffic = []
self.spawn_traffic(40)
self.sparks = []
self.sky_offset = 0.0
def build_track(self):
self.segments.clear()
def add_road(enter, hold, leave, curve, elevation):
start_y = 0 if len(self.segments) == 0 else self.segments[-1].p2['world'][1]
total = enter + hold + leave
for i in range(total):
idx = len(self.segments)
seg = Segment(idx)
seg.p1['world'] = [0, start_y if i == 0 else self.segments[-1].p2['world'][1], idx * SEGMENT_LENGTH]
# Плавная интерполяция высоты (косинусный холм)
if i < enter:
c = curve * (i / enter)
t = i / enter
y = start_y + (elevation - start_y) * (1.0 - math.cos(math.pi * t)) / 2.0
elif i < enter + hold:
c = curve
y = elevation
else:
c = curve * (1.0 - (i - enter - hold) / leave)
t = (i - enter - hold) / leave
y = elevation + (0 - elevation) * (1.0 - math.cos(math.pi * t)) / 2.0
seg.curve = c
seg.p2['world'] = [0, y, (idx + 1) * SEGMENT_LENGTH]
# Расстановка декораций по обочинам
if idx % 12 == 0 and random.random() < 0.8:
seg.sprite = 'tree'
seg.sprite_x = -1.7 if random.random() < 0.5 else 1.7
elif idx % 25 == 0:
seg.sprite = 'sign'
seg.sprite_x = -1.5 if curve < 0 else 1.5
self.segments.append(seg)
# Конфигурация гоночного трека
add_road(50, 60, 50, 0, 0)
add_road(60, 80, 60, 2.5, 1200) # Плавный правый поворот на холм
add_road(60, 80, 60, -2.0, -1000) # Спуск налево
add_road(80, 100, 80, 3.5, 2000) # Крутой холм
add_road(60, 60, 60, -3.0, 0) # Извилистый участок
add_road(40, 40, 40, 0, -800) # Низина
add_road(50, 70, 50, 0, 0) # Финишная прямая
self.track_length = len(self.segments) * SEGMENT_LENGTH
def spawn_traffic(self, count):
colors = [
(220, 20, 20), (30, 144, 255), (255, 215, 0),
(255, 255, 255), (50, 205, 50), (138, 43, 226)
]
lanes = [-0.6, -0.2, 0.2, 0.6]
for _ in range(count):
z = random.uniform(1500, self.track_length - 1000)
lane = random.choice(lanes)
spd = random.uniform(3000, 6500)
col = random.choice(colors)
self.traffic.append(TrafficCar(z, lane, spd, col))
def find_segment(self, z):
return self.segments[int(z / SEGMENT_LENGTH) % len(self.segments)]
def handle_input(self, dt):
if self.game_over:
return
keys = pygame.key.get_pressed()
speed_ratio = self.speed / self.max_speed
# Газ / Тормоз
if keys[pygame.K_UP] or keys[pygame.K_w]:
self.speed += self.accel * dt
elif keys[pygame.K_DOWN] or keys[pygame.K_s]:
self.speed += self.breaking * dt
else:
self.speed += self.decel * dt
# Руление (зависит от текущей скорости)
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.player_x -= 3.2 * speed_ratio * dt
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.player_x += 3.2 * speed_ratio * dt
# Сход на обочину (сильное трение и тряска)
if (self.player_x < -1.0 or self.player_x > 1.0) and self.speed > 500:
self.speed += self.offroad_decel * dt
if random.random() < 0.4:
self.sparks.append([
HALF_WIDTH + random.randint(-40, 40),
SCREEN_HEIGHT - 35,
random.uniform(-1, 1), -1.2,
random.randint(10, 20), (140, 140, 140)
])
self.speed = max(0.0, min(self.speed, self.max_speed))
def update(self, dt):
if self.game_over:
return
self.handle_input(dt)
old_z = self.player_z
self.player_z = (self.player_z + self.speed * dt) % self.track_length
# Пересечение круга / Чекпоинт
if self.player_z < old_z:
if self.best_lap == 0 or self.lap_time < self.best_lap:
self.best_lap = self.lap_time
self.lap_time = 0.0
self.time_left += 25.0
self.message = "CHECKPOINT! +25s"
self.message_timer = 2.5
self.sound.play_checkpoint()
self.lap_time += dt
self.time_left -= dt
if self.message_timer > 0:
self.message_timer -= dt
if self.time_left <= 0:
self.game_over = True
self.speed = 0
# Снос центробежной силой на поворотах
current_segment = self.find_segment(self.player_z)
self.player_x -= (current_segment.curve * (self.speed / self.max_speed) * self.centrifugal) * dt
self.sky_offset -= current_segment.curve * (self.speed / self.max_speed) * 1.5
# Обновление машин трафика
for car in self.traffic:
car.update(dt, self.track_length)
# Проверка столкновения
if abs(car.z - self.player_z) < 140 and abs(self.player_x - car.offset) < 0.35:
self.speed = min(self.speed * 0.3, 1500)
self.sound.play_crash()
for _ in range(16):
self.sparks.append([
HALF_WIDTH, SCREEN_HEIGHT - 60,
random.uniform(-4, 4), random.uniform(-4, -1),
random.randint(15, 30), (255, random.randint(100, 240), 0)
])
# Обновление частиц
for spark in self.sparks[:]:
spark[0] += spark[2]
spark[1] += spark[3]
spark[4] -= 1
if spark[4] <= 0:
self.sparks.remove(spark)
# =========================================================================
# ОТРИСОВКА МИРА
# =========================================================================
def draw_background(self):
# Небо
pygame.draw.rect(self.screen, COLOR_SKY, (0, 0, SCREEN_WIDTH, HALF_HEIGHT))
# Дальние горы (параллакс эффект)
mountain_offset = int(self.sky_offset) % SCREEN_WIDTH
for x_base in [-SCREEN_WIDTH, 0, SCREEN_WIDTH]:
points_far = [
(x_base + mountain_offset, HALF_HEIGHT),
(x_base + mountain_offset + 180, HALF_HEIGHT - 120),
(x_base + mountain_offset + 380, HALF_HEIGHT - 60),
(x_base + mountain_offset + 600, HALF_HEIGHT - 160),
(x_base + mountain_offset + 840, HALF_HEIGHT - 80),
(x_base + mountain_offset + 1024, HALF_HEIGHT)
]
pygame.draw.polygon(self.screen, COLOR_MOUNTAINS_FAR, points_far)
def draw_segment(self, seg):
p1 = seg.p1['screen']
p2 = seg.p2['screen']
# Трава слева и справа от полотна (честные полигоны без артефактов)
r1 = p1[2] // 6
r2 = p2[2] // 6
# Левая обочина и трава
pygame.draw.polygon(self.screen, seg.color['grass'], [
(0, p1[1]), (p1[0] - p1[2] - r1, p1[1]),
(p2[0] - p2[2] - r2, p2[1]), (0, p2[1])
])
# Правая обочина и трава
pygame.draw.polygon(self.screen, seg.color['grass'], [
(p1[0] + p1[2] + r1, p1[1]), (SCREEN_WIDTH, p1[1]),
(SCREEN_WIDTH, p2[1]), (p2[0] + p2[2] + r2, p2[1])
])
# Бордюры (Rumble strips)
pygame.draw.polygon(self.screen, seg.color['rumble'], [
(p1[0] - p1[2] - r1, p1[1]), (p1[0] - p1[2], p1[1]),
(p2[0] - p2[2], p2[1]), (p2[0] - p2[2] - r2, p2[1])
])
pygame.draw.polygon(self.screen, seg.color['rumble'], [
(p1[0] + p1[2], p1[1]), (p1[0] + p1[2] + r1, p1[1]),
(p2[0] + p2[2] + r2, p2[1]), (p2[0] + p2[2], p2[1])
])
# Асфальт
pygame.draw.polygon(self.screen, seg.color['road'], [
(p1[0] - p1[2], p1[1]), (p1[0] + p1[2], p1[1]),
(p2[0] + p2[2], p2[1]), (p2[0] - p2[2], p2[1])
])
# Разделительные полосы
if seg.color == ROAD_LIGHT:
lane_w1 = max(1, p1[2] // 32)
lane_w2 = max(1, p2[2] // 32)
# Центральная линия
pygame.draw.polygon(self.screen, seg.color['lane'], [
(p1[0] - lane_w1, p1[1]), (p1[0] + lane_w1, p1[1]),
(p2[0] + lane_w2, p2[1]), (p2[0] - lane_w2, p2[1])
])
def draw_tree(self, sx, sy, scale, clip_y):
w = int(600 * scale * HALF_WIDTH)
h = int(1000 * scale * HALF_WIDTH)
if h <= 2 or sy - h >= clip_y:
return
# Ствол
trunk_w = max(2, w // 6)
pygame.draw.rect(self.screen, (100, 65, 30), (sx - trunk_w // 2, sy - h // 3, trunk_w, h // 3))
# Крона (3 яруса треугольников)
for i in range(3):
layer_y = sy - h // 3 - (i * h // 4)
layer_w = w - (i * w // 4)
pygame.draw.polygon(self.screen, (20, 120 + i * 20, 30), [
(sx - layer_w // 2, layer_y),
(sx, layer_y - h // 3),
(sx + layer_w // 2, layer_y)
])
def draw_traffic_car(self, car, seg, clip_y):
scale = seg.p1['scale']
p1 = seg.p1['screen']
# Корректный расчет координат на полотне
sx = int(p1[0] + (scale * car.offset * ROAD_WIDTH * HALF_WIDTH))
sy = p1[1]
w = int(car.width * scale * HALF_WIDTH)
h = int(car.height * scale * HALF_WIDTH)
if w <= 3 or sy - h >= clip_y:
return
# Тень
shadow_rect = pygame.Rect(sx - w // 2, sy - h // 6, w, h // 4)
pygame.draw.ellipse(self.screen, (20, 20, 20), shadow_rect)
# Кузов
body_rect = (sx - w // 2, sy - h, w, int(h * 0.75))
pygame.draw.rect(self.screen, car.color, body_rect, border_radius=max(1, w // 8))
# Крыша/Стекло
roof_w = int(w * 0.7)
roof_h = int(h * 0.4)
pygame.draw.rect(self.screen, (40, 40, 50), (sx - roof_w // 2, sy - h + max(1, h // 12), roof_w, roof_h))
# Задние фонари
light_w = max(2, w // 6)
light_h = max(2, h // 6)
pygame.draw.rect(self.screen, (255, 30, 30), (sx - w // 2 + 2, sy - int(h * 0.4), light_w, light_h))
pygame.draw.rect(self.screen, (255, 30, 30), (sx + w // 2 - light_w - 2, sy - int(h * 0.4), light_w, light_h))
def draw_player(self):
x = HALF_WIDTH
y = SCREEN_HEIGHT - 20
w = 170
h = 85
# Наклон при поворотах
tilt = 0
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
tilt = -8
elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
tilt = 8
# Тень
pygame.draw.ellipse(self.screen, (15, 15, 15), (x - w // 2, y - 10, w, 25))
# Корпус спорткара
points = [
(x - w // 2 + tilt, y - 25),
(x - w // 3, y - h),
(x + w // 3, y - h),
(x + w // 2 + tilt, y - 25)
]
pygame.draw.polygon(self.screen, (220, 20, 40), points)
# Кабина
cabin_points = [
(x - w // 4, y - h + 5),
(x - w // 6, y - h - 22),
(x + w // 6, y - h - 22),
(x + w // 4, y - h + 5)
]
pygame.draw.polygon(self.screen, (30, 30, 40), cabin_points)
# Задние стоп-сигналы и антикрыло
pygame.draw.rect(self.screen, (40, 40, 40), (x - w // 2 + tilt, y - 30, w, 15))
brake_glow = (255, 30, 30) if (keys[pygame.K_DOWN] or keys[pygame.K_s]) else (180, 0, 0)
pygame.draw.rect(self.screen, brake_glow, (x - w // 2 + tilt + 8, y - 28, 28, 10))
pygame.draw.rect(self.screen, brake_glow, (x + w // 2 + tilt - 36, y - 28, 28, 10))
def render(self):
self.draw_background()
base_segment = self.find_segment(self.player_z)
base_percent = (self.player_z % SEGMENT_LENGTH) / SEGMENT_LENGTH
# Расчет вертикального положения камеры относительно холмов
player_y = base_segment.p1['world'][1] + (base_segment.p2['world'][1] - base_segment.p1['world'][1]) * base_percent
camera_y = CAMERA_HEIGHT + player_y
# 1. ПЕРВЫЙ ПРОХОД: Дорога (От переднего плана к горизонту с Z-отсечением)
max_y = SCREEN_HEIGHT
x = 0
dx = -(base_segment.curve * base_percent)
for n in range(DRAW_DISTANCE):
seg = self.segments[(base_segment.index + n) % len(self.segments)]
looped = seg.index < base_segment.index
camera_z = self.player_z - (self.track_length if looped else 0)
# Проекция геометрии
seg.project(seg.p1, (self.player_x * ROAD_WIDTH) - x, camera_y, camera_z)
seg.project(seg.p2, (self.player_x * ROAD_WIDTH) - x - dx, camera_y, camera_z)
x += dx
dx += seg.curve
p1 = seg.p1['screen']
p2 = seg.p2['screen']
# Отсекаем сегменты за камерой и за холмами
if seg.p1['camera_z'] if 'camera_z' in seg.p1 else False:
continue
if p2[1] >= max_y or p2[1] >= p1[1]:
seg.clip = max_y
continue
self.draw_segment(seg)
max_y = p2[1]
seg.clip = max_y
# 2. ВТОРОЙ ПРОХОД: Спрайты и трафик (От горизонта к игроку для идеального наложения)
for n in range(DRAW_DISTANCE - 1, -1, -1):
seg = self.segments[(base_segment.index + n) % len(self.segments)]
# Декорации на обочине
if seg.sprite == 'tree':
sx = int(seg.p1['screen'][0] + (seg.p1['scale'] * seg.sprite_x * ROAD_WIDTH * HALF_WIDTH))
self.draw_tree(sx, seg.p1['screen'][1], seg.p1['scale'], seg.clip)
# Машины трафика
for car in self.traffic:
if self.find_segment(car.z).index == seg.index:
self.draw_traffic_car(car, seg, seg.clip)
# Частицы и искры
for sp in self.sparks:
pygame.draw.circle(self.screen, sp[5], (int(sp[0]), int(sp[1])), random.randint(2, 5))
self.draw_player()
# HUD
speed_kmh = int((self.speed / self.max_speed) * 310)
hud_spd = self.font_speed.render(f"{speed_kmh:3d} KM/H", True, (255, 230, 0))
hud_time = self.font_hud.render(f"TIME: {max(0.0, self.time_left):04.1f}s", True, (255, 255, 255))
hud_lap = self.font_hud.render(f"LAP: {self.lap_time:04.1f}s", True, (220, 220, 220))
hud_best = self.font_hud.render(f"BEST: {self.best_lap:04.1f}s" if self.best_lap > 0 else "BEST: --.-s", True, (180, 180, 180))
self.screen.blit(hud_spd, (30, SCREEN_HEIGHT - 70))
self.screen.blit(hud_time, (30, 25))
self.screen.blit(hud_lap, (30, 60))
self.screen.blit(hud_best, (30, 95))
# Сообщения на экране (Checkpoint / Game Over)
if self.message_timer > 0:
msg_surf = self.font_msg.render(self.message, True, (255, 255, 255))
self.screen.blit(msg_surf, (HALF_WIDTH - msg_surf.get_width() // 2, 200))
if self.game_over:
over_surf = self.font_msg.render("TIME OVER", True, (255, 40, 40))
sub_surf = self.font_hud.render("PRESS 'R' TO RESTART", True, (255, 255, 255))
self.screen.blit(over_surf, (HALF_WIDTH - over_surf.get_width() // 2, HALF_HEIGHT - 40))
self.screen.blit(sub_surf, (HALF_WIDTH - sub_surf.get_width() // 2, HALF_HEIGHT + 35))
pygame.display.flip()
def reset_game(self):
self.player_x = 0.0
self.player_z = 0.0
self.speed = 0.0
self.time_left = 35.0
self.lap_time = 0.0
self.game_over = False
self.message = "GO!"
self.message_timer = 2.0
self.sparks.clear()
def run(self):
while True:
dt = self.clock.tick(FPS) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and self.game_over:
self.reset_game()
self.update(dt)
self.render()
if __name__ == "__main__":
game = RetroHighwayRacer()
game.run()