☰
Pygame乒乓球游戏开发实战:从游戏循环到碰撞检测的完整指南
2026/10/12 6:03:19 网站建设 项目流程

简介:游戏循环是几乎所有实时游戏的心跳,它决定了每一帧里输入、更新与渲染的执行顺序。碰撞检测则负责回答“物体是否重叠”这个基本问题,而引擎中那些微妙的物理手感,往往源于对碰撞响应和状态管理的精细控制。乒乓球游戏恰好是这些底层概念的最小载体:两块球拍、一个小球,却涵盖了按键状态读取、反弹角度映射、速度递增、回合状态机以及帧率稳定性等关键工程点。无论是Python初学者寻找第一个动手项目,还是开发者想验证自己对渲染循环和游戏架构的理解,用Pygame手写一个乒乓球游戏都是性价比极高的训练路径。本文从技术选型与最小骨架出发,逐步拆解反弹角度算法、碰撞位置修正、计分状态与常见坑点,最终交付一份可运行的完整代码和验收清单,帮助你将代码基本功落到实打实的游戏手感上。

1. 一块板一个球,为什么「乒乓球游戏」最考验代码基本功

一个看起来只是两块板子和一个球来回弹的「乒乓球游戏」,恰恰是最容易暴露代码基本功的项目。很多人在教程里跟着画了窗口、画了小球,程序也能跑,却说不清球为什么会在某个角度越弹越快、球拍为什么会把球吞掉、帧率一波动整个游戏手感就崩。这些不是玄学,是游戏循环、碰撞响应和状态管理没有立住。这篇笔记我想把手写一个 Pygame 乒乓球游戏的完整路径拆开:从选型和最小骨架,到反弹角度、计分状态,再到五个必踩的坑,最后给出一份能照着验收的清单。适合刚学完 Python 基础、正想找第一个动手项目的人,也适合想快速验证一套渲染循环设计的人。

2. 从零搭乒乓球游戏:先选路线,再跑通 60 行骨架

2.1 两条路线怎么选:Pygame 与 HTML5 Canvas 的取舍

一说到做乒乓球游戏,大部分人第一反应是 Pygame,也有人建议直接用 HTML5 Canvas。两条路线都能做出能玩的作品,但上手顺序和分发方式差别很大。我建议先想清楚你要把它放在哪里跑,再来决定用哪套技术。

对比项PygameHTML5 Canvas
运行环境本地 Python 解释器浏览器
分发方式打包成独立可执行文件网页 URL 即开即用
学习成本只需要 Python 基础需要同时处理 HTML、JS 与事件模型
主循环自己写 while 循环,过程直观靠 requestAnimationFrame,循环被浏览器接管
适合场景单机小游戏、练手项目、想理解游戏引擎原理想分享给朋友即点即玩、做 Web 集成

我一般会让刚开始写游戏的人走 Pygame 这条路。原因不是 Canvas 差,而是 Pygame 把游戏循环暴露得足够清楚:每一帧里你要自己决定收事件、更新状态、画画面这三件事的顺序。乒乓球游戏正好是这三个步骤的最小载体,跑通一遍就理解了所谓游戏引擎到底在做什么,之后换 Unity 或者 Godot 再看主循环都是同一套逻辑。

2.2 用 Pygame 跑通最小骨架:窗口、主循环与两个矩形

先别急着加计分和 AI,第一步是让窗口能弹出来、球拍能被画出来、关掉窗口程序能退出。这一步跑通,后面的所有逻辑都有了载体。以下是一份可以直接保存运行的骨架:

