ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Pygame打造逆行飙车小游戏:主循环、碰撞检测与难度设计

用Pygame打造逆行飙车小游戏:主循环、碰撞检测与难度设计 简介这是一份基于Python Pygame库编写的赛车小游戏完整源码包游戏名为“逆行飙车”适合刚接触游戏开发和Pygame的初学者动手实践。项目通过键盘方向键控制赛车涉及事件监听、角色移动、碰撞检测与界面刷新等核心逻辑能帮助读者理解2D小游戏从初始化到主循环的基本框架。压缩包共12个文件包含1个主程序py、1个说明文档md、4个字体ttf、4个游戏素材png以及2个背景/碰撞音效mp3整体大小约1.95MB素材齐全可即开即玩。目前已有2025人学习下载。随包附带的README对运行方式和代码结构有简要说明配合源码中的类设计与注释读者可快速定位赛车属性、障碍物规则和胜负判定等模块非常适合作为入门Pygame的第一个练手项目。1. 先把 pygame 赛车小游戏的主循环跑通逆行飙车到底在玩什么用 Python 做赛车小游戏最容易被劝退的不是算法而是第一次面对「pygame 赛车小游戏」时不知道代码该从哪一行看起。这份源码的核心玩法是逆行飙车玩家在画面下方控制自己的车左右移动对向车辆从上方逆向驶来撞上就结束。它不像真实竞速游戏那样有复杂的物理引擎纯粹靠矩形碰撞、坐标位移和难度递增撑起来恰恰因为这一点它非常适合当作 Python 图形编程的练手项目。我在拆这份代码时发现真正值得理解的是主循环的组织方式、对向车辆的生成规则以及碰撞判定里的那些边界细节。无论你是拿它交课程设计还是想搞懂 pygame 事件驱动的底层逻辑都能从中找到能直接抄走的段落。2. 拆开代码结构文件清单、主循环与 60 帧节奏2.1 源码文件的组成三个模块各管一段拿到这份赛车小游戏源码先别急着双击运行把它当作一个「pygame 小项目」来拆。常见的组织方式是三个文件入口主程序 main.py、玩家角色模块 player.py、对向车管理模块 enemy.py有的版本会加一个 config.py 来集中存放颜色、窗口尺寸、车速等常量。我第一次看这类代码时踩过坑以为所有逻辑都堆在 main.py 里结果翻了三屏才找到分数累加的位置。这里有一个实用原则凡是需要反复调的数字都放到 config.py 里。比如窗口大小、玩家移动速度、对向车初始速度、生成间隔、车道数量这些参数在游戏调优阶段会被频繁改动集中写在一个文件里意味着你不用在几百行代码里来回翻找。下面这张表列出的是这类赛车小游戏源码中常见的文件构成文件职责改动频率config.py存放常量窗口尺寸、颜色、初始速度、车道数调平衡性时改player.py玩家车的初始化、移动、边界限制改手感时动enemy.py对向车的生成、下落、移除、碰撞回调调难度时动main.py初始化 pygame、主循环、碰撞检测、分数绘制结构上基本不动文件数量不是越多越好但把常量单独拎出来的做法可以让你在调「车速越来越快」这个逻辑时不用靠肉眼搜索每个魔法数字。我一般会保持 main.py 里尽量只写流程不直接写死数字这样后面接音效、加菜单都方便。2.2 游戏主循环事件、更新、绘制三件事交替发生pygame 程序的核心是一个永不退出的 while 循环所有「动起来」的效果都是在这个循环里反复执行三件事处理事件、更新状态、绘制画面。这份源码的主循环骨架通常长这样import pygame import sys pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(逆行飙车) clock pygame.time.Clock() while True: # 阶段一处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() # 阶段二更新坐标、检测碰撞、刷新分数 # player.update() # enemy.update() # check_collision() # 阶段三绘制背景、车辆、分数 # screen.fill((30, 30, 30)) # screen.blit(player_image, player_rect) # pygame.display.flip() clock.tick(60)注意阶段一里pygame.event.get()拿到的是一个事件列表它把鼠标点击、键盘按下、窗口关闭等操作全部打包成事件对象。如果你把玩家移动逻辑也写在这个循环里直接处理按键会很别扭因为事件是一次性的。真正自然的做法是在阶段二里用pygame.key.get_pressed()获取键盘的实时状态这一点后面章节展开。pygame.display.flip()的作用是把画好的内容一次性提交到屏幕上——没有这一行窗口会一直保持黑屏或者上一帧的画面。clock.tick(60)控制循环每秒最多执行 60 次也就是常说的 60 帧。这里有个常见误解tick(60)并不是用来限制性能的而是让游戏在不同电脑上跑出差不多的速度。如果不加这一行高性能电脑上的赛车会快得根本躲不开。2.3 帧率与坐标为什么小车速度不用时间计算有了固定的 60 帧之后坐标移动就可以用「每帧移动几个像素」这种直观方式来表达。例如玩家速度设置成 6意味着每帧向右或向左移动 6 个像素在 60 帧下就是每秒最多移动 360 像素。这个数字并不绝对窗口宽度 800、车辆宽度约 50 时从最左到最右大约需要 2 秒出头手感上属于「来得及反应但不拖沓」。另一种做法是记录上一帧到当前帧的时间间隔 dt用「每秒移动多少像素」来控制速度代码更严谨但初学阶段容易把概念搞混。这份源码选择的是最简单的每帧定长移动我觉得对赛车小游戏来说够用了因为帧率已经被tick(60)锁住。真正要关心的是另一件事对向车的速度会不会因为分数升高而变得太快快到每帧位移超过自身高度导致碰撞检测直接穿模。这是第 4 章会讲的内容。3. 写一个能开的车键盘控制、边界限制与碰撞判定的两个关键点3.1 键盘控制用 get_pressed 实现长按移动很多第一次写 pygame 游戏的人都会在键盘控制上翻车因为他们用的是事件里的KEYDOWN判断每次按键只触发一次移动结果就是「点一下动一下」完全没法流畅躲车。这份源码里玩家移动采用的是常见的实时轮询方式keys pygame.key.get_pressed() player_speed 6 if keys[pygame.K_LEFT]: player_rect.x - player_speed if keys[pygame.K_RIGHT]: player_rect.x player_speed if keys[pygame.K_UP]: player_speed 9 # 短暂加速注意这里直接赋值不会生效需在外部重新赋值pygame.key.get_pressed()返回的是一个布尔元组每个元素对应一个按键的按下状态。只要按键没松开这个值就一直是 True所以长按左移、松开停止的效果天然就实现了。player_rect.x表示矩形左上角的横坐标坐标原点在窗口左上角x 越大越靠右。要注意一个容易被忽略的细节如果你想实现「按 UP 键短暂加速」只在移动分支里改player_speed是不够的因为下一帧循环重新进入时player_speed又会被赋值回初始值 6。所以加速逻辑要在按键判断区域之外维护一个统一的变量赋值结构比如开始时player_speed 9 if keys[pygame.K_UP] else 6。这类小坑在改代码时特别容易遇到。3.2 边界限制clamp 比 if 判断更省心玩家车如果不加边界限制按着左键几秒就会开出屏幕外再按右键又不知道从哪里冒出来。处理方式有两种我推荐用 clamp 方法一行代码取代四行判断# 方法一四行边界判断 if player_rect.left 0: player_rect.left 0 if player_rect.right SCREEN_WIDTH: player_rect.right SCREEN_WIDTH # 方法二clamp 一行搞定推荐 player_rect.clamp_ip(screen.get_rect())clamp_ip的意思是「就地钳制」直接将矩形限制在屏幕矩形范围内左右都兼顾到了。这里的ip后缀表示 in-place即直接修改调用方对象而不返回新对象。如果写player_rect.clamp(...)则会返回一个新矩形原坐标不变初学的时候容易把这两者搞混导致边界限制「看起来写了但没生效」。3.3 碰撞判定矩形相交的误差与缩小碰撞框赛车游戏最核心的判定就是「撞上了没」。pygame 提供了非常直接的矩形相交检测方法if player_rect.colliderect(enemy_rect): game_over True crash_sound.play()colliderect判断两个矩形是否有交集有就返回 True。这个方法计算的是精确到像素的矩形重叠对于赛车游戏来说已经足够。但问题来了车辆图片通常不是占满整个矩形区域的轮子外沿、车头车尾往往有不少透明或空白像素。如果你直接用原图大小的矩形做碰撞玩家会感觉「明明没碰到也被判撞车」。我提供的解决思路是给每辆车设定一个略小的碰撞框而不是直接拿图片矩形参与运算# 碰撞框比图片矩形小一圈减少判定误差 hitbox player_rect.inflate(-12, -10) enemy_hitbox enemy_rect.inflate(-12, -10) if hitbox.colliderect(enemy_hitbox): game_over Trueinflate(-12, -10)将矩形在宽度方向收缩 12 像素、高度方向收缩 10 像素。负数就是缩小正数是扩大。这个数值要根据车辆图片的留白量来调一般缩 10 到 20 像素就够。这个优化对游戏体验的提升很明显属于「改了才知道原来判定这么重要」的那种改动。4. 逆行机制与难度曲线对向车生成、加速与分数的算法关系4.1 对向车生成按车道等距生成不搞随机乱撞「逆行飙车」里对向车从哪里刷新决定了这个游戏好不好玩。最懒的做法是随机给一个 x 坐标然后让车往下掉但这样容易出现两个问题一是车可能卡在屏幕边缘二是两辆车重叠在一根车道上玩家根本没法判断路径。所以合格的做法是引入车道概念。车道数量由路面宽度决定。假设路面宽 800分成三道每道约 266 像素车宽约 50那生成逻辑一般长这样import random LANE_COUNT 3 ROAD_WIDTH 800 CAR_WIDTH 50 def spawn_enemy(cars_group): lane random.randint(0, LANE_COUNT - 1) # 车道中心坐标减去车宽一半防止车跑偏 car_x (ROAD_WIDTH / LANE_COUNT) * lane (ROAD_WIDTH / LANE_COUNT - CAR_WIDTH) / 2 car_rect cars_group[lane] if lane len(cars_group) else None注意上面代码里我留了一个坑cars_group[lane]这种写法在实际代码里是不存在的这里是想说明生成时最好维护一个「每车道最后一辆车」的列表用于检查最小间距。真正落地的常见做法是last_car_y [0] * LANE_COUNT # 记录每条车道最近一辆车的 y 坐标 def can_spawn(lane, min_gap150): # 如果这辆车离屏幕顶部太近说明上一辆刚刷出来先等等 return last_car_y[lane] min_gap def spawn_enemy(): lane random.randint(0, LANE_COUNT - 1) car_width 50 road_width_per_lane ROAD_WIDTH // LANE_COUNT x lane * road_width_per_lane (road_width_per_lane - car_width) // 2 return pygame.Rect(x, -80, car_width, 100)这里的last_car_y[lane]记录的是该车道最后一辆车当前移动到的 y 坐标当它还没离开生成区域上方一段距离时不生成新车确保上下两辆车之间至少留有min_gap像素的间隙。min_gap会随着游戏难度提高而减小也就是车流变得越来越密。4.2 难度曲线分数每涨一截车速涨一点游戏如果从头到尾一个速度玩家玩三十秒就会腻。这份源码的难度提升逻辑基本是「分数驱动」的每次成功躲避一辆车就加分分到某个阈值就提高对向车的下落速度同时可能略微提高生成频率。伪代码是这个逻辑score 10 enemy_speed 3 score // 500 spawn_interval max(120, 300 - score // 100)用整数除法来实现阶梯式加速看着很巧妙但也有隐患score // 500在分数小于 500 时一直是 0玩家会感觉前几百分的难度完全没变化。我见过更好的写法是分段配置分数区间对向车速度生成间隔玩家速度0 - 999350 帧61000 - 1999440 帧62000 - 3999530 帧64000 以上620 帧6速度不是线性增加而是跳变每到一个分数段玩家明显感觉到「不按加速不行了」这种节奏感比匀速递增更容易产生紧张感。同时你要注意一个物理问题对向车速度变成 6 以后如果帧率掉到 30等于每帧位移变成 12 像素接近车宽的五分之一穿模风险开始冒头。解决方法不是无限加速而是把分数段区间的增速放缓或者把生成间隔的缩短作为主要难度来源。4.3 分数与生命值撞车之后是直接结束还是给一次机会加分逻辑看似简单但有一个细节决定游戏的宽容度撞车后立即结束还是扣一条命后短暂无敌。立即结束放大了碰撞框的误差问题——因为玩家会觉得「就差一点也没撞到」所以我把碰撞框缩了一圈。而带生命值的版本会在碰撞触发时让两辆车各自移开一段距离同时给玩家 1 到 2 秒的无效帧。这份源码的默认行为是立即结束并展示总分如果你想改成三条命模式改动点集中在碰撞检测分支里把game_over True换成lives - 1加一个重置位置的函数。5. 复现路上的五个坑贴图黑框、误判碰撞与窗口卡死的排查记录5.1 贴图黑框背景色没去掉现象加载 PNG 车辆图片后屏幕上出现了黑色矩形边框把车的轮廓整个框住了。原因图片本身是 JPG 或有背景色的 PNGpygame 的image.load()只是把图片整个加载进来不会自动把某个颜色变成透明。解决准备背景为纯色并调用set_colorkey()指定透明色。注意图片四角如果带有抗锯齿渐变色抠色会残留一圈灰边。我一般会先用在线工具把图片背景统一改成纯绿色或纯白色再用set_colorkey((0, 255, 0))抠掉。5.2 还没碰到就判定游戏结束现象玩家的车与对向车之间明明还有一两个像素的间隙屏幕上却已经弹出 Game Over。原因直接用player_rect和enemy_rect做碰撞但图片矩形比实际车辆轮廓大尤其是车头前保险杠区域的透明留白被算进了碰撞范围。解决给两辆车的碰撞矩形都调用inflate缩小一圈再检测缩小后的矩形是否相交。建议把缩小的数值暴露在 config 里比如HITBOX_SHRINK_X 12方便反复试手感。5.3 窗口一启动就卡死或者点关闭没反应现象程序运行后窗口一直转圈鼠标点关闭按钮没响应偶尔出现黑屏窗口。原因常见的有两种。一种是主循环里缺少pygame.display.flip()绘制的内容没有真正提交到窗口另一种是pygame.QUIT事件处理不完整退出后没有调用sys.exit()进程还挂在后台。解决确保主循环里阶段三以pygame.display.flip()收尾退出处理写成pygame.quit(); sys.exit()两行连用。如果事件循环里有多层判断不要把pygame.quit()放在循环外否则事件没拿到程序就一直卡着。5.4 对向车两辆叠在一起生成现象一个车道上出现了上下两辆几乎重叠的车玩家根本来不及分辨路线。原因生成新对向车时只检查了 x 坐标是否在车道内没有检查当前车道上已存在车辆的位置导致第二辆车在 y 坐标还很小刚生成区域时就再次被创建。解决在生成函数里维护每辆车的列表新生成的矩形先和历史车辆逐一做colliderect判断重叠则换一条车道重试。更简单的方式是限制同一车道的生成频率记录该车道上次生成时的帧序号与当前帧序号之差小于最小间隔时不生成。5.5 分数涨得太快或者完全不涨现象对向车还没完全划过屏幕底部就消失但分数没有增加另一种情况是分数每帧加 10一秒涨 600 分难度曲线完全失效。原因分数累加的位置放错了。如果写在主循环的全局更新里而不是「车辆成功通过玩家」时才加分就会每帧触发。而如果移除对向车的条件写的是「y 屏幕高度」但碰撞检测先把它删了分数逻辑根本执行不到。解决把加分放在enemy的更新函数里用if enemy_rect.top screen_height:判断通过完成后加分并移除该车。同时把加分行放在碰撞检测之前用一个布尔变量记录当前帧是否已经加分避免一辆车通过时被重复计数。6. 交付前的最后一公里调试碰撞框、打包 exe 和给朋友试玩代码写完能跑只是第一步真正让这个赛车小游戏「能给别人玩」的往往是最后那几项看起来不起眼的收尾工作。我最常用的调试手段是画碰撞框。在绘制阶段把每辆车的碰撞矩形用描边方式显示出来能直观看到判定的实际区域# 调试模式把玩家和对向车的碰撞框画出来 if DEBUG: pygame.draw.rect(screen, (0, 255, 0), player_hitbox, 2) for enemy_hitbox in enemy_hitboxes: pygame.draw.rect(screen, (255, 0, 0), enemy_hitbox, 2)绿色的框是玩家实际判定范围红色是对向车的。如果红框和绿框还没接触就弹出了结算画面说明代码里拿来做碰撞的矩形和我调试时画出来的不一致八成是洁取了rect而不是hitbox。这个 2 像素宽的描边就是你的「透视眼」一眼看穿所有判定错觉。给朋友试玩的时候对方电脑上不一定装了 Python 和 pygame这时候可以用 PyInstaller 打成单个 exe 文件pip install pyinstaller pyinstaller -F -w main.py-F表示打包成单个文件-w表示运行时关闭控制台窗口避免游戏启动时弹出一个黑色命令框。第一次打包出来的文件可能比较大这很正常pygame 的资源会被一起塞进去。如果打包后图片加载失败检查一下代码里的图片路径是否用了相对路径且图片和 main.py 在同一目录。打包完成后exe 文件放在任意没装 Python 的电脑上都能跑这才是「交付」。从那以后我每次改速度或者碰撞参数都强制自己先把 DEBUG 模式打开跑一遍确认碰撞框和视觉位置对得上再关掉调试状态去玩。这个习惯帮我避开了至少十次「看起来没问题但一玩就穿模」的翻车。这份源码是个很好的练手起点坑我都替你踩过了希望帮到你。本文还有配套的精品资源点击获取
返回列表