1. 做这个转盘之前,先别急着写 pygame 代码
如果你接到一个“写个运气类小游戏”的需求,或者学完 Python 基础想找个有画面感的练习项目,用 pygame 一步步实现一个可视化的幸运大转盘,是我很推荐的方向。转盘这东西表面看着就是个“会转的圆”,真正拆开以后,它牵扯到图形绘制、角度计算、动画状态切换、结果判定这一整套逻辑,是一个能把基本功和工程意识一起练到的小项目。
这个项目最终能做什么呢?运行起来就是一个完整的抽奖程序:屏幕上有一个划分成多个奖项扇区的圆盘,点击“开始抽奖”按钮,转盘开始旋转并逐渐减速,最后停在某个扇区上,程序告诉你中了哪一项。整个过程全部可视化,代码量不大,但看完这篇以后你能带走的不只是一份能跑的代码,还有一套“以后遇到类似小游戏需求都适用的拆解方法”。
适合谁来读呢?有两种人:一是刚学完 Python 语法、想通过项目巩固 pygame 和基础数学知识的初学者;二是活动运营、宣传展示等相关场景里需要快速做出一个可演示抽奖工具的人。前者能在这里看到每一步为什么这么写,后者能直接拿到完整脚本改改参数就上线。我会按自己实际开发的顺序来讲,从空白窗口一直写到完整版本,途中所有容易翻车的地方都会单独拎出来说。
1.1 一张转盘拆开看:核心组成到底有哪些
很多初学者上来就盯着“怎么画圆”这一点,这恰恰是思路最容易跑偏的地方。我习惯先把东西拆成功能块:
- 盘体:一个圆形的底板,决定整个转盘的视觉主体。
- 扇区:被均分的若干块扇形区域,每块对应一个奖项,是转盘的信息载体。
- 标签文字:每个扇区内的奖品名称,需要跟随扇区角度排布。
- 指针:固定在某个方向(通常是正上方),用来指向最终停留的扇区。
- 抽奖按钮:触发旋转的交互入口。
- 旋转动画:从高速转动到逐渐减速停止的过程。
- 结果判定:根据最终角度算出指针落在哪个扇区,并反馈结果。
这八个部分里,前面五个偏“界面”,后面三个偏“逻辑”。我写这种小项目时习惯先做完静态界面、再做动画、最后做判定,因为判定依赖角度,角度依赖绘制顺序,顺序反了你后面会改得很难受。
1.2 为什么选 pygame 而不是其他方案
做可视化转盘其实有三类常见路线:网页方向用 CSS 动画或者 Canvas;桌面方向用 Tkinter;游戏方向就是 pygame。我选 pygame 的原因很直接:它的事件循环机制天然适合做“持续刷帧 + 鼠标交互”这种实时程序,绘图 API 也足够底层,你能清楚地看到每一帧做了什么,对理解动画本质非常有帮助。
另外一点是 python 生态的连贯性。Tkinter 也能画圆,但它的动画刷新方式比较别扭,很多时候要靠after()去模拟帧循环,写起来绕。pygame 的逻辑则非常直观:每一帧清屏、绘制、翻页,这种模型一旦理解,以后做任何小游戏都通用。这个项目里我们用的是 pygame 最核心的那几个 API——绘制多边形、绘制圆形、字体渲染、图片旋转、事件监听,覆盖面广但都不复杂,练完等于把 pygame 的地基打了一遍。
2. 动笔之前:坐标系和角度是最容易翻车的地方
写转盘真正难的不是 pygame 的绘图函数,而是你脑子里的“几何直觉”和屏幕上的“实际坐标”对不上。这一块你要是啃下来,后面写代码会非常顺;啃不下来,程序跑出来你会发现扇区顺序乱七八糟、指针指向不对,最后只能靠乱试碰运气。
2.1 屏幕坐标系跟数学课上的坐标系不一样
我们上学时画的坐标系,原点在中间,y 轴向上,角度逆时针方向为正。pygame 不一样:原点在屏幕左上角,x 轴向右,y 轴向下。这意味着你用math.cos和math.sin去算圆上的点时,角度是“顺时针方向”增长的,而不是数学里熟悉的“逆时针”。
这个差异对转盘来说反而是件好事。转盘的扇区本来就是顺时针排列的(从右侧 3 点钟方向开始,一路转到 12 点、9 点、6 点),所以你只需要把“顺时针为正”的坐标系直接当作基础,画出来的扇区顺序就是对的。
还有一个容易懵的点:在 pygame 的坐标体系下,12 点方向对应多少度?很多人的直觉是 0 度或者 90 度,实际上应该是 270 度(也就是 -90 度)。这个数字后面做结果判定时要直接用,先记下来。我用一个生活类比帮你记住:你把钟表平放在桌上,3 点方向是 0 度,顺时针往下转,6 点是 90 度,9 点是 180 度,12 点就是 270 度。整个转盘的指针我们就固定放在 270 度这个方向。
2.2 扇形怎么画:用多边形逼近圆弧
pygame 里没有直接画扇形的函数。它提供的pygame.draw.polygon可以画任意多边形,所以经典做法是:把圆心作为第一个点,然后沿圆弧每隔一个小角度采样一个点,把这一系列点连起来,最后再回到圆心。这样画出来的图形就是一个“圆润程度可控”的扇形。
采样步长很关键。我一般用 2 度:也就是从扇区起始角到结束角,每 2 度取一个圆周点,然后把圆心和这些点一起丢给polygon。步长如果太大,比如一步跨 10 度,扇区边缘会有明显锯齿;步长太小又没必要,比如 1 步 0.5 度,八个扇区加起来要多算几千个点,白白增加开销。实际上对一个半径 200~300 像素的转盘来说,2 度步长视觉上已经非常平滑。
2.3 角度取模:负数才是真正的坑
转盘转起来之后,角度会一直累加,可能超过 360,也可能在某种计算里变成负数。Python 的%运算符对负数会返回正余数,比如-30 % 360的结果是 330,这个特性在角度处理里特别好用,可以放心用% 360把角度归一化到 0~359。
真正容易犯的错是在“反推指针落在哪个扇区”的时候把除数方向搞反。后面我会给出完整公式,这里先提醒一句:结果判定前,先拿纸笔把“指针角度、扇区起始角、当前旋转角”三者的关系画出来,比你在代码里试错快十倍。别笑,我在这个点上真的调试过一小时,最后发现就是少写了一个括号。
3. 静态盘面一步步画出来
这一节先做“不会转的转盘”。静态画对了,后面所有动态逻辑才有意义。我建议你专门建一个draw_sector函数,不要把所有绘图代码堆在主循环里,因为主循环每帧要执行好几遍,拆成函数以后调试哪儿出了岔子能马上定位。
3.1 初始化窗口和全局参数
这一段很固定,直接抄就行:
import math import random import sys import pygame WIDTH, HEIGHT = 600, 700 FPS = 60 CENTER = (WIDTH // 2, 320) RADIUS = 260 PRIZES = [ "谢谢参与", "五元红包", "一等奖", "十元红包", "二等奖", "再来一次", "三等奖", "小礼品", ] SECTOR = 360 // len(PRIZES) FAN_COLORS = [ (229, 80, 90), (250, 205, 90), (229, 80, 90), (250, 205, 90), (229, 80, 90), (250, 205, 90), (229, 80, 90), (250, 205, 90), ]这里两个细节说一下:一是CENTER的 y 坐标我故意没有设在屏幕正中间,而是偏上,因为下方要留出按钮和结果显示区域;二是SECTOR用360 // len(PRIZES)计算而不是写死 45,这样以后你增删奖项数量,扇区角度会自动跟着变,不会出现 7 个奖项每个 45 度导致转盘空一块的尴尬。
3.2 扇区绘制函数:draw_sector
核心函数就是前面说的“多边形逼近扇形”:
def draw_sector(surface, center, radius, start_deg, end_deg, color): points = [center] for deg in range(start_deg, end_deg + 1, 2): rad = math.radians(deg) points.append(( center[0] + radius * math.cos(rad), center[1] + radius * math.sin(rad), )) points.append(center) pygame.draw.polygon(surface, color, points) pygame.draw.polygon(surface, (255, 255, 255), points, 2)start_deg和end_deg是扇区的起始和结束角度。因为上面说过 pygame 坐标系里角度是顺时针增长的,所以扇区就是从起始角顺时针铺到结束角。每次画完填充色,再描一条白色细边框,扇区之间的分界会特别清楚。
颜色上我用了红黄交替的方案,这种相邻色对比既是幸运大转盘的经典视觉风格,又能让玩家一眼看出扇区边界。你完全可以改成马卡龙色系或者公司主题色,只要保证相邻扇区颜色不同就行。
3.3 文字怎么摆:沿半径方向旋转
奖项文字不能放在圆心,也不能沿水平方向乱摆。合适的做法是放在扇区角平分线上、半径大约 60% 左右的位置,并且让文字“顺着半径方向”排布,也就是字形从圆心向外延伸。这样不管转到哪个位置,文字都跟随扇区一起转动,视觉上最像真实的转盘。
实现分两步。第一步算文字中心位置:
mid = math.radians(start_deg + SECTOR / 2) text_x = CENTER[0] + RADIUS * 0.62 * math.cos(mid) text_y = CENTER[1] + RADIUS * 0.62 * math.sin(mid)第二步是旋转文字。先用font.render渲染出文字图片,再用pygame.transform.rotate旋转。这里有一个我踩过的实际坑:如果不给文字 surface 留透明通道,旋转后空白区域会被黑色填充,屏幕上会出现一堆黑方块。解决办法是font.render时开启抗锯齿并且不传背景色,这样生成的就是带透明通道的 surface:
text_img = font.render(prize, True, (255, 255, 255)) rotated = pygame.transform.rotate(text_img, -(mid_deg) - 90) surface.blit(rotated, rotated.get_rect(center=(text_x, text_y)))旋转角度的公式是-(扇区中心角) - 90。这个公式我当时调试的时候试了好几个符号组合,最终确定下来是能让文字顶部朝外、顺着半径方向阅读的。如果你想把文字改成“顶部朝圆心”的方向,把-90改成+90再加个180就能得到反过来的效果,这个你试一下就明白,不用死记,知道原理就行。
3.4 指针和中心装饰
指针必须固定在屏幕正上方,也就是 270 度方向,在任何情况下都不要让它跟着转盘旋转。画法很简单,画一个三角形,顶点指向转盘边缘内侧:
def draw_pointer(surface): x, y = CENTER tip = (x, y - RADIUS + 24) left = (x - 18, y - RADIUS - 20) right = (x + 18, y - RADIUS - 20) pygame.draw.polygon(surface, (50, 50, 50), [tip, left, right])中心再画一个圆盖住所有扇区的连接点,视觉上会干净很多:
pygame.draw.circle(surface, (255, 255, 255), CENTER, 34) pygame.draw.circle(surface, (50, 50, 50), CENTER, 34, 3) pygame.draw.circle(surface, (50, 50, 50), CENTER, 10)这一步做完,你已经有一个完整的静态转盘了。跑一下代码,确认扇区顺序、颜色、文字方向都正常,再继续往下做动画,这样问题会好定位很多。
4. 让转盘转起来:动画状态与减速模型
静态画面搞定以后,接下来的问题就是:转盘怎么转、怎么停下来、停下来之后怎么处理。这里我会用一个非常朴素的状态机来管理。
4.1 旋转状态管理
转盘在一个游戏循环里只有两种大状态:空闲和旋转中。空闲时可以点击按钮;旋转中要禁止重复点击。实现上我用一个布尔变量spinning来控制:
- 空闲状态:按钮可点击,点击后把
spinning置为True,并给转盘一个初始角速度。 - 旋转状态:每一帧更新角度、衰减速度,直到速度低于阈值,停表并计算结果。
- 完成状态:显示结果文字,
spinning回到False,等待下一次点击。
这个结构看起来简单,但它是所有小游戏程序都通用的“状态机雏形”。如果你以后做更复杂的游戏,只需要把布尔变量换成枚举状态,再加一些状态切换条件,就扩展成真正的状态机了。
4.2 速度衰减模型:为什么乘系数而不是减
比较笨的做法是每帧给速度减去一个固定值,比如speed -= 0.1。这样会有一个问题:速度从 20 减到 0 的过程中,最后几帧明显“滑行”得特别久,观感上很像刹不住车。真实转盘应该是“转动惯性 + 摩擦阻力”,也就是每帧乘以一个小于 1 的系数,速度下降是等比衰减的,快的时候降得多、慢的时候降得少,最后平滑停下来。
我的做法是:
speed *= math.pow(0.97, dt * 60) rotation_angle = (rotation_angle + speed * dt * 60) % 360dt是上一帧到这一帧的时间差,来自clock.tick(FPS)。为什么要用dt归一化?因为如果不做归一化,帧率一变,转盘的旋转速度和衰减速度都会变。比如在 30 帧每秒的机器上,程序会觉得每一帧世界都过去得更久,于是转得飞快。用dt归一化以后,不管在什么帧率下,观感基本一致。
衰减系数0.97是我实测比较舒服的一个值,从初速度 20 左右减到停住大约要两三秒。喜欢转久一点改成0.98,想要快速出结果改成0.95,这个参数你完全可以按需调。
4.3 点击按钮的交互处理
按钮先创建一个矩形区域,然后在事件循环里判断鼠标左键是否落在区域内。为了体验好一点,我还会在鼠标悬停时换一个更深的颜色:
button_rect = pygame.Rect(WIDTH // 2 - 70, 620, 140, 48) # 事件循环中 if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if button_rect.collidepoint(mouse_pos) and not spinning: speed = random.uniform(15, 22) spinning = True result_text = ""初始速度用random.uniform(15, 22)取一个随机值,这样每次转动的时间和最终停留位置都不一样,游戏才有随机性。这里可以说一个我当时犯过的错:最初我把初始速度固定写死成 18,结果每次停下来的位置几乎在同一个区域附近。因为衰减系数固定、初速固定,转过的圈数也固定,结果就是“看似随机,实则每次都差不多”。用了随机初速以后,这个问题立刻消失了。
5. 结果判定:指针到底停在哪个扇区
这是整个项目里数学味道最重、也最容易被忽略的部分。很多人转盘动画做得很好,结果判定却用一堆 if 列表硬凑,代码又臭又长。其实一个公式就能解决。
5.1 从当前角度反推奖品索引
回想我们绘制扇区的顺序:第 i 个扇区的起始角度是i * SECTOR(局部角度),再叠加当前旋转角rotation_angle,就是它在屏幕上的实际位置。指针固定在屏幕 270 度方向(正上方)。
那么要判断指针落在哪个扇区,本质上就是求:指针所在的角度 270 度,对应到转盘未旋转时的局部角度是哪一个,再把这个局部角度除以扇区大小,得到索引。
公式如下:
def get_win_index(rotation_angle): local = (270 - rotation_angle) % 360 return int(local // SECTOR) % len(PRIZES)我解释一下为什么是270 - rotation_angle。屏幕上的实际角度等于局部角度加上旋转角,那么反过来,已知屏幕角度求局部角度,就是屏幕角度减去旋转角。指针的屏幕角度是 270,所以局部角度是270 - rotation_angle。再用% 360归一化到 0~359,最后整除扇区角度就得到索引。因为我画扇区是从 3 点钟方向逆/顺时针排列的,所以这个整除的结果天然对应奖项列表的顺序。
5.2 边界情况和精度问题
有一个边界值得说明:如果指针正好压在两个扇区的分界线上,int(local // SECTOR)会把它归到下一个扇区,也就是角度较大一侧的那个扇区。这个行为其实是符合扇区绘制约定的——扇区的“起始角度”属于当前扇区,而终止角度已经不属于了。所以理论上不会出现“两个扇区都算中奖”的歧义,你也不需要再额外做容错。
但如果你想要更稳妥的判定,可以在公式里加一个半扇区偏移,让判定值指向每个扇区的中心角而不是起始角。比如local = (270 - rotation_angle + SECTOR / 2) % 360,然后再整除。两者都可以,我建议新手用不加偏移的版本,因为和绘制逻辑一一对应,出了 bug 更好查。
6. 完整代码:可以直接复制运行的版本
前面各节的代码片段全是项目里的实际片段,我把它们拼起来就是一个完整的可运行脚本。你新建一个 Python 文件,把下面内容整个复制进去,有 pygame 环境就能直接跑。
import math import random import sys import pygame WIDTH, HEIGHT = 600, 700 FPS = 60 CENTER = (WIDTH // 2, 320) RADIUS = 260 PRIZES = [ "谢谢参与", "五元红包", "一等奖", "十元红包", "二等奖", "再来一次", "三等奖", "小礼品", ] SECTOR = 360 // len(PRIZES) FAN_COLORS = [ (229, 80, 90), (250, 205, 90), (229, 80, 90), (250, 205, 90), (229, 80, 90), (250, 205, 90), (229, 80, 90), (250, 205, 90), ] BG = (240, 240, 248) TEXT_COLOR = (255, 255, 255) BUTTON_IDLE = (52, 152, 219) BUTTON_HOVER = (39, 120, 178) def get_font(size): for name in ["microsoftyahei", "simhei", "pingfangsc", "notosanscjk", "wenquanyimicrohei"]: font = pygame.font.SysFont(name, size) if font: return font return pygame.font.SysFont(None, size) def draw_sector(surface, center, radius, start_deg, end_deg, color): points = [center] for deg in range(start_deg, end_deg + 1, 2): rad = math.radians(deg) points.append(( center[0] + radius * math.cos(rad), center[1] + radius * math.sin(rad), )) points.append(center) pygame.draw.polygon(surface, color, points) pygame.draw.polygon(surface, (255, 255, 255), points, 2) def draw_wheel(surface, rotation_angle): for i, prize in enumerate(PRIZES): start = i * SECTOR + rotation_angle end = start + SECTOR draw_sector(surface, CENTER, RADIUS, start, end, FAN_COLORS[i]) mid_deg = start + SECTOR / 2 mid = math.radians(mid_deg) text_x = CENTER[0] + RADIUS * 0.62 * math.cos(mid) text_y = CENTER[1] + RADIUS * 0.62 * math.sin(mid) font = get_font(24) text_img = font.render(prize, True, TEXT_COLOR) rotated = pygame.transform.rotate(text_img, -mid_deg - 90) surface.blit(rotated, rotated.get_rect(center=(text_x, text_y))) pygame.draw.circle(surface, (255, 255, 255), CENTER, 34) pygame.draw.circle(surface, (50, 50, 50), CENTER, 34, 3) pygame.draw.circle(surface, (50, 50, 50), CENTER, 10) def draw_pointer(surface): x, y = CENTER tip = (x, y - RADIUS + 24) left = (x - 18, y - RADIUS - 20) right = (x + 18, y - RADIUS - 20) pygame.draw.polygon(surface, (50, 50, 50), [tip, left, right]) pygame.draw.circle(surface, (255, 255, 255), (x, y - RADIUS - 4), 5) def draw_button(surface, button_rect, hovered): color = BUTTON_HOVER if hovered else BUTTON_IDLE pygame.draw.rect(surface, color, button_rect, border_radius=8) font = get_font(24) img = font.render("开始抽奖", True, (255, 255, 255)) surface.blit(img, img.get_rect(center=button_rect.center)) def get_win_index(rotation_angle): local = (270 - rotation_angle) % 360 return int(local // SECTOR) % len(PRIZES) def main(): pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("幸运大转盘") clock = pygame.time.Clock() button_rect = pygame.Rect(WIDTH // 2 - 70, 620, 140, 48) rotation_angle = 0.0 speed = 0.0 spinning = False result_text = "" while True: dt = clock.tick(FPS) / 1000.0 mouse_pos = pygame.mouse.get_pos() for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if button_rect.collidepoint(mouse_pos) and not spinning: speed = random.uniform(15, 22) spinning = True result_text = "" screen.fill(BG) hovered = button_rect.collidepoint(mouse_pos) draw_button(screen, button_rect, hovered) draw_wheel(screen, rotation_angle) draw_pointer(screen) if spinning: speed *= math.pow(0.97, dt * 60) rotation_angle = (rotation_angle + speed * dt * 60) % 360 if speed < 0.05: speed = 0.0 spinning = False result_text = "恭喜中得:" + PRIZES[get_win_index(rotation_angle)] if result_text: font = get_font(26) img = font.render(result_text, True, (229, 80, 90)) screen.blit(img, img.get_rect(center=(WIDTH // 2, 580))) pygame.display.flip() if __name__ == "__main__": main()这段代码在保证完整性的同时,我已经把不必要的重复抽取成了函数。get_font这个辅助函数特别注意一下:它尝试一串常见中文字体名,目的就是解决不同操作系统下默认字体不显示中文的问题。如果你在自己电脑上第一次跑,发现中文全是方块,优先去检查这个函数有没有找到合适的字体。
7. 常见问题与排查技巧实录
写这类项目时,代码逻辑之外的问题往往比逻辑问题更折磨人。下面几个问题是我自己在开发中真实碰到的,整理成速查表,你可以直接对照着排。
| 现象 | 常见原因 | 处理办法 |
|---|---|---|
| 中文全部显示成方块 | pygame 默认字体不包含中文字形 | 用pygame.font.SysFont指定系统中文字体名;更稳妥的做法是下载一份 ttf 字体,用pygame.font.Font("字体文件路径", size)加载 |
| 文字旋转后出现黑色方块 | 文字 surface 没有透明通道,旋转时空白区被黑色填充 | font.render时开启抗锯齿且不传背景色;或者用pygame.Surface((w, h), pygame.SRCALPHA)自行创建透明图层 |
| 转盘转动一卡一卡 | 主循环没有锁帧,或者每帧重复创建字体对象 | 主循环最前面加dt = clock.tick(FPS) / 1000.0;把字体对象放到循环外创建 |
| 点击按钮偶尔没反应 | 按钮 rect 在事件处理时还没定义,导致引用报错或判断失效 | 在主循环开始前就定义好button_rect,事件和绘制共用同一个矩形 |
| 指针停在两个扇区之间不知道算谁 | 边界判定存在精度模糊感 | 用local // SECTOR整除,边界天然归入下一个扇区;如果仍不放心,加SECTOR / 2半扇区偏移再整除 |
| 转到后来停特别慢,体验拖沓 | 衰减系数设置得太大,速度衰减太慢 | 把0.97调成0.95,游戏节奏明显变快;反之想要更长的悬念效果就调成0.98 |
| 窗口在高分屏下看起来很糊 | 系统对窗口做了缩放,但 pygame 分辨率没变 | 用pygame.display.set_mode创建窗口后,在系统显示设置里匹配缩放比例;也可以把WIDTH/HEIGHT等比例调大 |
第 5 行的“半扇区偏移”我再多说一句。我的代码里get_win_index用的是整除起始角,逻辑上和绘制完全一致,一般不需要偏移。但如果你把文字位置看作扇区的“代表点”,可能心里会觉得应该按中心角来算。两种写法结果在绝大多数情况下一样,只在指针恰好压线时会有细微差别。这个属于个人风格问题,用哪种都可以。
还有一个外挂级别的调试技巧:在旋转过程中打印rotation_angle和get_win_index(rotation_angle)到控制台,看最终结果出来之前打印的最后几个值,就能确认判定逻辑有没有线性地跟随指针变化。如果发现索引跳变异常,回看 5.1 的公式,大概率是符号写反了。
8. 还能怎么玩:几个立竿见影的扩展方向
项目做到这里已经是一个完整的可玩状态,但离“拿得出手”还差一口气。我推荐几个扩展方向,按性价比从高到低排:
加音效是最快提升质感的方式。用pygame.mixer.Sound加载一个短促的“咔哒”声,然后在旋转过程中检测指针是否跨过了扇区边界,每次跨过就播放一下。实现思路是:每一帧计算当前get_win_index(rotation_angle),和上一帧对比,不一样就播放音效。这个机制还能暗示“指针扫过每个格子”,配合转速衰减会非常带感。
改成“先定结果再转过去”的模式也很值得尝试。做法是先随机抽取一个奖项,计算它要达到屏幕 270 度位置所需的目标角度,然后从当前角度出发做缓动插值。这种方案的好处是结果可控,适合活动场景下你需要人为控制中奖概率的情况;缺点是需要额外处理跨圈和方向问题,比现在这种“直接衰减”的方案复杂一些。
还有就是记录抽奖历史。抽完一次就把本次结果追加到一个列表里,界面上显示最近几次的抽取记录。这个需求在实际活动里出现频率极高,而且实现成本很低,无非是多画一个文本区域而已。再往上可以加“抽奖次数上限”“奖品库存扣减”“同一奖项不能连续中”这类业务规则,本质上都是在result_text生成那一行做文章。
最后说一点我自己的感受
这个转盘项目虽然不大,但它把游戏程序里最常见的几个环节——绘制、动画、交互、判定——全部过了一遍。我当初写第一版的时候,花时间最多的反而不是 pygame API,而是那些“看起来不起眼”的细节:中文显示、角度符号方向、旋转后黑边、帧率归一化。这些坑每一个都很小,但每个都能耗掉你半小时以上的调试时间。这篇里我把它们全抖出来了,你照着走能省下不少冤枉路。
如果你是自己练手的初学者,我建议拿到完整代码后不要急着跑,先把第 3 节的绘制函数、第 5 节的判定公式自己在纸上推一遍,再运行对照结果。等你把draw_sector改成一个画 12 个扇区的版本还不出错的时候,这个项目就算真正吃透了。