目录
[1. 技术栈与环境搭建](#1. 技术栈与环境搭建)
[2. 需求分析与功能规划](#2. 需求分析与功能规划)
[1. 基础框架搭建(Pygame 初始化)](#1. 基础框架搭建(Pygame 初始化))
[2. 地形系统(游戏核心场景)](#2. 地形系统(游戏核心场景))
[3. 车辆系统(玩家核心交互)](#3. 车辆系统(玩家核心交互))
[4. 交互 UI 与状态管理](#4. 交互 UI 与状态管理)
[1. 数据持久化(存档系统)](#1. 数据持久化(存档系统))
[2. 辅助系统开发](#2. 辅助系统开发)
[3. 高级功能(可选增强)](#3. 高级功能(可选增强))
[1. BUG 修复](#1. BUG 修复)
[2. 性能优化](#2. 性能优化)
[3. 用户体验优化](#3. 用户体验优化)
一、引言
登山赛车游戏基于 Pygame 开发,是一个包含物理模拟、关卡系统、养成系统、交互 UI的完整休闲游戏,本文将详细介绍该登山游戏的开发过程和Python代码完整实现。
二、开发准备阶段
1. 技术栈与环境搭建
- 核心技术:Python(逻辑开发) + Pygame(图形渲染、输入处理、音频管理);
- 环境配置 :安装 Python 3.x,通过
pip install pygame安装 Pygame 库; - 项目结构规划 :虽最终代码为单文件,但实际开发中通常按模块拆分(如
terrain.py、car.py、ui.py),此处为简化整合为单文件。
2. 需求分析与功能规划
明确游戏核心玩法与扩展功能:
- 核心玩法:车辆在随机地形上行驶,通过控制方向 / 加速保持行驶,消耗燃料并维持耐久;
- 扩展功能:关卡系统、车辆升级、皮肤自定义、成就系统、道具收集、BOSS 挑战、数据存档、回放系统等。
三、核心模块开发(按功能优先级)
1. 基础框架搭建(Pygame 初始化)
先实现游戏的基础运行框架:
- 初始化 Pygame、设置窗口(支持自适应缩放)、创建主循环(处理事件、更新逻辑、渲染画面);
- 核心代码:
HillClimbGame类的__init__、run方法,完成窗口创建、时钟控制、状态机(菜单 / 游戏中 / UI 界面)切换。
2. 地形系统(游戏核心场景)
登山赛车的核心是随机地形生成与碰撞检测 ,对应Terrain类的开发:
- 地形生成:通过 "关键点插值" 生成随机起伏地形,结合权重随机分配地形类型(草地 / 岩石 / 水域);
- 性能优化 :引入高度缓存(
height_cache),避免重复计算地形高度,提升运行效率; - 地形交互 :实现
get_height(获取指定 x 坐标的地形高度)、get_slope(获取坡度),为车辆物理提供基础数据。
3. 车辆系统(玩家核心交互)
Car类是游戏的核心逻辑载体,实现物理模拟与车辆状态管理:
- 基础属性:定义不同车型(跑车 / 越野车 / 卡车)的基础参数(加速度、最大速度、燃料上限);
- 物理模拟:结合重力、摩擦力、地形坡度实现车辆运动逻辑(加速 / 转向 / 碰撞地形);
- 状态管理:燃料消耗、耐久度变化、道具效果应用(如加速、修复)。
4. 交互 UI 与状态管理
实现用户与游戏的交互入口,包括:
- 主菜单:通过 Pygame 绘制按钮(开始游戏 / 车辆改装 / 退出),处理鼠标点击事件;
- 子界面 :关卡选择、车辆升级、皮肤自定义、成就系统等 UI 的绘制与交互逻辑(如
LevelSelector、CarUpgradeSystem类); - 状态机 :通过
self.state(menu/playing/upgrade 等)管理不同界面的切换。
四、功能扩展阶段
1. 数据持久化(存档系统)
通过SaveSystem类实现游戏数据的存储与加载:
- 存储内容:最高分、车辆升级进度、解锁的关卡 / 车辆 / 成就、车辆皮肤、回放数据;
- 存储方式 :使用 JSON 格式存储到本地文件(
save_data.json),处理数据的序列化 / 反序列化(如颜色元组与列表的转换)。
2. 辅助系统开发
为游戏增加趣味性与深度:
- 道具系统 (
PropSystem):随机生成道具(燃料 / 加速 / 修复),处理收集与效果应用; - 天气系统 (
WeatherSystem):实现不同天气(晴天 / 雨天 / 雪天)的视觉特效与物理修正(如摩擦力降低); - 粒子系统 (
ParticleSystem):添加爆炸、烟雾等特效,提升视觉表现力; - BOSS 系统 (
BossSystem):在高难度关卡中生成 BOSS,实现 BOSS 的移动、射击、受击逻辑。
3. 高级功能(可选增强)
- 回放系统:记录游戏过程中的车辆状态,支持后续回放;
- 网络联机(基础实现):通过 Socket 实现简单的多玩家联机(服务器 / 客户端模式)。
五、测试与优化阶段
1. BUG 修复
解决开发中的逻辑问题:
- 燃料 / 耐久度溢出问题(限制数值在 0~ 上限之间);
- 道具重复收集问题(通过唯一 ID 标记已收集道具);
- 中文显示问题(适配系统中文字体)。
2. 性能优化
- 地形高度缓存:减少重复计算地形高度的性能开销;
- 粒子批量处理:限制粒子数量,批量更新 / 绘制粒子;
- 界面缩放适配:支持窗口自适应缩放,保证不同分辨率下 UI 显示正常。
3. 用户体验优化
- 首次进入提示(引导用户了解功能);
- 界面交互反馈(按钮 hover 效果、操作提示文字)。
六、登山赛车的Python代码完整实现
python
import pygame
import sys
import random
import math
import json
import os
import socket
import threading
import pickle
from datetime import datetime
from typing import Tuple, List, Dict, Optional
# ===================== 全局配置扩展 =====================
WEATHER_TYPES = ["sunny", "rainy", "snowy"]
UPGRADE_COSTS = {
"acceleration": 100, "durability": 150, "fuel_capacity": 120,
"grip": 110, "max_speed": 130
}
UPGRADE_MAX_LEVEL = 10
ACHIEVEMENTS = {
"score_500": {"name": "小试牛刀", "desc": "单局达到500分", "unlocked": False},
"score_1000": {"name": "登山高手", "desc": "单局达到1000分", "unlocked": False},
"unlock_all": {"name": "收藏大师", "desc": "解锁所有车辆", "unlocked": False},
"use_10_props": {"name": "道具达人", "desc": "单局使用10个道具", "unlocked": False},
"level_5": {"name": "关卡突破", "desc": "通关第5关", "unlocked": False},
"boss_defeat": {"name": "击败BOSS", "desc": "击败最终BOSS", "unlocked": False},
"level_8": {"name": "终极挑战", "desc": "通关第8关", "unlocked": False} # 新增成就
}
SKIN_COLORS = [
(255, 0, 0), (0, 0, 255), (0, 255, 0), (255, 255, 0),
(128, 0, 128), (255, 165, 0), (0, 255, 255), (128, 128, 128)
]
REPLAY_FPS = 60
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 12345
BUILTIN_LEVELS = [
{
"id": 1,
"name": "新手山丘",
"difficulty": "简单",
"terrain_params": {
"起伏范围": (-80, 80), # 坡度
"地形权重": {"grass": 0.8, "rock": 0.1, "ramp": 0.1},
"长度": 8000,
"起始y比例": 0.7
},
"prop_density": 0.8,
"prop_weights": {"fuel": 0.5, "boost": 0.2, "repair": 0.3},
"has_boss": False,
"boss_spawn_x": 0,
"unlock_condition": None
},
{
"id": 2,
"name": "森林小径",
"difficulty": "简单",
"terrain_params": {
"起伏范围": (-100, 100),
"地形权重": {"grass": 0.7, "rock": 0.15, "ramp": 0.15},
"长度": 9000,
"起始y比例": 0.7
},
"prop_density": 0.7,
"prop_weights": {"fuel": 0.4, "boost": 0.25, "repair": 0.2, "shield": 0.15},
"has_boss": False,
"boss_spawn_x": 0,
"unlock_condition": {"required_level": 1}
},
{
"id": 3,
"name": "岩石峡谷",
"difficulty": "中等",
"terrain_params": {
"起伏范围": (-150, 150),
"地形权重": {"grass": 0.5, "rock": 0.3, "water": 0.1, "ramp": 0.1},
"长度": 10000,
"起始y比例": 0.65
},
"prop_density": 0.6,
"prop_weights": {"fuel": 0.35, "boost": 0.2, "nitro": 0.15, "repair": 0.2, "shield": 0.1},
"has_boss": False,
"boss_spawn_x": 0,
"unlock_condition": {"required_level": 2}
},
{
"id": 4,
"name": "沙漠陡坡",
"difficulty": "中等",
"terrain_params": {
"起伏范围": (-200, 200),
"地形权重": {"grass": 0.4, "rock": 0.3, "water": 0.15, "ramp": 0.15},
"长度": 11000,
"起始y比例": 0.65
},
"prop_density": 0.5,
"prop_weights": {"fuel": 0.25, "boost": 0.15, "nitro": 0.15, "repair": 0.15, "shield": 0.1, "weapon": 0.2},
"has_boss": True,
"boss_spawn_x": 5000,
"unlock_condition": {"required_level": 3}
},
{
"id": 5,
"name": "雪山之巅",
"difficulty": "困难",
"terrain_params": {
"起伏范围": (-250, 250),
"地形权重": {"grass": 0.3, "rock": 0.4, "water": 0.2, "ramp": 0.1},
"长度": 12000,
"起始y比例": 0.6
},
"prop_density": 0.4,
"prop_weights": {"fuel": 0.2, "boost": 0.15, "nitro": 0.2, "repair": 0.15, "shield": 0.1, "weapon": 0.2},
"has_boss": True,
"boss_spawn_x": 6000,
"unlock_condition": {"required_level": 4}
},
# ========== 地图 ==========
{
"id": 6,
"name": "火山熔岩路",
"difficulty": "极难",
"terrain_params": {
"起伏范围": (-300, 300),
"地形权重": {"grass": 0.2, "rock": 0.5, "water": 0.1, "ramp": 0.2},
"长度": 13000,
"起始y比例": 0.55
},
"prop_density": 0.35,
"prop_weights": {"fuel": 0.15, "boost": 0.1, "nitro": 0.25, "repair": 0.2, "shield": 0.15, "weapon": 0.15},
"has_boss": True,
"boss_spawn_x": 7000,
"unlock_condition": {"required_level": 5}
},
{
"id": 7,
"name": "太空边缘",
"difficulty": "地狱级",
"terrain_params": {
"起伏范围": (-400, 400),
"地形权重": {"grass": 0.1, "rock": 0.6, "water": 0.05, "ramp": 0.25},
"长度": 15000,
"起始y比例": 0.5
},
"prop_density": 0.3,
"prop_weights": {"fuel": 0.1, "boost": 0.1, "nitro": 0.3, "repair": 0.2, "shield": 0.15, "weapon": 0.15},
"has_boss": True,
"boss_spawn_x": 8000,
"unlock_condition": {"required_level": 6}
},
{
"id": 8,
"name": "末日废土",
"difficulty": "终极",
"terrain_params": {
"起伏范围": (-500, 500),
"地形权重": {"grass": 0.05, "rock": 0.7, "water": 0.05, "ramp": 0.2},
"长度": 18000,
"起始y比例": 0.45
},
"prop_density": 0.25,
"prop_weights": {"fuel": 0.05, "boost": 0.05, "nitro": 0.4, "repair": 0.2, "shield": 0.15, "weapon": 0.15},
"has_boss": True,
"boss_spawn_x": 9000,
"unlock_condition": {"required_level": 7}
}
]
INIT_WIDTH, INIT_HEIGHT = 800, 600
FPS = 60
GRAVITY = 0.15
MAX_DAMAGE = 100
FUEL_PACK_VALUE = 30
BOOST_DURATION = 60
NITRO_DURATION = 90
BOOST_MULTIPLIER = 2.0
NITRO_MULTIPLIER = 2.5
SHIELD_DURATION = 120
REPAIR_VALUE = 40
MAX_PARTICLES = 100
HEIGHT_CACHE_SIZE = 200
PARTICLE_BATCH_SIZE = 30
FRICTION_MAP = {
"grass": {"friction": 0.98, "grip": 0.95},
"water": {"friction": 0.90, "grip": 0.7},
"rock": {"friction": 0.95, "grip": 0.85},
"ramp": {"friction": 1.02, "grip": 0.98}
}
CAR_UNLOCK_CONDITIONS = {
"sports": {"unlocked": True, "score": 0},
"offroad": {"unlocked": True, "score": 0},
"truck": {"unlocked": False, "score": 500},
"motorcycle": {"unlocked": False, "score": 1000}
}
# 颜色定义
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GREEN = (34, 139, 34)
BROWN = (139, 69, 19)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
GRAY = (128, 128, 128)
CYAN = (0, 255, 255)
PURPLE = (128, 0, 128)
DARKRED = (150, 0, 0)
LIGHT_GRAY = (200, 200, 200)
DARK_GRAY = (80, 80, 80)
DARK_BLUE = (0, 0, 50)
ORANGE = (255, 165, 0)
# ===================== 工具函数 =====================
def get_font(size: int) -> pygame.font.Font:
"""获取中文字体(兼容不同系统)"""
# 清空字体缓存避免内存泄漏
pygame.font.init()
fonts = ["SimHei", "Microsoft YaHei", "Heiti TC", "WenQuanYi Micro Hei", "Arial", "Helvetica"]
for font_name in fonts:
try:
font = pygame.font.SysFont(font_name, size)
# 测试字体是否能渲染中文
test_surf = font.render("测试", True, WHITE)
return font
except Exception:
continue
# 兜底:强制使用默认字体(即使显示方块,避免崩溃)
return pygame.font.Font(None, size)
# ===================== 滑动条组件 =====================
class Scrollbar:
def __init__(self, x: int, y: int, width: int, height: int, orientation: str = "vertical"):
self.x = x
self.y = y
self.width = width
self.height = height
self.orientation = orientation # vertical / horizontal
self.value = 0.0 # 0.0 ~ 1.0
self.grabbed = False
self.handle_rect = self._get_handle_rect()
def _get_handle_rect(self) -> pygame.Rect:
if self.orientation == "vertical":
handle_height = max(20, self.height * 0.2)
handle_y = self.y + (self.height - handle_height) * self.value
return pygame.Rect(self.x, handle_y, self.width, handle_height)
else:
handle_width = max(20, self.width * 0.2)
handle_x = self.x + (self.width - handle_width) * self.value
return pygame.Rect(handle_x, self.y, handle_width, self.height)
def update(self, events: List[pygame.event.Event], mouse_pos: Tuple[int, int]):
self.handle_rect = self._get_handle_rect()
mouse_x, mouse_y = mouse_pos
for event in events:
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1 and self.handle_rect.collidepoint(mouse_pos):
self.grabbed = True
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
self.grabbed = False
elif event.type == pygame.MOUSEMOTION and self.grabbed:
if self.orientation == "vertical":
# 垂直滑动条
new_y = mouse_y - self.handle_rect.height / 2
new_y = max(self.y, min(self.y + self.height - self.handle_rect.height, new_y))
self.value = (new_y - self.y) / (self.height - self.handle_rect.height)
else:
# 水平滑动条
new_x = mouse_x - self.handle_rect.width / 2
new_x = max(self.x, min(self.x + self.width - self.handle_rect.width, new_x))
self.value = (new_x - self.x) / (self.width - self.handle_rect.width)
def draw(self, surface: pygame.Surface):
# 绘制滑动条背景
bg_rect = pygame.Rect(self.x, self.y, self.width, self.height)
pygame.draw.rect(surface, DARK_GRAY, bg_rect)
pygame.draw.rect(surface, BLACK, bg_rect, 1)
# 绘制滑动条手柄
pygame.draw.rect(surface, BLUE, self.handle_rect)
pygame.draw.rect(surface, BLACK, self.handle_rect, 1)
def get_scroll_offset(self, total_content_size: int, visible_size: int) -> int:
"""根据滑动条值计算滚动偏移量"""
max_offset = max(0, total_content_size - visible_size)
return int(self.value * max_offset)
# ===================== 存档系统 =====================
class SaveSystem:
def __init__(self):
self.save_path = "save_data.json"
self.save_data = self.load_save()
def load_save(self) -> Dict:
default_data = {
"unlocked_cars": CAR_UNLOCK_CONDITIONS,
"highest_score": {"sports": 0, "offroad": 0, "truck": 0, "motorcycle": 0},
"last_level": 1,
"last_car": "sports",
"settings": {"sound_volume": 0.5, "music_volume": 0.3, "weather": "sunny"},
"car_upgrades": {
"sports": {"acceleration": 0, "durability": 0, "fuel_capacity": 0, "grip": 0, "max_speed": 0},
"offroad": {"acceleration": 0, "durability": 0, "fuel_capacity": 0, "grip": 0, "max_speed": 0},
"truck": {"acceleration": 0, "durability": 0, "fuel_capacity": 0, "grip": 0, "max_speed": 0},
"motorcycle": {"acceleration": 0, "durability": 0, "fuel_capacity": 0, "grip": 0, "max_speed": 0}
},
"achievements": ACHIEVEMENTS,
"car_skins": {
"sports": {"color": (255, 0, 0), "custom_img": None},
"offroad": {"color": (0, 0, 255), "custom_img": None},
"truck": {"color": (139, 69, 19), "custom_img": None},
"motorcycle": {"color": (128, 0, 128), "custom_img": None}
},
"saved_replays": [],
"total_score": 0, # 累计总分数(所有局数相加)
"props_used": 0,
"first_time": {
"menu": True,
"playing": True,
"upgrade": True,
"skin": True
},
"unlocked_levels": [1],
"last_selected_level": 1,
"last_car_x": 100,
"last_car_y": 300,
"last_car_angle": 0,
"last_car_score": 0,
"weapons_used": 0
}
try:
with open(self.save_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 补全缺失的键
for key, value in default_data.items():
if key not in data:
data[key] = value
if isinstance(value, dict):
for subkey, subval in value.items():
if subkey not in data[key]:
data[key][subkey] = subval
# 修复颜色元组的JSON序列化问题
for car in data["car_skins"]:
color = data["car_skins"][car]["color"]
if isinstance(color, (list, tuple)):
data["car_skins"][car]["color"] = tuple(color)
else:
data["car_skins"][car]["color"] = (255, 0, 0)
return data
except (FileNotFoundError, json.JSONDecodeError):
return default_data
def save_game(self):
save_data = self.save_data.copy()
for car in save_data["car_skins"]:
if isinstance(save_data["car_skins"][car]["color"], tuple):
save_data["car_skins"][car]["color"] = list(save_data["car_skins"][car]["color"])
with open(self.save_path, "w", encoding="utf-8") as f:
json.dump(save_data, f, indent=2, ensure_ascii=False)
def update_high_score(self, car_type: str, score: float):
"""修复分数保存逻辑:1. 更新车型最高分 2. 累计总分数"""
score = int(score)
current_high = self.save_data["highest_score"][car_type]
# 更新该车型的最高分
if score > current_high:
self.save_data["highest_score"][car_type] = score
# 累计总分数(每局结束都累加,而非仅新高分差值)
self.save_data["total_score"] += score
self.save_game()
def unlock_car(self, car_type: str):
if not self.save_data["unlocked_cars"][car_type]["unlocked"]:
self.save_data["unlocked_cars"][car_type]["unlocked"] = True
self.save_game()
def check_unlock(self, car_type: str, current_score: float) -> bool:
cond = self.save_data["unlocked_cars"][car_type]
if not cond["unlocked"] and current_score >= cond["score"]:
self.unlock_car(car_type)
self.check_achievement("unlock_all")
return True
return False
def upgrade_car_attr(self, car_type: str, attr: str, current_score: float) -> Tuple[bool, str]:
current_level = self.save_data["car_upgrades"][car_type][attr]
if current_level >= UPGRADE_MAX_LEVEL:
return False, "已达满级"
cost = UPGRADE_COSTS[attr] * (current_level + 1)
if self.save_data["total_score"] < cost:
return False, "分数不足"
self.save_data["total_score"] -= cost
self.save_data["car_upgrades"][car_type][attr] += 1
self.save_game()
return True, "升级成功"
def get_upgraded_attr(self, car_type: str, base_attrs: Dict) -> Dict:
upgrades = self.save_data["car_upgrades"][car_type]
upgraded = base_attrs.copy()
upgraded["acceleration"] *= (1 + upgrades["acceleration"] * 0.1)
upgraded["durability"] = int(base_attrs["durability"] * (1 + upgrades["durability"] * 0.1))
upgraded["max_fuel"] *= (1 + upgrades["fuel_capacity"] * 0.1)
upgraded["grip_bonus"] += upgrades["grip"] * 0.05
upgraded["max_speed"] *= (1 + upgrades["max_speed"] * 0.1)
return upgraded
def unlock_achievement(self, ach_id: str) -> bool:
if not self.save_data["achievements"][ach_id]["unlocked"]:
self.save_data["achievements"][ach_id]["unlocked"] = True
self.save_game()
return True
return False
def check_achievement(self, ach_id: str, value: Optional[float] = None) -> bool:
if ach_id == "score_500" and value and value >= 500:
return self.unlock_achievement("score_500")
elif ach_id == "score_1000" and value and value >= 1000:
return self.unlock_achievement("score_1000")
elif ach_id == "unlock_all":
all_unlocked = all([v["unlocked"] for v in self.save_data["unlocked_cars"].values()])
if all_unlocked:
return self.unlock_achievement("unlock_all")
elif ach_id == "use_10_props":
if self.save_data["props_used"] >= 10:
return self.unlock_achievement("use_10_props")
elif ach_id == "level_5" and value and isinstance(value, int) and value >= 5:
return self.unlock_achievement("level_5")
elif ach_id == "level_8" and value and isinstance(value, int) and value >= 8:
return self.unlock_achievement("level_8")
elif ach_id == "boss_defeat" and value:
return self.unlock_achievement("boss_defeat")
return False
def set_car_skin(self, car_type: str, color: Optional[Tuple[int, int, int]] = None,
custom_img: Optional[str] = None):
if color:
self.save_data["car_skins"][car_type]["color"] = color
if custom_img:
self.save_data["car_skins"][car_type]["custom_img"] = custom_img
self.save_game()
def save_replay(self, replay_data: List, name: Optional[str] = None):
replay_name = name or f"replay_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
replay_entry = {
"name": replay_name,
"data": replay_data,
"timestamp": datetime.now().isoformat(),
"car_type": self.save_data["last_car"],
"score": replay_data[-1]["score"] if replay_data else 0
}
self.save_data["saved_replays"].append(replay_entry)
self.save_game()
def add_prop_used(self):
self.save_data["props_used"] += 1
self.check_achievement("use_10_props")
self.save_game()
def add_weapon_used(self):
self.save_data["weapons_used"] += 1
self.save_game()
def mark_first_time(self, screen_name: str):
if self.save_data["first_time"][screen_name]:
self.save_data["first_time"][screen_name] = False
self.save_game()
def unlock_level(self, level_id: int) -> bool:
if level_id not in self.save_data["unlocked_levels"] and level_id <= len(BUILTIN_LEVELS):
self.save_data["unlocked_levels"].append(level_id)
self.save_data["unlocked_levels"].sort()
self.save_game()
return True
return False
def is_level_unlocked(self, level_id: int) -> bool:
if level_id > len(BUILTIN_LEVELS) or level_id < 1:
return False
level = next(l for l in BUILTIN_LEVELS if l["id"] == level_id)
if level["unlock_condition"] is None:
return True
cond = level["unlock_condition"]
if cond.get("required_level") in self.save_data["unlocked_levels"]:
self.unlock_level(level_id)
return True
return level_id in self.save_data["unlocked_levels"]
def set_last_selected_level(self, level_id: int):
if 1 <= level_id <= len(BUILTIN_LEVELS):
self.save_data["last_selected_level"] = level_id
self.save_game()
# ===================== 天气系统 =====================
class WeatherSystem:
def __init__(self, save_system: SaveSystem):
self.save_system = save_system
self.current_weather = save_system.save_data["settings"]["weather"]
self.particles = []
self.weather_effects = {
"sunny": {"friction_multiplier": 1.0, "grip_multiplier": 1.0, "color": (255, 255, 200)},
"rainy": {"friction_multiplier": 0.95, "grip_multiplier": 0.85, "color": (180, 200, 220)},
"snowy": {"friction_multiplier": 0.90, "grip_multiplier": 0.80, "color": (240, 240, 255)}
}
def set_weather(self, weather_type: str):
if weather_type in WEATHER_TYPES:
self.current_weather = weather_type
self.save_system.save_data["settings"]["weather"] = weather_type
self.save_system.save_game()
self.particles.clear()
def create_weather_particles(self):
if len(self.particles) > MAX_PARTICLES // 2:
return
# 修复:获取窗口尺寸前先判断是否存在显示表面
try:
screen_surface = pygame.display.get_surface()
if not screen_surface:
return
screen_w = screen_surface.get_width()
except:
return
if self.current_weather == "rainy":
for _ in range(3): # 原5 减少粒子数量优化卡顿
x = random.randint(0, screen_w)
y = random.randint(-50, 0)
vel_x = random.uniform(-0.5, 0.5)
vel_y = random.uniform(5, 8)
self.particles.append({"x": x, "y": y, "vel_x": vel_x, "vel_y": vel_y, "type": "rain"})
elif self.current_weather == "snowy":
for _ in range(2): # 原4 减少粒子数量优化卡顿
x = random.randint(0, screen_w)
y = random.randint(-50, 0)
vel_x = random.uniform(-1, 1)
vel_y = random.uniform(2, 4)
self.particles.append({"x": x, "y": y, "vel_x": vel_x, "vel_y": vel_y, "type": "snow"})
def update(self):
self.create_weather_particles()
# 修复:获取窗口尺寸前先判断是否存在显示表面
try:
screen_surface = pygame.display.get_surface()
if not screen_surface:
return
screen_h = screen_surface.get_height()
screen_w = screen_surface.get_width()
except:
return
for p in self.particles[:]:
p["x"] += p["vel_x"]
p["y"] += p["vel_y"]
if p["y"] > screen_h or p["x"] < 0 or p["x"] > screen_w:
self.particles.remove(p)
def get_physics_modifiers(self) -> Dict:
return self.weather_effects[self.current_weather]
def draw(self, surface: pygame.Surface):
overlay = pygame.Surface((surface.get_width(), surface.get_height()))
overlay.fill(self.weather_effects[self.current_weather]["color"])
overlay.set_alpha(50)
surface.blit(overlay, (0, 0))
for p in self.particles:
if p["type"] == "rain":
pygame.draw.line(surface, (100, 150, 200),
(p["x"], p["y"]), (p["x"], p["y"] + 5), 1)
elif p["type"] == "snow":
pygame.draw.circle(surface, WHITE, (int(p["x"]), int(p["y"])), 2)
# ===================== 关卡选择界面 =====================
class LevelSelector:
def __init__(self):
self.level_files = self.scan_level_files()
# 追加内置关卡(避免仅显示自定义关卡)
for level in BUILTIN_LEVELS:
self.level_files.append({
"name": f"{level['id']}_{level['name']}",
"path": "builtin",
"points": level["terrain_params"]["起伏范围"],
"created": "内置关卡"
})
self.selected_index = 0
self.scrollbar = Scrollbar(0, 0, 10, 0, "vertical") # 初始化滑动条
self.is_open = False
def scan_level_files(self) -> List[Dict]:
level_files = []
if os.path.exists("levels"):
for f in os.listdir("levels"):
if f.endswith(".json"):
level_path = os.path.join("levels", f)
try:
with open(level_path, "r", encoding="utf-8") as file:
data = json.load(file)
level_files.append({
"name": f.replace(".json", ""),
"path": level_path,
"points": data.get("points", []),
"created": data.get("created", "未知时间")
})
except:
continue
return level_files
def handle_input(self, events: List[pygame.event.Event], mouse_pos: Tuple[int, int],
window_size: Tuple[int, int]) -> Optional[str]:
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
preview_width = 150 * scale_x
preview_height = 100 * scale_y
# 更新滑动条
self.scrollbar.x = window_size[0] - 20 * scale_x
self.scrollbar.y = 150 * scale_y
self.scrollbar.height = window_size[1] - 250 * scale_y # 留出退出按钮空间
self.scrollbar.update(events, mouse_pos)
# 计算可见的关卡数量
visible_count = int((window_size[1] - 250 * scale_y) / (preview_height + 20 * scale_y))
scroll_offset = self.scrollbar.get_scroll_offset(len(self.level_files) * (preview_height + 20 * scale_y),
visible_count * (preview_height + 20 * scale_y)) // int(
preview_height + 20 * scale_y)
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
for event in events:
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
# 检查退出按钮
if exit_rect.collidepoint(mouse_pos):
self.is_open = False
return None
# 检查关卡预览点击
for i in range(scroll_offset, min(scroll_offset + visible_count, len(self.level_files))):
x = 50 * scale_x
y = 150 * scale_y + (i - scroll_offset) * (preview_height + 20 * scale_y)
rect = pygame.Rect(x, y, preview_width, preview_height)
if rect.collidepoint(mouse_pos):
self.selected_index = i
return self.level_files[i]["name"]
# 滚轮控制滑动条
elif event.button == 4: # 上滚
self.scrollbar.value = max(0.0, self.scrollbar.value - 0.1)
elif event.button == 5: # 下滚
self.scrollbar.value = min(1.0, self.scrollbar.value + 0.1)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: # 修复K_ESC错误
self.is_open = False
elif event.key == pygame.K_RETURN and self.level_files:
return self.level_files[self.selected_index]["name"]
elif event.key == pygame.K_UP:
self.selected_index = max(0, self.selected_index - 1)
# 同步滑动条
self.scrollbar.value = self.selected_index / max(1, len(self.level_files) - visible_count)
elif event.key == pygame.K_DOWN:
self.selected_index = min(len(self.level_files) - 1, self.selected_index + 1)
self.scrollbar.value = self.selected_index / max(1, len(self.level_files) - visible_count)
return None
def draw_preview(self, surface: pygame.Surface, level_data: Dict, x: int, y: int, is_selected: bool, scale_x: float,
scale_y: float):
color = BLUE if is_selected else GRAY
preview_size = (150 * scale_x, 100 * scale_y)
pygame.draw.rect(surface, color, (x, y, preview_size[0], preview_size[1]), 2)
preview_surf = pygame.Surface(preview_size)
preview_surf.fill((135, 206, 235))
if level_data["points"]:
try:
# 修复points数据类型判断
points = level_data["points"]
if isinstance(points, (list, tuple)) and len(points) >= 2:
if isinstance(points[0], (list, tuple)) and len(points[0]) >= 2:
min_x = min(p[0] for p in points)
max_x = max(p[0] for p in points)
min_y = min(p[1] for p in points)
max_y = max(p[1] for p in points)
else:
# 如果是起伏范围的数值
min_x, max_x = -100, 100
min_y, max_y = points[0], points[1]
else:
min_x, max_x = -100, 100
min_y, max_y = -50, 50
scaled_points = []
# 生成预览地形
for px in range(5, int(preview_size[0]) - 5, 5):
norm_x = (px - 5) / (preview_size[0] - 10)
py = min_y + norm_x * (max_y - min_y)
scaled_points.append((px, 50 + py * 0.5))
if len(scaled_points) > 1:
pygame.draw.lines(preview_surf, GREEN, False, scaled_points, 2)
terrain_polygon = scaled_points + [(preview_size[0] - 5, preview_size[1] - 5),
(5, preview_size[1] - 5)]
pygame.draw.polygon(preview_surf, BROWN, terrain_polygon, 0)
except Exception as e:
# 出错时绘制默认预览
pygame.draw.line(preview_surf, GREEN, (5, 50), (preview_size[0] - 5, 50), 2)
surface.blit(preview_surf, (x, y))
# 绘制关卡名称(修复字体)
name = level_data["name"][:10] + "..." if len(level_data["name"]) > 10 else level_data["name"]
name_text = get_font(int(16 * scale_y)).render(name, True, BLACK)
surface.blit(name_text, (x + 5, y + preview_size[1] + 5))
def draw(self, surface: pygame.Surface, window_size: Tuple[int, int]):
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
# 绘制遮罩
overlay = pygame.Surface(window_size)
overlay.fill(BLACK)
overlay.set_alpha(180)
surface.blit(overlay, (0, 0))
# 标题(修复字体)
title = get_font(int(48 * scale_y)).render("选择关卡", True, WHITE)
surface.blit(title, (window_size[0] // 2 - title.get_width() // 2, 50 * scale_y))
if not self.level_files:
no_levels = get_font(int(36 * scale_y)).render("暂无关卡", True, WHITE)
surface.blit(no_levels, (window_size[0] // 2 - no_levels.get_width() // 2, 200 * scale_y))
else:
# 更新滑动条位置和尺寸
self.scrollbar.x = window_size[0] - 30 * scale_x
self.scrollbar.y = 150 * scale_y
self.scrollbar.height = window_size[1] - 250 * scale_y
self.scrollbar.width = 10 * scale_x
# 计算可见区域
preview_width = 150 * scale_x
preview_height = 100 * scale_y
visible_count = int((window_size[1] - 250 * scale_y) / (preview_height + 20 * scale_y))
scroll_offset = self.scrollbar.get_scroll_offset(len(self.level_files) * (preview_height + 20 * scale_y),
visible_count * (preview_height + 20 * scale_y)) // int(
preview_height + 20 * scale_y)
# 绘制关卡预览
for i in range(scroll_offset, min(scroll_offset + visible_count, len(self.level_files))):
x = 50 * scale_x
y = 150 * scale_y + (i - scroll_offset) * (preview_height + 20 * scale_y)
self.draw_preview(surface, self.level_files[i], x, y, i == self.selected_index, scale_x, scale_y)
# 绘制滑动条
self.scrollbar.draw(surface)
# 【修改4】绘制退出主菜单按钮
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
pygame.draw.rect(surface, RED, exit_rect)
exit_text = get_font(int(24 * scale_y)).render("返回主菜单", True, WHITE)
surface.blit(exit_text,
(exit_rect.centerx - exit_text.get_width() // 2, exit_rect.centery - exit_text.get_height() // 2))
# 提示文字(修复字体)
hint = get_font(int(24 * scale_y)).render("鼠标选择 / 滚轮滚动 / ESC退出 / Enter确认", True,
WHITE)
surface.blit(hint, (window_size[0] // 2 - hint.get_width() // 2, window_size[1] - 120 * scale_y))
# ===================== 车辆改装系统 =====================
class CarUpgradeSystem:
def __init__(self, save_system: SaveSystem):
self.save_system = save_system
self.selected_car = "sports"
self.selected_attr = None
self.is_open = True
self.scrollbar = Scrollbar(0, 0, 10, 0, "vertical") # 滑动条
def handle_input(self, events: List[pygame.event.Event], window_size: Tuple[int, int]) -> Optional[str]:
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
# 更新滑动条
self.scrollbar.x = window_size[0] - 20 * scale_x
self.scrollbar.y = 150 * scale_y
self.scrollbar.height = window_size[1] - 250 * scale_y # 留出退出按钮空间
self.scrollbar.update(events, pygame.mouse.get_pos())
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
msg = None
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.is_open = False
elif event.key == pygame.K_1:
if self.save_system.save_data["unlocked_cars"]["sports"]["unlocked"]:
self.selected_car = "sports"
elif event.key == pygame.K_2:
if self.save_system.save_data["unlocked_cars"]["offroad"]["unlocked"]:
self.selected_car = "offroad"
elif event.key == pygame.K_3:
if self.save_system.save_data["unlocked_cars"]["truck"]["unlocked"]:
self.selected_car = "truck"
elif event.key == pygame.K_4:
if self.save_system.save_data["unlocked_cars"]["motorcycle"]["unlocked"]:
self.selected_car = "motorcycle"
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
# 检查退出按钮
if exit_rect.collidepoint(pygame.mouse.get_pos()):
self.is_open = False
return None
mouse_pos = pygame.mouse.get_pos()
attrs = ["acceleration", "durability", "fuel_capacity", "grip", "max_speed"]
# 计算滚动偏移
scroll_offset = self.scrollbar.get_scroll_offset(len(attrs) * 60 * scale_y,
window_size[1] - 250 * scale_y)
for i, attr in enumerate(attrs):
btn_rect = pygame.Rect(
500 * scale_x,
150 * scale_y + i * 60 * scale_y - scroll_offset,
100 * scale_x,
40 * scale_y
)
if btn_rect.collidepoint(mouse_pos):
success, msg = self.save_system.upgrade_car_attr(
self.selected_car, attr, self.save_system.save_data["total_score"]
)
# 滚轮控制
elif event.button == 4:
self.scrollbar.value = max(0.0, self.scrollbar.value - 0.1)
elif event.button == 5:
self.scrollbar.value = min(1.0, self.scrollbar.value + 0.1)
return msg
def draw(self, surface: pygame.Surface, window_size: Tuple[int, int]):
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
# 绘制遮罩
overlay = pygame.Surface(window_size)
overlay.fill(BLACK)
overlay.set_alpha(180)
surface.blit(overlay, (0, 0))
# 标题(修复字体)
title = get_font(int(48 * scale_y)).render(f"车辆改装 - {self.selected_car}", True, WHITE)
surface.blit(title, (window_size[0] // 2 - title.get_width() // 2, 30 * scale_y))
# 可用分数(修复字体)
score_text = get_font(int(36 * scale_y)).render(
f"可用分数: {self.save_system.save_data['total_score']}", True, YELLOW)
surface.blit(score_text, (50 * scale_x, 50 * scale_y))
# 车辆切换提示(修复重叠:垂直排列)
car_hints = [
f"1 - 跑车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['sports']['unlocked'] else '未解锁'})",
f"2 - 越野车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['offroad']['unlocked'] else '未解锁'})",
f"3 - 卡车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['truck']['unlocked'] else '未解锁'})",
f"4 - 摩托车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['motorcycle']['unlocked'] else '未解锁'})",
"ESC - 退出"
]
for i, hint in enumerate(car_hints):
hint_text = get_font(int(24 * scale_y)).render(hint, True, WHITE)
surface.blit(hint_text, (50 * scale_x, 90 * scale_y + i * 40 * scale_y))
# 更新滑动条
self.scrollbar.x = window_size[0] - 30 * scale_x
self.scrollbar.y = 150 * scale_y
self.scrollbar.height = window_size[1] - 250 * scale_y
self.scrollbar.width = 10 * scale_x
# 计算滚动偏移
attrs = ["acceleration", "durability", "fuel_capacity", "grip", "max_speed"]
attr_names = ["加速度", "耐久度", "燃料容量", "抓地力", "最大速度"]
scroll_offset = self.scrollbar.get_scroll_offset(len(attrs) * 60 * scale_y,
window_size[1] - 250 * scale_y)
# 绘制属性升级按钮(修复字体)
upgrades = self.save_system.save_data["car_upgrades"][self.selected_car]
for i, (attr, name) in enumerate(zip(attrs, attr_names)):
level = upgrades[attr]
cost = UPGRADE_COSTS[attr] * (level + 1) if level < UPGRADE_MAX_LEVEL else "MAX"
y_pos = 150 * scale_y + i * 60 * scale_y - scroll_offset
# 属性名称和等级
attr_text = get_font(int(36 * scale_y)).render(f"{name}: {level}/{UPGRADE_MAX_LEVEL}",
True, WHITE)
surface.blit(attr_text, (100 * scale_x, y_pos))
# 升级按钮
btn_color = GREEN if (
level < UPGRADE_MAX_LEVEL and self.save_system.save_data["total_score"] >= cost) else RED
btn_rect = pygame.Rect(500 * scale_x, y_pos, 100 * scale_x, 40 * scale_y)
pygame.draw.rect(surface, btn_color, btn_rect)
btn_text = get_font(int(24 * scale_y)).render(
f"升级 {cost}" if level < UPGRADE_MAX_LEVEL else "已满级", True, BLACK)
btn_text_rect = btn_text.get_rect(center=btn_rect.center)
surface.blit(btn_text, btn_text_rect)
# 绘制滑动条
self.scrollbar.draw(surface)
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
pygame.draw.rect(surface, RED, exit_rect)
exit_text = get_font(int(24 * scale_y)).render("返回主菜单", True, WHITE)
surface.blit(exit_text,
(exit_rect.centerx - exit_text.get_width() // 2, exit_rect.centery - exit_text.get_height() // 2))
# 首次进入提示
if self.save_system.save_data["first_time"]["upgrade"]:
tip_text = get_font(int(24 * scale_y)).render("提示:使用分数升级车辆属性,提升性能!", True,
YELLOW)
surface.blit(tip_text, (window_size[0] // 2 - tip_text.get_width() // 2, window_size[1] - 120 * scale_y))
# ===================== 成就系统界面 =====================
class AchievementSystem:
def __init__(self, save_system: SaveSystem):
self.save_system = save_system
self.is_open = True
self.scrollbar = Scrollbar(0, 0, 10, 0, "vertical") # 滑动条
def update(self, events: List[pygame.event.Event], mouse_pos: Tuple[int, int]):
"""从handle_events传递事件,避免事件丢失"""
# 【修改4】处理退出主菜单按钮
window_size = pygame.display.get_surface().get_size() if pygame.display.get_surface() else (INIT_WIDTH,
INIT_HEIGHT)
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
self.scrollbar.update(events, mouse_pos)
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: # 修复K_ESC错误
self.is_open = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1 and exit_rect.collidepoint(mouse_pos):
self.is_open = False
elif event.button == 4: # 上滚
self.scrollbar.value = max(0.0, self.scrollbar.value - 0.1)
elif event.button == 5: # 下滚
self.scrollbar.value = min(1.0, self.scrollbar.value + 0.1)
def draw(self, surface: pygame.Surface, window_size: Tuple[int, int]):
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
# 绘制遮罩
overlay = pygame.Surface(window_size)
overlay.fill(BLACK)
overlay.set_alpha(180)
surface.blit(overlay, (0, 0))
# 标题(修复字体)
title = get_font(int(48 * scale_y)).render("成就系统", True, YELLOW)
surface.blit(title, (window_size[0] // 2 - title.get_width() // 2, 30 * scale_y))
# 更新滑动条位置和尺寸
self.scrollbar.x = window_size[0] - 30 * scale_x
self.scrollbar.y = 150 * scale_y
self.scrollbar.height = window_size[1] - 250 * scale_y
self.scrollbar.width = 10 * scale_x
# 成就列表
achievements = self.save_system.save_data["achievements"]
ach_list = [(aid, data) for aid, data in achievements.items()]
item_height = 80 * scale_y
visible_count = int((window_size[1] - 250 * scale_y) / item_height)
scroll_offset = self.scrollbar.get_scroll_offset(len(ach_list) * item_height,
visible_count * item_height) // int(item_height)
# 绘制成就项
for i in range(scroll_offset, min(scroll_offset + visible_count, len(ach_list))):
aid, ach_data = ach_list[i]
y_pos = 150 * scale_y + (i - scroll_offset) * item_height
# 成就背景
bg_color = GREEN if ach_data["unlocked"] else GRAY
bg_rect = pygame.Rect(50 * scale_x, y_pos, window_size[0] - 110 * scale_x, item_height - 10 * scale_y)
pygame.draw.rect(surface, bg_color, bg_rect, border_radius=5)
pygame.draw.rect(surface, WHITE, bg_rect, 2, border_radius=5)
# 成就名称和描述(修复字体)
name_text = get_font(int(28 * scale_y)).render(ach_data["name"], True,
BLACK if ach_data["unlocked"] else DARK_GRAY)
desc_text = get_font(int(20 * scale_y)).render(ach_data["desc"], True,
BLACK if ach_data["unlocked"] else DARK_GRAY)
status_text = get_font(int(24 * scale_y)).render("已解锁" if ach_data["unlocked"] else "未解锁", True,
RED if not ach_data["unlocked"] else DARK_BLUE)
surface.blit(name_text, (60 * scale_x, y_pos + 10 * scale_y))
surface.blit(desc_text, (60 * scale_x, y_pos + 40 * scale_y))
surface.blit(status_text, (bg_rect.right - 120 * scale_x, y_pos + 20 * scale_y))
# 绘制滑动条
self.scrollbar.draw(surface)
# 绘制退出主菜单按钮
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
pygame.draw.rect(surface, RED, exit_rect)
exit_text = get_font(int(24 * scale_y)).render("返回主菜单", True, WHITE)
surface.blit(exit_text,
(exit_rect.centerx - exit_text.get_width() // 2, exit_rect.centery - exit_text.get_height() // 2))
# 提示文字(修复字体)
hint = get_font(int(24 * scale_y)).render("ESC退出 / 滚轮滚动查看更多", True, WHITE)
surface.blit(hint, (window_size[0] // 2 - hint.get_width() // 2, window_size[1] - 120 * scale_y))
# 首次进入提示
if self.save_system.save_data["first_time"]["menu"]:
tip_text = get_font(int(24 * scale_y)).render("提示:完成指定目标解锁成就!", True, YELLOW)
surface.blit(tip_text, (window_size[0] // 2 - tip_text.get_width() // 2, window_size[1] - 160 * scale_y))
# 车辆皮肤系统
class CarSkinSystem:
def __init__(self, save_system: SaveSystem):
self.save_system = save_system
self.selected_car = "sports"
self.selected_color_idx = 0
self.is_open = True
self.scrollbar = Scrollbar(0, 0, 10, 0, "vertical")
def handle_input(self, events: List[pygame.event.Event], window_size: Tuple[int, int]) -> Optional[str]:
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
# 更新滑动条
self.scrollbar.x = window_size[0] - 30 * scale_x
self.scrollbar.y = 150 * scale_y
self.scrollbar.height = window_size[1] - 250 * scale_y
self.scrollbar.update(events, pygame.mouse.get_pos())
# 退出按钮
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
msg = None
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.is_open = False
elif event.key == pygame.K_1 and self.save_system.save_data["unlocked_cars"]["sports"]["unlocked"]:
self.selected_car = "sports"
self._update_color_idx()
elif event.key == pygame.K_2 and self.save_system.save_data["unlocked_cars"]["offroad"]["unlocked"]:
self.selected_car = "offroad"
self._update_color_idx()
elif event.key == pygame.K_3 and self.save_system.save_data["unlocked_cars"]["truck"]["unlocked"]:
self.selected_car = "truck"
self._update_color_idx()
elif event.key == pygame.K_4 and self.save_system.save_data["unlocked_cars"]["motorcycle"]["unlocked"]:
self.selected_car = "motorcycle"
self._update_color_idx()
elif event.key == pygame.K_LEFT:
self.selected_color_idx = (self.selected_color_idx - 1) % len(SKIN_COLORS)
self.save_system.set_car_skin(self.selected_car, SKIN_COLORS[self.selected_color_idx])
elif event.key == pygame.K_RIGHT:
self.selected_color_idx = (self.selected_color_idx + 1) % len(SKIN_COLORS)
self.save_system.set_car_skin(self.selected_car, SKIN_COLORS[self.selected_color_idx])
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
if event.button == 1:
# 退出按钮
if exit_rect.collidepoint(mouse_pos):
self.is_open = False
return None
# 颜色选择
color_size = 40 * scale_x
start_x = 100 * scale_x
start_y = 400 * scale_y
for i, color in enumerate(SKIN_COLORS):
x = start_x + (i % 4) * (color_size + 20 * scale_x)
y = start_y + (i // 4) * (color_size + 20 * scale_y)
color_rect = pygame.Rect(x, y, color_size, color_size)
if color_rect.collidepoint(mouse_pos):
self.selected_color_idx = i
self.save_system.set_car_skin(self.selected_car, color)
# 滚轮
elif event.button == 4:
self.scrollbar.value = max(0.0, self.scrollbar.value - 0.1)
elif event.button == 5:
self.scrollbar.value = min(1.0, self.scrollbar.value + 0.1)
return msg
def _update_color_idx(self):
"""同步当前车辆颜色索引"""
current_color = self.save_system.save_data["car_skins"][self.selected_car]["color"]
for i, color in enumerate(SKIN_COLORS):
if color == current_color:
self.selected_color_idx = i
break
else:
self.selected_color_idx = 0
def draw(self, surface: pygame.Surface, window_size: Tuple[int, int]):
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
# 遮罩
overlay = pygame.Surface(window_size)
overlay.fill(BLACK)
overlay.set_alpha(180)
surface.blit(overlay, (0, 0))
# 标题
title = get_font(int(48 * scale_y)).render(f"车辆皮肤 - {self.selected_car}", True, WHITE)
surface.blit(title, (window_size[0] // 2 - title.get_width() // 2, 30 * scale_y))
# 车辆切换提示
car_hints = [
f"1 - 跑车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['sports']['unlocked'] else '未解锁'})",
f"2 - 越野车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['offroad']['unlocked'] else '未解锁'})",
f"3 - 卡车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['truck']['unlocked'] else '未解锁'})",
f"4 - 摩托车 ({'已解锁' if self.save_system.save_data['unlocked_cars']['motorcycle']['unlocked'] else '未解锁'})",
"←/→ 切换颜色 | 点击色块选择 | ESC退出"
]
for i, hint in enumerate(car_hints):
hint_text = get_font(int(24 * scale_y)).render(hint, True, WHITE)
surface.blit(hint_text, (50 * scale_x, 90 * scale_y + i * 40 * scale_y))
# 绘制颜色选择块
color_size = 40 * scale_x
start_x = 100 * scale_x
start_y = 400 * scale_y
for i, color in enumerate(SKIN_COLORS):
x = start_x + (i % 4) * (color_size + 20 * scale_x)
y = start_y + (i // 4) * (color_size + 20 * scale_y)
color_rect = pygame.Rect(x, y, color_size, color_size)
pygame.draw.rect(surface, color, color_rect)
pygame.draw.rect(surface, YELLOW if i == self.selected_color_idx else WHITE, color_rect, 3)
# 预览车辆
preview_x = window_size[0] // 2 - 50 * scale_x
preview_y = 250 * scale_y
car_color = self.save_system.save_data["car_skins"][self.selected_car]["color"]
# 绘制简易车辆预览
pygame.draw.rect(surface, car_color, (preview_x, preview_y, 80 * scale_x, 40 * scale_y), border_radius=5)
pygame.draw.circle(surface, BLACK, (preview_x + 20 * scale_x, preview_y + 40 * scale_x), 15 * scale_x)
pygame.draw.circle(surface, BLACK, (preview_x + 60 * scale_x, preview_y + 40 * scale_x), 15 * scale_x)
# 退出按钮
exit_rect = pygame.Rect(window_size[0] // 2 - 100 * scale_x, window_size[1] - 80 * scale_y, 200 * scale_x,
50 * scale_y)
pygame.draw.rect(surface, RED, exit_rect)
exit_text = get_font(int(24 * scale_y)).render("返回主菜单", True, WHITE)
surface.blit(exit_text,
(exit_rect.centerx - exit_text.get_width() // 2, exit_rect.centery - exit_text.get_height() // 2))
# 首次提示
if self.save_system.save_data["first_time"]["skin"]:
tip_text = get_font(int(24 * scale_y)).render("提示:选择喜欢的颜色自定义车辆外观!", True, YELLOW)
surface.blit(tip_text, (window_size[0] // 2 - tip_text.get_width() // 2, window_size[1] - 120 * scale_y))
# 粒子系统
class ParticleSystem:
def __init__(self):
self.particles = []
self.particle_cache = {}
def add_particle(self, x: float, y: float, color: Tuple[int, int, int],
vel_x: float = 0, vel_y: float = 0, lifetime: int = 30,
size: int = 3, particle_type: str = "default"):
if len(self.particles) >= MAX_PARTICLES:
return
self.particles.append({
"x": x, "y": y, "color": color, "vel_x": vel_x, "vel_y": vel_y,
"lifetime": lifetime, "max_lifetime": lifetime, "size": size,
"type": particle_type
})
def update(self):
# 批量更新粒子(性能优化)
for p in self.particles[:PARTICLE_BATCH_SIZE]:
p["x"] += p["vel_x"]
p["y"] += p["vel_y"]
p["lifetime"] -= 1
p["vel_y"] += GRAVITY * 0.5 # 粒子重力
p["vel_x"] *= 0.98 # 空气阻力
# 类型特效
if p["type"] == "explosion":
p["size"] = max(1, p["size"] - 0.2)
elif p["type"] == "smoke":
p["color"] = (
max(0, p["color"][0] - 2),
max(0, p["color"][1] - 2),
max(0, p["color"][2] - 2)
)
# 清理死亡粒子
self.particles = [p for p in self.particles if p["lifetime"] > 0]
def draw(self, surface: pygame.Surface, camera_x: float = 0):
# 批量绘制粒子
for p in self.particles:
draw_x = p["x"] - camera_x
if 0 <= draw_x <= surface.get_width() and 0 <= p["y"] <= surface.get_height():
alpha = int(255 * (p["lifetime"] / p["max_lifetime"]))
temp_surf = pygame.Surface((p["size"] * 2, p["size"] * 2), pygame.SRCALPHA)
pygame.draw.circle(temp_surf, (*p["color"], alpha), (p["size"], p["size"]), p["size"])
surface.blit(temp_surf, (draw_x - p["size"], p["y"] - p["size"]))
# 道具系统
class PropSystem:
def __init__(self, save_system: SaveSystem):
self.save_system = save_system
self.props = []
self.active_props = {
"boost": 0, "nitro": 0, "shield": 0, "weapon_cooldown": 0
}
self.collected_props = set() # 新增:记录已收集的道具ID,避免重复收集
def spawn_props(self, terrain_x: int, terrain_length: int, prop_density: float, prop_weights: Dict):
"""根据关卡配置生成道具(为每个道具添加唯一ID)"""
prop_types = list(prop_weights.keys())
prop_probs = list(prop_weights.values())
total_props = int(terrain_length * prop_density / 1000)
for idx in range(total_props): # 使用idx作为唯一ID
x = random.randint(terrain_x, terrain_length)
y = random.randint(100, 400)
prop_type = random.choices(prop_types, weights=prop_probs)[0]
self.props.append({
"id": f"prop_{idx}_{x}_{y}", # 唯一ID
"x": x, "y": y, "type": prop_type, "collected": False,
"size": 20, "timer": 0
})
def update(self, car_rect: pygame.Rect, camera_x: float):
"""修复:收集道具后立即移除,避免重复检测"""
# 临时列表存储待移除的道具
props_to_remove = []
for prop in self.props:
if prop["collected"] or prop["id"] in self.collected_props:
props_to_remove.append(prop)
continue
prop["timer"] += 1
prop["y"] += math.sin(prop["timer"] * 0.1) * 0.5 # 漂浮动画
# 检测碰撞(转换为屏幕坐标)
prop_screen_x = prop["x"] - camera_x
prop_rect = pygame.Rect(
prop_screen_x - prop["size"] // 2,
prop["y"] - prop["size"] // 2,
prop["size"], prop["size"]
)
if prop_rect.colliderect(car_rect):
# 收集道具并标记
self.collect_prop(prop["type"])
prop["collected"] = True
self.collected_props.add(prop["id"])
props_to_remove.append(prop)
for prop in props_to_remove:
if prop in self.props:
self.props.remove(prop)
# 更新主动道具时长
for prop_type in ["boost", "nitro", "shield"]:
if self.active_props[prop_type] > 0:
self.active_props[prop_type] -= 1
# 更新武器冷却
if self.active_props["weapon_cooldown"] > 0:
self.active_props["weapon_cooldown"] -= 1
def collect_prop(self, prop_type: str):
"""修复:确保每个道具只被收集一次"""
self.save_system.add_prop_used()
# 按类型返回效果,且仅触发一次
if prop_type == "fuel":
return "fuel", FUEL_PACK_VALUE
elif prop_type == "boost":
self.active_props["boost"] = BOOST_DURATION
return "boost", BOOST_MULTIPLIER
elif prop_type == "nitro":
self.active_props["nitro"] = NITRO_DURATION
return "nitro", NITRO_MULTIPLIER
elif prop_type == "repair":
return "repair", REPAIR_VALUE
elif prop_type == "shield":
self.active_props["shield"] = SHIELD_DURATION
return "shield", SHIELD_DURATION
elif prop_type == "weapon":
self.save_system.add_weapon_used()
return "weapon", 1
return None, 0
def use_weapon(self) -> bool:
if self.active_props["weapon_cooldown"] == 0:
self.active_props["weapon_cooldown"] = 120
return True
return False
def draw(self, surface: pygame.Surface, camera_x: float):
prop_colors = {
"fuel": YELLOW, "boost": RED, "nitro": PURPLE, "repair": GREEN,
"shield": CYAN, "weapon": DARKRED
}
prop_icons = {
"fuel": "⛽", "boost": "⚡", "nitro": "💨", "repair": "🔧",
"shield": "🛡️", "weapon": "🔫"
}
for prop in self.props:
if not prop["collected"]:
draw_x = prop["x"] - camera_x
if 0 <= draw_x <= surface.get_width():
pygame.draw.circle(surface, prop_colors[prop["type"]],
(int(draw_x), int(prop["y"])), prop["size"] // 2)
pygame.draw.circle(surface, WHITE, (int(draw_x), int(prop["y"])),
prop["size"] // 2, 2)
icon_text = get_font(16).render(prop_icons[prop["type"]], True, BLACK)
surface.blit(icon_text, (int(draw_x) - 8, int(prop["y"]) - 8))
# 绘制主动道具状态
y_offset = 20
for prop_type, duration in self.active_props.items():
if duration > 0 and prop_type not in ["weapon_cooldown"]:
bar_width = 100 * (duration / (BOOST_DURATION if prop_type == "boost" else
NITRO_DURATION if prop_type == "nitro" else SHIELD_DURATION))
bg_rect = pygame.Rect(20, y_offset, 100, 20)
fill_rect = pygame.Rect(20, y_offset, bar_width, 20)
pygame.draw.rect(surface, GRAY, bg_rect)
pygame.draw.rect(surface, prop_colors[prop_type], fill_rect)
pygame.draw.rect(surface, WHITE, bg_rect, 1)
text = get_font(16).render(prop_type, True, WHITE)
surface.blit(text, (130, y_offset))
y_offset += 30
# BOSS 系统
class BossSystem:
def __init__(self, save_system: SaveSystem, particle_system: ParticleSystem):
self.save_system = save_system
self.particle_system = particle_system
self.boss = None
self.bullets = []
self.boss_health = 0
self.boss_defeated = False
def spawn_boss(self, x: int, level_difficulty: str):
"""生成BOSS"""
difficulty_multipliers = {
"中等": 1.0, "困难": 1.5, "极难": 2.0, "地狱级": 2.5, "终极": 3.0
}
multiplier = difficulty_multipliers.get(level_difficulty, 1.0)
self.boss = {
"x": x, "y": 300, "width": 100, "height": 80,
"health": 500 * multiplier, "max_health": 500 * multiplier,
"speed": 2 * multiplier, "direction": 1,
"shoot_cooldown": 0, "shoot_interval": 60 // multiplier
}
self.boss_health = self.boss["health"]
self.boss_defeated = False
def update(self, car_x: float, car_y: float):
if not self.boss or self.boss_defeated:
return
# BOSS移动
self.boss["x"] += self.boss["speed"] * self.boss["direction"]
if self.boss["x"] > car_x + 500 or self.boss["x"] < car_x - 500:
self.boss["direction"] *= -1
# BOSS射击
self.boss["shoot_cooldown"] -= 1
if self.boss["shoot_cooldown"] <= 0:
self.shoot_bullet()
self.boss["shoot_cooldown"] = self.boss["shoot_interval"]
# 更新子弹
for bullet in self.bullets[:]:
bullet["x"] += bullet["vel_x"]
bullet["y"] += bullet["vel_y"]
bullet["lifetime"] -= 1
# 子弹粒子效果
self.particle_system.add_particle(
bullet["x"], bullet["y"], (255, 100, 0),
vel_x=random.uniform(-1, 1), vel_y=random.uniform(-1, 1),
lifetime=5, size=2, particle_type="default"
)
if bullet["lifetime"] <= 0:
self.bullets.remove(bullet)
# BOSS碰撞检测(简化)
boss_rect = pygame.Rect(
self.boss["x"] - self.boss["width"] // 2,
self.boss["y"] - self.boss["height"] // 2,
self.boss["width"], self.boss["height"]
)
car_rect = pygame.Rect(car_x - 20, car_y - 20, 40, 40)
if boss_rect.colliderect(car_rect):
return "collision"
def shoot_bullet(self):
"""BOSS射击"""
if not self.boss:
return
bullet_speed = 5
self.bullets.append({
"x": self.boss["x"], "y": self.boss["y"],
"vel_x": random.uniform(-bullet_speed, bullet_speed),
"vel_y": random.uniform(-bullet_speed, bullet_speed),
"lifetime": 120, "damage": 10
})
def take_damage(self, damage: int) -> bool:
"""BOSS受击"""
if not self.boss or self.boss_defeated:
return False
self.boss["health"] -= damage
self.boss_health = self.boss["health"]
# 爆炸粒子
for _ in range(5):
self.particle_system.add_particle(
self.boss["x"] + random.uniform(-50, 50),
self.boss["y"] + random.uniform(-40, 40),
(255, 0, 0),
vel_x=random.uniform(-3, 3), vel_y=random.uniform(-3, 3),
lifetime=20, size=5, particle_type="explosion"
)
if self.boss["health"] <= 0:
self.boss_defeated = True
self.save_system.check_achievement("boss_defeat", True)
# 死亡特效
for _ in range(30):
self.particle_system.add_particle(
self.boss["x"] + random.uniform(-50, 50),
self.boss["y"] + random.uniform(-40, 40),
(255, 100, 0),
vel_x=random.uniform(-5, 5), vel_y=random.uniform(-5, 5),
lifetime=30, size=8, particle_type="explosion"
)
return True
return False
def draw(self, surface: pygame.Surface, camera_x: float):
if not self.boss or self.boss_defeated:
return
# 绘制BOSS
draw_x = self.boss["x"] - camera_x
boss_rect = pygame.Rect(
draw_x - self.boss["width"] // 2,
self.boss["y"] - self.boss["height"] // 2,
self.boss["width"], self.boss["height"]
)
pygame.draw.rect(surface, DARKRED, boss_rect)
pygame.draw.rect(surface, RED, boss_rect, 3)
# 绘制BOSS血条
health_bar_width = self.boss["width"]
health_bar_height = 10
health_ratio = self.boss["health"] / self.boss["max_health"]
bg_rect = pygame.Rect(draw_x - health_bar_width // 2, self.boss["y"] - self.boss["height"] // 2 - 15,
health_bar_width, health_bar_height)
fill_rect = pygame.Rect(draw_x - health_bar_width // 2, self.boss["y"] - self.boss["height"] // 2 - 15,
health_bar_width * health_ratio, health_bar_height)
pygame.draw.rect(surface, GRAY, bg_rect)
pygame.draw.rect(surface, RED, fill_rect)
pygame.draw.rect(surface, WHITE, bg_rect, 1)
# 绘制子弹
for bullet in self.bullets:
draw_x = bullet["x"] - camera_x
pygame.draw.circle(surface, ORANGE, (int(draw_x), int(bullet["y"])), 5)
# 网络联机系统(基础实现)
class NetworkSystem:
def __init__(self, save_system: SaveSystem):
self.save_system = save_system
self.client_socket = None
self.server_socket = None
self.connected = False
self.thread = None
self.other_players = {}
self.running = False
def start_server(self):
"""启动服务器"""
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((SERVER_HOST, SERVER_PORT))
self.server_socket.listen(5)
self.running = True
def accept_clients():
while self.running:
try:
conn, addr = self.server_socket.accept()
self.thread = threading.Thread(target=self.handle_client, args=(conn, addr))
self.thread.daemon = True
self.thread.start()
except:
break
server_thread = threading.Thread(target=accept_clients)
server_thread.daemon = True
server_thread.start()
def connect_client(self):
"""连接服务器"""
try:
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client_socket.connect((SERVER_HOST, SERVER_PORT))
self.connected = True
def receive_data():
while self.connected:
try:
data = self.client_socket.recv(4096)
if not data:
break
player_data = pickle.loads(data)
self.other_players[player_data["id"]] = player_data
except:
self.connected = False
self.thread = threading.Thread(target=receive_data)
self.thread.daemon = True
self.thread.start()
return True
except:
return False
def handle_client(self, conn, addr):
"""处理客户端连接"""
client_id = str(addr)
self.other_players[client_id] = {"x": 100, "y": 300, "angle": 0, "score": 0}
while self.running:
try:
data = conn.recv(4096)
if not data:
del self.other_players[client_id]
break
player_data = pickle.loads(data)
self.other_players[client_id] = player_data
# 广播给其他玩家
for pid, pdata in self.other_players.items():
if pid != client_id:
try:
conn.sendall(pickle.dumps(pdata))
except:
pass
except:
del self.other_players[client_id]
break
def send_data(self, car_data: Dict):
"""发送玩家数据"""
if self.connected and self.client_socket:
try:
data = pickle.dumps({
"id": str(self.client_socket.getsockname()),
"x": car_data["x"], "y": car_data["y"],
"angle": car_data["angle"], "score": car_data["score"]
})
self.client_socket.sendall(data)
except:
self.connected = False
def stop(self):
"""停止网络服务"""
self.running = False
self.connected = False
if self.client_socket:
self.client_socket.close()
if self.server_socket:
self.server_socket.close()
if self.thread and self.thread.is_alive():
self.thread.join(1)
# 车辆类(核心逻辑)
class Car:
BASE_ATTRS = {
"sports": {"acceleration": 0.5, "max_speed": 8, "max_fuel": 100, "durability": 100, "grip_bonus": 0.1},
"offroad": {"acceleration": 0.4, "max_speed": 7, "max_fuel": 120, "durability": 150, "grip_bonus": 0.2},
"truck": {"acceleration": 0.3, "max_speed": 6, "max_fuel": 150, "durability": 200, "grip_bonus": 0.15},
"motorcycle": {"acceleration": 0.6, "max_speed": 9, "max_fuel": 80, "durability": 80, "grip_bonus": 0.05}
}
def __init__(self, car_type: str, save_system: SaveSystem):
self.save_system = save_system
self.car_type = car_type
self.base_attrs = self.BASE_ATTRS[car_type].copy()
self.upgraded_attrs = save_system.get_upgraded_attr(car_type, self.base_attrs)
# 物理属性
self.x = 100.0
self.y = 300.0
self.velocity_x = 0.0
self.velocity_y = 0.0
self.angle = 0.0
self.acceleration = self.upgraded_attrs["acceleration"]
self.max_speed = self.upgraded_attrs["max_speed"]
self.fuel = self.upgraded_attrs["max_fuel"] # 初始燃料为最大值
self.max_fuel = self.upgraded_attrs["max_fuel"] # 燃料上限
self.durability = self.upgraded_attrs["durability"]
self.max_durability = self.upgraded_attrs["durability"]
self.grip_bonus = self.upgraded_attrs["grip_bonus"]
# 状态
self.on_ground = False
self.terrain_type = "grass"
self.score = 0.0
self.damage = 0
def update(self, keys, terrain, weather_system: WeatherSystem):
# 燃料消耗(修复:强制边界检查,避免燃料为负或无限)
fuel_consume = 0.1 if (keys[pygame.K_UP] or keys[pygame.K_DOWN]) else 0
self.fuel = max(0.0, min(self.max_fuel, self.fuel - fuel_consume))
# 物理更新(燃料为0时无法加速)
if self.fuel > 0:
if keys[pygame.K_UP]:
self.velocity_x += math.cos(math.radians(self.angle)) * self.acceleration
if keys[pygame.K_DOWN]:
self.velocity_x -= math.cos(math.radians(self.angle)) * self.acceleration
if keys[pygame.K_LEFT]:
self.angle -= 2
if keys[pygame.K_RIGHT]:
self.angle += 2
else:
# 燃料耗尽时减速
self.velocity_x *= 0.95
self.velocity_y *= 0.95
# 天气和地形物理修正
weather_mod = weather_system.get_physics_modifiers()
terrain_mod = FRICTION_MAP[self.terrain_type]
self.velocity_x *= terrain_mod["friction"] * weather_mod["friction_multiplier"]
self.velocity_x = max(-self.max_speed, min(self.max_speed, self.velocity_x))
# 重力
self.velocity_y += GRAVITY
self.x += self.velocity_x
self.y += self.velocity_y
# 地形碰撞
self.on_ground = False
terrain_y = terrain.get_height(self.x)
if self.y >= terrain_y:
self.y = terrain_y
self.velocity_y = 0
self.on_ground = True
self.terrain_type = terrain.get_terrain_type(self.x)
# 抓地力修正
grip = terrain_mod["grip"] * weather_mod["grip_multiplier"] + self.grip_bonus
self.angle = terrain.get_slope(self.x) * grip
# 边界检测
if self.y > INIT_HEIGHT + 100:
self.durability -= 1
if self.x < 0:
self.x = 0
self.velocity_x = 0
# 得分计算
self.score = max(self.score, self.x / 10)
def apply_prop(self, prop_type: str, value: float) -> str:
"""修复燃料应用逻辑:强制不超过上限"""
if prop_type == "fuel":
# 核心修复:严格限制燃料不超过最大值
self.fuel = min(self.max_fuel, self.fuel + value)
return f"燃料+{value}"
elif prop_type == "boost":
self.max_speed *= BOOST_MULTIPLIER
return f"加速x{BOOST_MULTIPLIER}"
elif prop_type == "nitro":
self.max_speed *= NITRO_MULTIPLIER
return f"氮气x{NITRO_MULTIPLIER}"
elif prop_type == "repair":
self.durability = min(self.max_durability, self.durability + value)
return f"修复+{value}"
elif prop_type == "shield":
return "护盾激活"
elif prop_type == "weapon":
return "武器就绪"
return ""
def take_damage(self, damage: int):
self.durability -= damage
self.durability = max(0, self.durability)
def draw(self, surface: pygame.Surface, camera_x: float, color: Tuple[int, int, int] = None):
draw_x = self.x - camera_x
draw_y = self.y
# 使用自定义皮肤颜色
car_color = color or self.save_system.save_data["car_skins"][self.car_type]["color"]
# 绘制简易车辆
car_points = [
(draw_x, draw_y - 10),
(draw_x + 20, draw_y - 20),
(draw_x + 40, draw_y - 20),
(draw_x + 60, draw_y - 10),
(draw_x + 60, draw_y + 10),
(draw_x + 40, draw_y + 20),
(draw_x + 20, draw_y + 20),
(draw_x, draw_y + 10)
]
# 旋转车辆
rotated_points = []
cx, cy = draw_x + 30, draw_y
angle_rad = math.radians(self.angle)
for (x, y) in car_points:
dx = x - cx
dy = y - cy
rotated_dx = dx * math.cos(angle_rad) - dy * math.sin(angle_rad)
rotated_dy = dx * math.sin(angle_rad) + dy * math.cos(angle_rad)
rotated_points.append((cx + rotated_dx, cy + rotated_dy))
pygame.draw.polygon(surface, car_color, rotated_points)
pygame.draw.polygon(surface, BLACK, rotated_points, 2)
# 绘制车轮
wheel_radius = 8
pygame.draw.circle(surface, BLACK, (int(draw_x + 10), int(draw_y + 20)), wheel_radius)
pygame.draw.circle(surface, BLACK, (int(draw_x + 50), int(draw_y + 20)), wheel_radius)
# 地形类(生成关卡地形)
class Terrain:
def __init__(self, level_data: Dict):
self.level_data = level_data
self.points = []
self.terrain_types = []
self.height_cache = {}
self.generate_terrain()
def generate_terrain(self):
"""生成地形数据"""
params = self.level_data["terrain_params"]
length = params["长度"]
start_y = INIT_HEIGHT * params["起始y比例"]
min_y, max_y = params["起伏范围"]
# 生成基础地形
self.points.append((0, start_y))
self.terrain_types.append(self._get_terrain_type(0))
for x in range(100, length + 1, 100):
y = start_y + random.randint(min_y, max_y)
self.points.append((x, y))
self.terrain_types.append(self._get_terrain_type(x))
# 缓存高度数据
self._cache_heights()
def _get_terrain_type(self, x: int) -> str:
"""获取地形类型"""
weights = self.level_data["terrain_params"]["地形权重"]
types = list(weights.keys())
probs = list(weights.values())
return random.choices(types, weights=probs)[0]
def _cache_heights(self):
"""缓存高度数据(性能优化)"""
self.height_cache.clear()
for i in range(len(self.points) - 1):
x1, y1 = self.points[i]
x2, y2 = self.points[i + 1]
dx = x2 - x1
dy = y2 - y1
for x in range(int(x1), int(x2), HEIGHT_CACHE_SIZE // 10):
t = (x - x1) / dx
y = y1 + dy * t
self.height_cache[x] = y
def get_height(self, x: float) -> float:
"""获取指定x位置的高度"""
x_int = int(x)
if x_int in self.height_cache:
return self.height_cache[x_int]
# 插值计算
for i in range(len(self.points) - 1):
x1, y1 = self.points[i]
x2, y2 = self.points[i + 1]
if x1 <= x <= x2:
t = (x - x1) / (x2 - x1)
y = y1 + (y2 - y1) * t
# 更新缓存
if len(self.height_cache) < HEIGHT_CACHE_SIZE:
self.height_cache[x_int] = y
return y
return self.points[-1][1]
def get_slope(self, x: float) -> float:
"""获取指定x位置的坡度"""
h1 = self.get_height(x - 50)
h2 = self.get_height(x + 50)
return math.degrees(math.atan2(h2 - h1, 100))
def get_terrain_type(self, x: float) -> str:
"""获取指定x位置的地形类型"""
for i in range(len(self.points) - 1):
x1 = self.points[i][0]
x2 = self.points[i + 1][0]
if x1 <= x <= x2:
return self.terrain_types[i]
return self.terrain_types[-1]
def draw(self, surface: pygame.Surface, camera_x: float):
"""绘制地形"""
draw_points = []
terrain_colors = {
"grass": GREEN, "rock": GRAY, "water": BLUE, "ramp": YELLOW
}
# 绘制可见地形
for (x, y), t_type in zip(self.points, self.terrain_types):
draw_x = x - camera_x
if draw_x < -100 or draw_x > surface.get_width() + 100:
continue
draw_points.append((draw_x, y))
# 绘制地形多边形
if len(draw_points) > 1:
poly_points = draw_points + [(surface.get_width() + 100, surface.get_height()),
(-100, surface.get_height())]
pygame.draw.polygon(surface, BROWN, poly_points)
# 绘制地形纹理(简化)
for i in range(len(draw_points) - 1):
x1, y1 = draw_points[i]
x2, y2 = draw_points[i + 1]
t_type = self.terrain_types[i]
pygame.draw.line(surface, terrain_colors[t_type], (x1, y1), (x2, y2), 5)
# 游戏主类(整合所有系统)
class HillClimbGame:
def __init__(self):
pygame.init()
pygame.display.set_caption("登山赛车")
self.screen = pygame.display.set_mode((INIT_WIDTH, INIT_HEIGHT), pygame.RESIZABLE)
self.clock = pygame.time.Clock()
self.running = True
self.state = "menu"
# 系统初始化
self.save_system = SaveSystem()
self.weather_system = WeatherSystem(self.save_system)
self.level_selector = LevelSelector()
self.upgrade_system = CarUpgradeSystem(self.save_system)
self.achievement_system = AchievementSystem(self.save_system)
self.skin_system = CarSkinSystem(self.save_system)
self.particle_system = ParticleSystem()
self.prop_system = PropSystem(self.save_system) # 使用修复后的PropSystem
self.boss_system = BossSystem(self.save_system, self.particle_system)
self.network_system = NetworkSystem(self.save_system)
# 游戏数据
self.selected_level = self.save_system.save_data["last_selected_level"]
self.car = None
self.terrain = None
self.camera_x = 0
self.replay_data = []
self.replay_index = 0
def handle_events(self):
events = pygame.event.get()
mouse_pos = pygame.mouse.get_pos()
window_size = self.screen.get_size()
for event in events:
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.VIDEORESIZE:
self.screen = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE)
# 状态处理
if self.state == "menu":
self._handle_menu_events(events, mouse_pos, window_size)
elif self.state == "level_select":
self._handle_level_select_events(events, mouse_pos, window_size)
elif self.state == "upgrade":
msg = self.upgrade_system.handle_input(events, window_size)
if not self.upgrade_system.is_open:
self.state = "menu"
elif self.state == "skin":
msg = self.skin_system.handle_input(events, window_size)
if not self.skin_system.is_open:
self.state = "menu"
elif self.state == "achievement":
self.achievement_system.update(events, mouse_pos)
if not self.achievement_system.is_open:
self.state = "menu"
elif self.state == "playing":
self._handle_playing_events(events)
elif self.state == "replay":
self._handle_replay_events(events)
def _handle_menu_events(self, events, mouse_pos, window_size):
"""处理主菜单事件"""
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
# 按钮区域
buttons = [
("开始游戏", (window_size[0] // 2, 150 * scale_y), "level_select"),
("车辆改装", (window_size[0] // 2, 250 * scale_y), "upgrade"),
("车辆皮肤", (window_size[0] // 2, 350 * scale_y), "skin"),
("成就系统", (window_size[0] // 2, 450 * scale_y), "achievement"),
("退出游戏", (window_size[0] // 2, 550 * scale_y), "quit")
]
for event in events:
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
for text, (x, y), action in buttons:
btn_rect = pygame.Rect(x - 100 * scale_x, y - 25 * scale_y, 200 * scale_x, 50 * scale_y)
if btn_rect.collidepoint(mouse_pos):
if action == "quit":
self.running = False
elif action == "level_select":
self.state = "level_select"
self.level_selector.is_open = True
else:
self.state = action
if action == "upgrade":
self.upgrade_system.is_open = True
elif action == "skin":
self.skin_system.is_open = True
elif action == "achievement":
self.achievement_system.is_open = True
# 首次进入提示
if self.save_system.save_data["first_time"]["menu"]:
self.save_system.mark_first_time("menu")
def _handle_level_select_events(self, events, mouse_pos, window_size):
"""处理关卡选择事件"""
selected_level = self.level_selector.handle_input(events, mouse_pos, window_size)
if selected_level and self.level_selector.is_open:
# 解析内置关卡
for level in BUILTIN_LEVELS:
if selected_level == f"{level['id']}_{level['name']}":
self.selected_level = level["id"]
self.save_system.set_last_selected_level(level["id"])
self._start_game(level)
self.state = "playing"
break
def _handle_playing_events(self, events):
"""处理游戏中事件"""
keys = pygame.key.get_pressed()
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.state = "menu"
elif event.key == pygame.K_r:
# 重新开始
self._start_game(BUILTIN_LEVELS[self.selected_level - 1])
elif event.key == pygame.K_s:
# 保存回放
self.save_system.save_replay(self.replay_data)
def _handle_replay_events(self, events):
"""处理回放事件"""
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.state = "menu"
elif event.key == pygame.K_SPACE:
# 暂停/继续
pass
def _start_game(self, level_data: Dict):
"""开始游戏"""
# 初始化车辆
car_type = self.save_system.save_data["last_car"]
self.car = Car(car_type, self.save_system)
# 初始化地形
self.terrain = Terrain(level_data)
# 生成道具
self.prop_system.props.clear()
self.prop_system.spawn_props(
0, level_data["terrain_params"]["长度"],
level_data["prop_density"], level_data["prop_weights"]
)
# 生成BOSS
self.boss_system.boss = None
self.boss_system.boss_defeated = False
if level_data["has_boss"]:
self.boss_system.spawn_boss(
level_data["boss_spawn_x"],
level_data["difficulty"]
)
# 重置游戏状态
self.camera_x = 0
self.replay_data = []
self.replay_index = 0
# 首次进入提示
if self.save_system.save_data["first_time"]["playing"]:
self.save_system.mark_first_time("playing")
def update(self):
if self.state == "playing" and self.car and self.terrain:
# 原有更新逻辑保持不变
self.weather_system.update()
self.particle_system.update()
keys = pygame.key.get_pressed()
self.car.update(keys, self.terrain, self.weather_system)
car_rect = pygame.Rect(
self.car.x - self.camera_x - 20,
self.car.y - 20, 40, 40
)
self.prop_system.update(car_rect, self.camera_x)
# 检查道具收集(修复:仅处理一次)
for prop in self.prop_system.props[:]: # 遍历副本避免列表变化
if prop["collected"] and not prop["id"] in self.prop_system.collected_props:
prop_type, value = self.prop_system.collect_prop(prop["type"])
self.car.apply_prop(prop_type, value)
self.boss_system.update(self.car.x, self.car.y)
if self.boss_system.update(self.car.x, self.car.y) == "collision":
self.car.take_damage(5)
self.camera_x = max(0, self.car.x - self.screen.get_width() // 2)
self.replay_data.append({
"x": self.car.x, "y": self.car.y, "angle": self.car.angle,
"score": self.car.score, "fuel": self.car.fuel,
"durability": self.car.durability
})
# 修复游戏结束逻辑:确保分数保存和成就检测
if self.car.durability <= 0 or self.car.fuel <= 0:
# 1. 保存该车型最高分 + 累计总分数
self.save_system.update_high_score(self.car.car_type, self.car.score)
# 2. 检测成就
self.save_system.check_achievement("score_500", self.car.score)
self.save_system.check_achievement("score_1000", self.car.score)
self.save_system.check_achievement("level_5", self.selected_level)
self.save_system.check_achievement("level_8", self.selected_level)
# 3. 解锁下一关(如果当前关卡通关)
if self.selected_level < len(BUILTIN_LEVELS):
self.save_system.unlock_level(self.selected_level + 1)
# 4. 切换状态
self.state = "menu"
def draw(self):
self.screen.fill(BLACK)
window_size = self.screen.get_size()
scale_x = window_size[0] / INIT_WIDTH
scale_y = window_size[1] / INIT_HEIGHT
if self.state == "menu":
self._draw_menu(scale_x, scale_y)
elif self.state == "level_select":
self.level_selector.draw(self.screen, window_size)
elif self.state == "upgrade":
self.upgrade_system.draw(self.screen, window_size)
elif self.state == "skin":
self.skin_system.draw(self.screen, window_size)
elif self.state == "achievement":
self.achievement_system.draw(self.screen, window_size)
elif self.state == "playing":
self._draw_playing(scale_x, scale_y)
pygame.display.flip()
def _draw_menu(self, scale_x: float, scale_y: float):
"""绘制主菜单"""
# 背景
self.screen.fill(DARK_BLUE)
# 标题
title = get_font(int(64 * scale_y)).render("登山赛车", True, YELLOW)
self.screen.blit(title, (self.screen.get_width() // 2 - title.get_width() // 2, 50 * scale_y))
# 按钮
buttons = [
("开始游戏", (self.screen.get_width() // 2, 150 * scale_y)),
("车辆改装", (self.screen.get_width() // 2, 250 * scale_y)),
("车辆皮肤", (self.screen.get_width() // 2, 350 * scale_y)),
("成就系统", (self.screen.get_width() // 2, 450 * scale_y)),
("退出游戏", (self.screen.get_width() // 2, 550 * scale_y))
]
mouse_pos = pygame.mouse.get_pos()
for text, (x, y) in buttons:
btn_rect = pygame.Rect(x - 100 * scale_x, y - 25 * scale_y, 200 * scale_x, 50 * scale_y)
color = GREEN if btn_rect.collidepoint(mouse_pos) else BLUE
pygame.draw.rect(self.screen, color, btn_rect, border_radius=10)
pygame.draw.rect(self.screen, WHITE, btn_rect, 2, border_radius=10)
text_surf = get_font(int(36 * scale_y)).render(text, True, WHITE)
self.screen.blit(text_surf, (x - text_surf.get_width() // 2, y - text_surf.get_height() // 2))
# 最高分显示
high_score = self.save_system.save_data["highest_score"][self.save_system.save_data["last_car"]]
score_text = get_font(int(24 * scale_y)).render(f"最高分: {int(high_score)}", True, WHITE)
self.screen.blit(score_text, (50 * scale_x, self.screen.get_height() - 50 * scale_y))
def _draw_playing(self, scale_x: float, scale_y: float):
"""绘制游戏画面"""
# 绘制天气效果
self.weather_system.draw(self.screen)
# 绘制地形
self.terrain.draw(self.screen, self.camera_x)
# 绘制道具
self.prop_system.draw(self.screen, self.camera_x)
# 绘制BOSS
self.boss_system.draw(self.screen, self.camera_x)
# 绘制车辆
self.car.draw(self.screen, self.camera_x)
# 绘制粒子
self.particle_system.draw(self.screen, self.camera_x)
# 绘制UI
# 燃料条
fuel_ratio = self.car.fuel / self.car.max_fuel
fuel_bg = pygame.Rect(20 * scale_x, 20 * scale_y, 200 * scale_x, 30 * scale_y)
fuel_fill = pygame.Rect(20 * scale_x, 20 * scale_y, 200 * scale_x * fuel_ratio, 30 * scale_y)
pygame.draw.rect(self.screen, GRAY, fuel_bg)
pygame.draw.rect(self.screen, YELLOW, fuel_fill)
pygame.draw.rect(self.screen, WHITE, fuel_bg, 2)
fuel_text = get_font(int(24 * scale_y)).render("燃料", True, WHITE)
self.screen.blit(fuel_text, (230 * scale_x, 20 * scale_y))
# 血条
health_ratio = self.car.durability / self.car.max_durability
health_bg = pygame.Rect(20 * scale_x, 60 * scale_y, 200 * scale_x, 30 * scale_y)
health_fill = pygame.Rect(20 * scale_x, 60 * scale_y, 200 * scale_x * health_ratio, 30 * scale_y)
pygame.draw.rect(self.screen, GRAY, health_bg)
pygame.draw.rect(self.screen, RED, health_fill)
pygame.draw.rect(self.screen, WHITE, health_bg, 2)
health_text = get_font(int(24 * scale_y)).render("耐久", True, WHITE)
self.screen.blit(health_text, (230 * scale_x, 60 * scale_y))
# 分数
score_text = get_font(int(36 * scale_y)).render(f"分数: {int(self.car.score)}", True, WHITE)
self.screen.blit(score_text, (self.screen.get_width() - 200 * scale_x, 20 * scale_y))
# 关卡信息
level_text = get_font(int(24 * scale_y)).render(
f"关卡: {BUILTIN_LEVELS[self.selected_level - 1]['name']}", True, WHITE)
self.screen.blit(level_text, (self.screen.get_width() // 2 - level_text.get_width() // 2, 20 * scale_y))
def run(self):
while self.running:
self.handle_events()
self.update()
self.draw()
self.clock.tick(FPS)
# 退出清理
self.save_system.save_game()
self.network_system.stop()
pygame.quit()
sys.exit()
# 主函数(启动游戏)
if __name__ == "__main__":
try:
game = HillClimbGame()
game.run()
except Exception as e:
print(f"游戏出错: {e}")
import traceback
traceback.print_exc()
pygame.quit()
sys.exit(1)
七、程序运行部分截图展示





八、发布阶段
通过pyinstaller将 Python 代码打包为可执行文件(如 Windows 的.exe),步骤:
- 安装
pyinstaller:pip install pyinstaller; - 打包命令:
pyinstaller --onefile --windowed --name HillClimbGame main.py(main.py为游戏代码文件); - 处理资源依赖(如字体、图片,若有额外资源需手动打包)。
九、总结
本文介绍了一款基于Pygame开发的登山赛车游戏,详细阐述了其开发过程和完整实现。游戏包含物理模拟、关卡系统、养成系统等核心功能,以及天气系统、BOSS挑战等扩展功能。文章从技术栈选择、基础框架搭建开始,逐步讲解了地形生成、车辆物理、道具系统等关键模块的实现,并介绍了存档系统、网络联机等高级功能。最后提供了游戏截图和打包发布方法,展示了从开发到发布的完整流程。该游戏通过Python实现,具有丰富的可玩性和扩展性,适合作为休闲游戏项目参考。