import sys import pygame WIDTH, HEIGHT = 800, 600 FPS = 60 pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("乒乓球游戏") clock = pygame.time.Clock() PADDLE_W, PADDLE_H = 12, 90 left_paddle = pygame.Rect(40, HEIGHT // 2 - PADDLE_H // 2, PADDLE_W, PADDLE_H) BALL_SIZE = 16 ball = pygame.Rect(WIDTH // 2 - BALL_SIZE // 2, HEIGHT // 2 - BALL_SIZE // 2, BALL_SIZE, BALL_SIZE) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() screen.fill((20, 24, 34)) pygame.draw.rect(screen, (220, 220, 220), left_paddle) pygame.draw.rect(screen, (220, 220, 220), ball) pygame.display.flip() clock.tick(FPS)

这段代码的每一行都在为一个目标服务:把画面稳定地保持在 60 帧。pygame.Rect(left, top, width, height)接受四个参数,分别代表左上角 x、左上角 y、宽度和高度,后面做碰撞检测时 Rect 自带的方法会帮你省掉大量手写比较。screen.fill用 RGB 元组清空整块画布,不清理的话上一帧的残影会留在窗口里。pygame.display.flip()是双缓冲的提交动作,把所有绘制结果一次性推到屏幕,放在帧末调用。clock.tick(FPS)的作用是让循环尽量每秒只跑 60 次,同时返回上次调用至今经过的毫秒数,这个返回值后面调手感时有大用。

这里有个新手经常遇到的现象:窗口一闪而过然后命令行报错退出。原因通常是忘了写事件循环直接画图,或者pygame.quit()之后没有sys.exit(),导致窗口虽然关了但进程还挂着。把事件循环放在 while 开头、把退出逻辑写干净,是后面所有调试的基础。

3. 把乒乓球游戏做「活」:按键、反弹角度与球速递增

3.1 用按键状态而不是事件来移动球拍:WASD 与方向键映射

骨架跑通之后,第一件要做的事是让球拍响应键盘。很多人第一次写用的是KEYDOWN事件,在事件里给球拍坐标加一个固定值,结果发现按住按键时球拍只动一下。原因很简单:键盘按下事件只在按下那一刻触发一次,不按住连发。对于菜单跳转场景这种设计是对的,但球拍移动需要的是持续读取当前按键状态。

MOVE_SPEED = 6 keys = pygame.key.get_pressed() if keys[pygame.K_w]: left_paddle.y -= MOVE_SPEED if keys[pygame.K_s]: left_paddle.y += MOVE_SPEED

pygame.key.get_pressed()返回一整张键盘状态表,每一帧你查这张表才知道哪些键现在被按着。这段代码要放在主循环的更新阶段,也就是清空事件、画画之前。把左球拍绑在 W/S,右球拍绑在方向键上,双人同键盘对战是这个游戏的传统布局。

参数上,MOVE_SPEED = 6是每帧位移的像素数。在 60FPS 下相当于每秒移动 360 像素,对于 800x600 的场地,球拍从中央走到边界需要大约 0.8 秒,这个速度对双人反应是合适的。想要更快的比赛节奏就调到 7 或 8,但不要低于 5,否则追不上后面要加的球速。

3.2 反弹角度按球拍落点计算:把 0 到 60 度映射成可控手感

乒乓球游戏的核心手感,全在球碰到球拍那一瞬间的反弹角度上。最偷懒的写法是ball_vx = -ball_vx,球碰拍后水平方向翻转。这种做法能让游戏跑起来,但玩不了两分钟就会觉得无聊:球的轨迹永远只有固定的几个夹角,玩家只要站到中间上下移动就够了。真实乒乓球不是这么弹的,球拍的不同部位决定球飞向哪个方向。

常见做法是:计算球与球拍中心的偏移比例,再把这个比例映射到角度区间。球拍中心附近击球,球水平弹回;越靠近球拍上下边缘,弹射角度越陡。用代码说清楚这件事:

import math MAX_ANGLE = 60 # 最大反弹角度,单位是度 def bounce_off_paddle(ball_vx, ball_vy, ball_centery, paddle_centery, paddle_h): offset = (ball_centery - paddle_centery) / (paddle_h / 2) offset = max(-1.0, min(1.0, offset)) angle = math.radians(MAX_ANGLE) * offset speed = math.hypot(ball_vx, ball_vy) new_vx = speed * math.cos(angle) new_vy = speed * math.sin(angle) return abs(new_vx), new_vy

这段代码做了三件事:先把球的中心相对球拍中心的偏移换算成 -1 到 1 之间的比例,max和min的作用是裁剪越界值;再把比例乘到 60 度上得到反弹角度;最后用当前速度的模长和角度重新计算水平和竖直分量。注意这里没有直接取反原来的ball_vx,而是用余弦重新生成,因为你要保证球碰到左拍以后一定是向右飞,碰到右拍呢,返回时在外面包一层方向处理。

MAX_ANGLE = 60是一个值得反复试的参数。角度越小,游戏越像打壁球;角度越大,球越过对手的弧度越夸张。60 度是这个体量的游戏里比较平衡的值,球不至于完全横向飘走,也不会让反弹轨迹过于平直。

3.3 球速递增与回合重置:让难度曲线可见

乒乓球游戏的对局乐趣来自动态难度:回合持续越久,球越快,玩家的反应压力越大。这个递增不能直接写成ball_vx *= 1.01,指数增长会在几秒内把速度推到无法跟上的地步。更可控的做法是线性递增,并且设一个硬上限。

BASE_BALL_SPEED = 5 SPEED_STEP = 0.4 MAX_BALL_SPEED = 12 hit_count = 0 def current_speed(): return min(BASE_BALL_SPEED + hit_count * SPEED_STEP, MAX_BALL_SPEED)

hit_count记录这个回合里球被球拍击中的次数,SPEED_STEP = 0.4让每次击球提升 0.4 个像素每帧。连续对打 10 个来回,球速从 5 提高到 9,玩家能明显感到压力在上升,但节奏还在掌控范围内。MAX_BALL_SPEED = 12是兜底,防止回合变成纯粹的反应速度测试。

球出界后要重置回合:球回到场地中心,方向交给输掉这一分的选手发球方向。重置时把hit_count归零,速度也回到基础值,否则会出现下一回合开局球就快得离谱的情况。发球方向我习惯用一个serve_dir变量控制,左方得分后向右发,右方得分后向左发,交替进行,和真实乒乓球换发制的思路一致。

4. 计分、回合状态机与画面文字渲染

4.1 把回合拆成状态机:SERVE 与 PLAY 的落地写法

没有状态机的乒乓球游戏会出现一个常见怪象:球刚刚飞出边界,下一瞬间就出现在场地中央继续弹,玩家根本来不及看清比分。这是因为所有逻辑都在同一帧里执行完了,显示层没有机会把结果呈现出来。解决办法是给游戏加一个最简状态机。

SERVE, PLAY = 0, 1 state = SERVE # 主循环里的逻辑分支 if state == SERVE: # 等玩家按空格,球停在场地中心 pass elif state == PLAY: # 球移动、碰撞、出界检测 pass

状态机不需要一开始就做得多精细。对乒乓球游戏来说,SERVE 和 PLAY 两个状态就能覆盖完整对局流程:球出界后立刻切到 SERVE,球停在中心显示比分,玩家按下空格再进入 PLAY。后续想做的球权提示、回合间停顿、先到 11 分获胜,都是在这个状态机上加分支的事。不要在一开始就把状态拆得太细,状态越多,每个分支的边界条件就越难排查,先把最小闭环跑顺。

4.2 用 pygame.font 渲染比分与发球提示

比分是字面意义上的“画面文字”,Pygame 里渲染文字需要走字体模块:先创建一个字体对象,再调用render生成一张带透明通道的 Surface,最后用blit贴到主画布上。

font = pygame.font.SysFont(None, 48) score_surface = font.render(f"{score_left} {score_right}", True, (200, 200, 200)) screen.blit(score_surface, (WIDTH // 2 - score_surface.get_width() // 2, 18))

第一个参数写None表示用系统默认字体,这是最不容易翻车的写法;如果你指定一个本机不存在的字体名,程序会直接报错。第二个参数True表示开启抗锯齿,画面边缘会平滑很多。每次分数变化才需要重新render,但在这种规模的小项目里每帧渲染一次也完全跑得动,不用过度优化。需要显示中文提示时要注意:系统默认字体不一定包含中文字形,渲染出来可能是方块,所以发球提示这类短文本我用英文,既能保证显示正常,也避免字体文件分发的麻烦。

4.3 中线、边框与中心圆:装饰层怎么画不抢戏

乒乓球游戏的可读性比画质重要。一条中线、一个中心圆、两侧边界,玩家扫一眼就能判断球的方位和出界与否。装饰层的绘制顺序有讲究:先填背景色,再画静态装饰,然后画球拍和球,最后叠加文字层。这样保证文字永远在最上面,不会被球遮住。

# 背景 screen.fill((20, 24, 34)) # 装饰层 pygame.draw.line(screen, (80, 90, 110), (WIDTH // 2, 0), (WIDTH // 2, HEIGHT), 2) pygame.draw.circle(screen, (80, 90, 110), (WIDTH // 2, HEIGHT // 2), 48, 2) # 球拍和球 pygame.draw.rect(screen, (200, 200, 200), left_paddle) pygame.draw.rect(screen, (200, 200, 200), ball)

draw.circle的最后一个参数 2 表示圆环宽度,不填这个参数画出来的是实心圆。中线的颜色我用偏灰的蓝灰色,和背景的深色区分开,但又不至于像纯白那样刺眼。这种细节属于画面风格的一部分,颜色值随时可以改,重点是记住图层顺序,画面上所有元素都应该在同一帧内完成绘制,不能一个在 fill 之前一个在之后。

4.4 把前几段拼成一份完整可运行的 main.py

前面拆开的逻辑,合在一起就是一份可以直接运行的完整程序。我把它整理成一份带注释的完整版本,你可以对照前几节的说明逐段看,也可以直接存成 main.py 跑起来。

import math import sys import pygame WIDTH, HEIGHT = 800, 600 FPS = 60 PADDLE_W, PADDLE_H = 12, 90 BALL_SIZE = 16 MOVE_SPEED = 6 MAX_ANGLE = 60 BASE_BALL_SPEED = 5 SPEED_STEP = 0.4 MAX_BALL_SPEED = 12 SERVE, PLAY = 0, 1 pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("乒乓球游戏") clock = pygame.time.Clock() font = pygame.font.SysFont(None, 48) left_paddle = pygame.Rect(40, HEIGHT // 2 - PADDLE_H // 2, PADDLE_W, PADDLE_H) right_paddle = pygame.Rect(WIDTH - 40 - PADDLE_W, HEIGHT // 2 - PADDLE_H // 2, PADDLE_W, PADDLE_H) ball = pygame.Rect(WIDTH // 2 - BALL_SIZE // 2, HEIGHT // 2 - BALL_SIZE // 2, BALL_SIZE, BALL_SIZE) score_left = 0 score_right = 0 state = SERVE serve_dir = 1 hit_count = 0 ball_vx = 0.0 ball_vy = 0.0 def reset_ball(direction): global hit_count, state, ball_vx, ball_vy ball.center = (WIDTH // 2, HEIGHT // 2) hit_count = 0 state = SERVE angle = math.radians(6) ball_vx = direction * BASE_BALL_SPEED * math.cos(angle) ball_vy = BASE_BALL_SPEED * math.sin(angle) def launch_ball(direction): global ball_vx, ball_vy, state speed = min(BASE_BALL_SPEED + hit_count * SPEED_STEP, MAX_BALL_SPEED) angle = math.radians(6) ball_vx = direction * speed * math.cos(angle) ball_vy = speed * math.sin(angle) state = PLAY reset_ball(serve_dir) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.KEYDOWN and state == SERVE: if event.key == pygame.K_SPACE: launch_ball(serve_dir) keys = pygame.key.get_pressed() if keys[pygame.K_w]: left_paddle.y -= MOVE_SPEED if keys[pygame.K_s]: left_paddle.y += MOVE_SPEED if keys[pygame.K_UP]: right_paddle.y -= MOVE_SPEED if keys[pygame.K_DOWN]: right_paddle.y += MOVE_SPEED left_paddle.y = max(0, min(HEIGHT - PADDLE_H, left_paddle.y)) right_paddle.y = max(0, min(HEIGHT - PADDLE_H, right_paddle.y)) if state == PLAY: ball.x += ball_vx ball.y += ball_vy if ball.top <= 0 or ball.bottom >= HEIGHT: ball_vy = -ball_vy ball.top = max(0, min(HEIGHT - BALL_SIZE, ball.top)) if ball_vx < 0 and ball.colliderect(left_paddle): offset = (ball.centery - left_paddle.centery) / (PADDLE_H / 2) offset = max(-1.0, min(1.0, offset)) angle = math.radians(MAX_ANGLE) * offset speed = math.hypot(ball_vx, ball_vy) speed = min(speed + SPEED_STEP, MAX_BALL_SPEED) ball_vx = abs(speed * math.cos(angle)) ball_vy = speed * math.sin(angle) ball.left = left_paddle.right hit_count += 1 if ball_vx > 0 and ball.colliderect(right_paddle): offset = (ball.centery - right_paddle.centery) / (PADDLE_H / 2) offset = max(-1.0, min(1.0, offset)) angle = math.radians(MAX_ANGLE) * offset speed = math.hypot(ball_vx, ball_vy) speed = min(speed + SPEED_STEP, MAX_BALL_SPEED) ball_vx = -abs(speed * math.cos(angle)) ball_vy = speed * math.sin(angle) ball.right = right_paddle.left hit_count += 1 if ball.left <= 0: score_right += 1 serve_dir = -1 reset_ball(serve_dir) if ball.right >= WIDTH: score_left += 1 serve_dir = 1 reset_ball(serve_dir) screen.fill((20, 24, 34)) pygame.draw.line(screen, (80, 90, 110), (WIDTH // 2, 0), (WIDTH // 2, HEIGHT), 2) pygame.draw.circle(screen, (80, 90, 110), (WIDTH // 2, HEIGHT // 2), 48, 2) pygame.draw.rect(screen, (200, 200, 200), left_paddle) pygame.draw.rect(screen, (200, 200, 200), right_paddle) pygame.draw.rect(screen, (230, 230, 230), ball) score_surface = font.render(f"{score_left} {score_right}", True, (200, 200, 200)) screen.blit(score_surface, (WIDTH // 2 - score_surface.get_width() // 2, 18)) if state == SERVE: tip_surface = font.render("Press SPACE to serve", True, (160, 170, 190)) screen.blit(tip_surface, (WIDTH // 2 - tip_surface.get_width() // 2, HEIGHT // 2 + 60)) pygame.display.flip() clock.tick(FPS)

运行方式很简单:安装好 pygame 依赖后,在命令行执行python main.py,窗口弹出来按空格发球,W/S 控制左拍,方向键控制右拍。这份代码把前面讲的所有参数都集中在文件顶部的常量区,想调手感就改那些常量,不用深入逻辑内部。需要特别注意的地方有两处:一是球的碰撞分支用ball_vx的正负判断它朝哪个方向飞,这是防止同一帧内左右拍同时触发碰撞的关键;二是每次碰撞修正位置时,ball.left = left_paddle.right这类操作要把球推出接触面,否则下一帧还会继续判定碰撞。

5. 乒乓球游戏翻车实录:5 个必踩的坑与排查方法

5.1 球在球拍边缘反复抖动:碰撞响应顺序错了

现象:球碰到球拍后没有被弹走,而是在球拍边缘高频闪烁抖动,速度明显变慢。

原因:碰撞检测通过colliderect触发后,你只翻转了速度方向,没有把球移出球拍矩形。下一帧开始时球还在球拍内部,colliderect返回值依然是真,于是速度方向又被翻转回去,形成振铃。

解决:翻转速度之前,先按碰撞方向修正位置。球向左飞撞到左拍,就把ball.left设为left_paddle.right,让球完全落到球拍右侧;撞右拍则把ball.right设为right_paddle.left。位置修正和速度翻转必须放在同一个分支里,顺序不能颠倒。

5.2 球速越来越快或突然卡死:速度变量的作用域被谁改了

现象:打着打着球快得只剩残影,或者某一刻球垂直上下飞永不落地。

原因:速度递增被写进了每帧更新里,比如ball_vx *= 1.01,指数增长很快冲破上限;另一个常见问题是多处代码同时修改ball_vy,比如发球函数和碰撞函数互不知道对方的存在,导致某个分量被意外置零。

解决:把速度状态收拢到一个函数里管理。碰撞时用局部变量计算出完整的新速度向量再统一赋值,发球时只调用launch_ball,不让其他函数直接改写全局速度。排查时可以打印关键帧的ball_vx和ball_vy,看是哪个分支把数值改坏的。

5.3 窗口一拖动球就变速:帧率与位移量的关系没理清

现象:拖动窗口或切走再切回时,球明显突然加速或减速,手感断裂。

原因:所有位移都写成每帧固定像素,但clock.tick(FPS)只是尽力维持帧率,系统负载高时每帧实际耗时超过 16.7ms。帧率降到 30 帧时,球一秒只移动了一半距离,看起来就像卡顿。

解决:要么接受固定 FPS 的限制,用clock.tick(FPS)锁帧并避免后台运行;要么引入 delta time,把位移从每帧像素改成每秒像素乘时间。后者的写法是把MOVE_SPEED重新定义为像素每秒,然后位移量乘上dt / 16.667等比换算。对乒乓球游戏来说,锁帧是更优先的做法,逻辑简单且手感一致。

5.4 切出去再切回来,球拍自己往上跑:KEYUP 丢失

现象:游戏运行中切换窗口,回来后球拍自动向一个方向移动停不下来。

原因:用事件队列维护按下的键集合时,窗口失焦期间的KEYUP事件被系统吞掉了,程序还认为那个键被按着。pygame.key.get_pressed()读取的是实时键盘状态,不会出现这个问题,但如果你用了事件收集法,就必须处理失焦场景。

解决:监听pygame.WINDOWFOCUSLOST事件,在事件里把维护的按键集合清空。如果完全改用get_pressed()做移动判断,可以绕开这个问题,代价是你无法区分“按住”和“按下”这两个语义,后续加组合键功能时再换回事件法。

5.5 球拍按到底再松开,拍子被墙切掉一半:坐标越界没兜底

现象:球拍一直移动到窗口顶边,松开按键后拍子在边界线上被切掉一部分,看起来像半个拍子悬浮在上边缘。

原因:球拍的 y 坐标被键位持续修改,没有限制范围。pygame.Rect允许坐标超出窗口,绘制时超出部分会被裁掉,于是出现拍子残缺的现象。

解决:在每次更新完位置后,用max和min把 y 值夹在合法范围内。正确写法是left_paddle.y = max(0, min(HEIGHT - PADDLE_H, left_paddle.y)),下限是 0,上限是窗口高度减去拍子高度,这样拍子下边缘永远不会越过窗口底边。

6. 把乒乓球游戏升级成能见人的作品:验证、AI 与打包

6.1 十分钟验收清单:不靠感觉判断有没有写坏

改完参数之后不是靠手玩几把就说“没问题”,而是按清单逐项过一遍。我最常用来验收乒乓球游戏的检查项如下:

检查项操作期望结果
上下墙反弹让球以接近垂直角度撞墙球不穿墙、不抖动
球拍边界球拍按到底再松开拍子完整停在边缘
按键手感按住 W 不放拍子连续上移且不抖动
碰撞角度从拍子边缘位置击球反弹角度接近 60 度
比分更新让球飞出左边界右方比分加 1 且球回到中心
窗口失焦切走再切回球拍不发生漂移
速度上限连续击打 20 次以上球速不再增长

每项都是手工可测的,不需要额外的测试框架。发现哪一项不对,直接定位到对应章节的代码段排查。

6.2 低成本进阶:AI 对手、暂停、把比分写进文件

双人打法验证完毕之后,加一个 AI 对手是性价比最高的进阶。AI 的实现不需要复杂算法,让右拍朝球的 y 位置移动,但移动速度限制得比玩家低一些,再给目标位置加一点随机偏置,对手就不会显得像机器人一样精确:

AI_SPEED = 4 AI_MISTAKE = 30 target_y = ball.centery + random.randint(-AI_MISTAKE, AI_MISTAKE) if right_paddle.centery < target_y: right_paddle.y += min(AI_SPEED, target_y - right_paddle.centery) elif right_paddle.centery > target_y: right_paddle.y -= min(AI_SPEED, right_paddle.centery - target_y)

AI_SPEED = 4低于玩家的MOVE_SPEED = 6,保证玩家始终有机会赢。AI_MISTAKE的值越大,AI 越像新手。暂停功能只要加一个布尔变量,在更新逻辑前判断是否跳过状态更新即可。比分落盘用最简单的方式:每得一分就追加写入本地文件,open("scores.txt", "a")一行搞定,比引入数据库这种重型方案实际得多。

6.3 打包成独立可执行文件:字体与资源的坑

作品要给别人玩,打包是躲不开的一步。常见做法是用 pyinstaller 打单文件包,命令行执行pyinstaller --onefile --noconsole main.py。这里最大的坑是资源路径:打包后的程序运行时工作目录不一定是程序所在目录,直接读取相对路径会找不到文件。资源加载要写成兼容普通运行和打包运行两种状态:

import os import sys base_path = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(__file__)

如果是加载外部字体文件,用os.path.join(base_path, "assets", "font.ttf")拼出完整路径,再把字体文件和 exe 放在一起分发。系统默认字体虽然在目标机器上大概率存在,但不同系统渲染出的字形不一致,对成品要求高的话,随程序带一个开源字体文件是最省心的方案。

我一直把“先验证手感,再谈画面”当作做小游戏的顺序。最早我写乒乓球游戏时直接把反弹做成水平翻转,结果玩法十分钟就让人厌倦。后来才明白,这个项目表面上练的是语法,实际上练的是对玩家感受的建模:球速、角度、反馈,每一项都是可以调出来的。现在每做完一个版本,我都会开着帧率显示打几局,确认手感稳定再考虑加功能。写完记得把这份清单留好,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询