ARTICLE DETAIL

资讯详情

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

从零搭建pygame窗口:星露谷风格游戏主循环与事件处理入门

从零搭建pygame窗口:星露谷风格游戏主循环与事件处理入门 在B站和GitHub上刷到过太多用pygame复刻星露谷的标题点进去多半是直接甩一个几百行的完整代码新手看完脑子嗡嗡的连窗口是怎么冒出来的都没搞明白。我从三年前开始拿pygame折腾像素农场类小游戏前前后后废掉过七八个半成品最大的感受就是窗口这一关看着最简单其实藏着一堆后面会反复咬你的坑。分辨率设多少、游戏主循环怎么写、退出事件为什么处理不干净、帧率锁不锁——这些东西第一天没想清楚等你写到地图滚动和角色移动的时候就得推倒重来。这篇就专门聊第一块砖用pygame把星露谷风格游戏的那个窗口立起来并让它稳定地跑起来。内容围绕pygame窗口的创建与展示展开涉及pygame安装、显示模式选择、主循环结构、事件处理、帧率控制这些最基础但最容易翻车的环节。适合两类人看一是Python基础语法刚学完、想找个像样的项目练手的朋友二是之前抄过代码但没跑通、或者跑通了却不知道每行在干嘛的朋友。我会把每一步为什么这么做讲透也会把当年踩过的坑原样端出来你照着抄作业基本能少走两星期弯路。1. 开工前先把地基砸实环境与依赖的稳妥搭建1.1 为什么第一步不是写代码而是理清运行环境很多人拿到项目第一反应是打开编辑器敲import pygame然后就撞上ModuleNotFoundError。pygame是一个第三方库Python标准库里没有它必须先装。但装之前有件事比装本身更重要确认你的Python版本和pygame版本的对应关系。pygame官方对Python版本是挑食的2023年之后发布的pygame 2.5.x对Python 3.8到3.12支持都比较稳如果你用的是特别老或者特别新的Python很容易在装的时候报编译错误。我个人的习惯是固定用Python 3.10或3.11这两个相对成熟的版本第三方库生态对它们的适配最完整能避开大量版本太新、轮子还没打好的糟心事。装的时候有个细节强烈建议在虚拟环境里装别直接往系统Python里怼。你以后做三消、做塔防、做别的pygame项目各自依赖的库版本可能打架虚拟环境就是给每个项目一个独立房间。用venv三行命令就能搞定成本极低收益极高。# 创建虚拟环境Windows和macOS/Linux通用 python -m venv venv # 激活Windows venv\Scripts\activate # 激活macOS / Linux source venv/bin/activate # 安装pygame pip install pygame装完后敲一行验证能打印出版本号就说明成了python -c import pygame; print(pygame.version.ver)注意如果安装过程中出现一大堆红色报错八成是pip版本太旧导致拉不到预编译的wheel包先执行python -m pip install --upgrade pip再重试绝大多数情况能直接解决。1.2 项目目录结构的提前规划窗口创建是系列第一篇但目录结构如果一开始就乱后面加地图、加角色、加音效文件时会一路乱到底。我在第三个半成品项目里吃过这个亏所有.py文件平铺在根目录到后面自己都找不到哪个文件负责什么。后来我固定了一套轻量结构新手项目完全够用又不至于像大型工程那样上来就分层分到头晕stardew_like/ ├── main.py # 入口负责启动游戏 ├── settings.py # 全局配置分辨率、帧率、颜色常量 ├── assets/ # 图片、音效、字体资源 │ ├── images/ │ └── sounds/ └── src/ # 后续的游戏逻辑模块这个结构的关键在于把配置和逻辑提前分开。settings.py里放的是那种全项目到处都要用、但不该硬编码散落各处的常量比如窗口宽高、目标帧率、常用的颜色值。为什么这个动作现在做因为你写窗口代码时马上就会用到分辨率和帧率如果直接写成数字1280、60散在代码里过两周你想改成全屏或者调整窗口大小就得拿全局搜索一个个改还容易漏。提前抽成常量改一处全项目生效这是从业者的肌肉记忆。settings.py的内容长这样# settings.py # 基础显示配置 SCREEN_WIDTH 1280 SCREEN_HEIGHT 720 FPS 60 TITLE Stardew Like Demo # 基础配色星露谷的泥土色、草地色参考 COLOR_GRASS (106, 190, 48) COLOR_SOIL (134, 96, 67) COLOR_SKY (135, 206, 235) COLOR_WHITE (255, 255, 255) COLOR_BLACK (0, 0, 0)这里的宽高为什么选1280×720而不是1920×1080原因很实际星露谷原作的视觉基底是低分辨率像素画核心画面区域并不大。用1280×720这个档位一是对大多数笔记本屏幕友好二是放大像素素材时比例好算三是渲染压力小老机器也能锁住60帧。如果你一上来就1080p甚至2K像素素材要么被拉伸得糊要么需要额外的整数倍缩放逻辑纯属给自己加难度。等游戏成型需要高清时再调不迟第一版以能流畅看到画面为最高优先级。2. 窗口创建的底层逻辑与最小可运行代码2.1 pygame初始化到底初始化了什么pygame.init()这一行几乎是所有教程的第一行代码但很少有人说清楚它干了什么。pygame不是一个单一模块它是很多子模块的集合——负责显示的display、负责绘图的draw、负责事件处理的event、负责字体的font、负责声音的mixer等等。pygame.init()的作用就是把这些子模块挨个唤醒让它们进入可用状态。换句话说它是一次总开关你不按这个开关后面的显示模块和事件模块都处于沉睡状态调用就会报错。理解这一点后一个进阶习惯就出来了pygame.init()会尝试初始化所有模块其中声音模块mixer在部分环境里会因为音频设备问题初始化失败进而拖慢启动甚至卡住。如果你暂时不需要声音做窗口阶段确实不需要可以精准初始化跳过声音import pygame # 精准初始化先不碰声音模块 pygame.display.init() pygame.font.init() # 如果你确定后续要声音再单独初始化mixer并做好异常兜底 try: pygame.mixer.init() except pygame.error as e: print(f音频模块初始化失败不影响画面{e})这种写法在真实项目里更稳因为团队协作时你没法保证每个同事的电脑音频设备都正常。窗口阶段就养成模块按需初始化 关键模块异常兜底的习惯后面项目体量涨起来会感谢自己。2.2 set_mode一行代码背后的显示模式选择创建窗口的核心就一行pygame.display.set_mode((width, height))但它的参数大有讲究。它返回的是一个Surface对象你可以把它理解成一块画布游戏里所有的绘制最终都是往这块画布上涂色再由pygame.display.flip()或update()把画布内容推到屏幕上显示。这个绘制到画布再整体翻页的双缓冲机制是游戏画面不闪烁的关键——如果每画一笔就直接显示你会看到明显的撕裂和闪烁这是早期图形编程的血泪教训。set_mode的第二个参数flags决定了显示模式的档位常用的几个必须搞明白显示模式标志作用星露谷风格项目的适用场景默认不传flags固定尺寸的普通窗口开发调试首选方便切出看报错pygame.FULLSCREEN全屏独占显示正式发布、沉浸体验时使用pygame.RESIZABLE窗口可拖拽改变大小需要玩家自由调整窗口时pygame.SCALED自动整数倍缩放像素画不糊像素风游戏的推荐选项pygame.DOUBLEBUF启用双缓冲配合全屏时明显减少撕裂对星露谷风格这种像素游戏我的推荐组合是开发期用普通窗口发布期用FULLSCREEN | SCALED。SCALED这个标志在pygame 2.x之后加入专门解决像素游戏放大的问题——它会把你的低分辨率画面按整数倍放大铺满屏幕同时保持像素的锐利边缘不会出现模糊糊的插值。这一点比手动拉伸Surface省事太多也是pygame相比早期版本最实用的改进之一。import pygame from settings import SCREEN_WIDTH, SCREEN_HEIGHT, TITLE pygame.display.init() pygame.font.init() # 开发期普通窗口 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(TITLE) # 发布期可切换成这样本文先注释掉讲解用 # screen pygame.display.set_mode( # (SCREEN_WIDTH, SCREEN_HEIGHT), # pygame.FULLSCREEN | pygame.SCALED # )提示set_caption负责设置窗口标题栏文字。别小看这一行调试多开窗口时清晰的标题能让你一眼认出哪个窗口是哪个项目省得任务栏里一堆pygame window傻傻分不清。2.3 主循环游戏的心跳为什么必须存在窗口建出来了但如果你只写set_mode就结束程序窗口会一闪而过、立刻关闭。因为它需要一个活着的状态来维持——这就是游戏主循环也就是那个经典的while running:结构。主循环是游戏的心跳它每秒钟要跳几十次每次跳动都完成三件事处理输入事件、更新游戏状态、绘制画面。这三件事的顺序在绝大多数项目里都是固定的顺序错了会出现按键延迟或者画面用了上一帧的数据这类诡异现象。import pygame from settings import SCREEN_WIDTH, SCREEN_HEIGHT, TITLE, FPS, COLOR_SKY pygame.display.init() pygame.font.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(TITLE) clock pygame.time.Clock() running True while running: # 第一件事处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 第二件事更新逻辑这一篇里暂时为空先占位 # 第三件事绘制 screen.fill(COLOR_SKY) # 用天空色铺底 pygame.display.flip() # 把画布推到屏幕 # 第四件事控制帧率 clock.tick(FPS) pygame.quit()注意这个循环里我用fill把整个画面刷成天空蓝而不是什么都不画。为什么因为双缓冲机制下每一帧都是重新绘制的如果你不刷底上一帧残留的内容会和新内容叠加出现拖影。这个每帧先清屏的动作是所有实时渲染项目的铁律写窗口第一天就要刻进肌肉记忆。2.4 事件处理里藏着的新手陷阱pygame.event.get()返回的是当前帧所有事件的列表必须用for遍历全部取走。我见过不少新手写成if pygame.event.get()或者只取第一个事件结果就是窗口能显示但关不掉点了叉号没反应只能去任务管理器强杀进程。根本原因就是事件列表没被清空点击关闭的QUIT事件被遗漏了。除了QUIT窗口阶段还有几个事件值得提前认识KEYDOWN按键按下、KEYUP按键抬起、VIDEORESIZE窗口被拖拽改变大小时触发只有用RESIZABLE模式才有、WINDOWFOCUSLOST窗口失去焦点。这些事件在后面的角色控制和菜单切换里都会用到现在先建立事件是外部世界给游戏的信号这个心智模型就够。for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: running False # ESC退出调试期神器 elif event.key pygame.K_F11: # 预留全屏切换后续篇章实现 pass这里加个ESC退出的快捷键是我强烈建议的调试习惯。开发期你频繁要关掉重开去点右上角那个叉号远不如按一下ESC来得快一天下来能省几十次鼠标移动积少成多。3. 让窗口稳如老狗的工程化细节3.1 帧率控制clock.tick为什么不能省clock.tick(FPS)这行放在循环末尾作用是限制主循环每秒最多跑FPS次。有人会问不控制会怎样答案是主循环会以CPU的最大能力疯狂运转可能跑到每秒几千次。这带来两个问题一是CPU占用飙满笔记本风扇狂转二是游戏逻辑速度和你机器性能挂钩——在快机器上角色走得飞快在慢机器上慢吞吞完全失控。锁定60帧后你的游戏在任何机器上表现都一致这是可复现性的基础。clock.tick()还有个好用的副产品它返回自上次调用以来经过的毫秒数。做动画和移动时用得上比如每帧移动速度乘以实际耗时能让不同帧率下的移动速度保持真实世界一致这个技巧在角色走路、作物生长计时里非常关键后面做移动时再展开。# 计算delta time的典型写法 dt clock.tick(FPS) / 1000.0 # 转成秒 # 后续移动写成position speed * dt注意tick要在循环里每轮都调用而且只能调用一次。我见过有人把它放在if分支里导致某些帧跳过计时帧率彻底失控。记住它是心跳的节拍器节拍器不能停。3.2 分辨率的两种思路固定还是自适应窗口阶段就要想清楚一件事你的游戏画面按固定分辨率设计还是随窗口大小自适应星露谷风格游戏的标准答案是前者——按固定逻辑分辨率设计然后用SCALED或手动缩放适配到实际窗口。原因在于像素游戏的所有素材、碰撞盒、坐标规划强依赖于固定像素网格如果画面随窗口拉伸你精心摆放的树木和栅栏就会错位。所以正确做法是把1280×720当成逻辑画布尺寸实际窗口尺寸是它的显示放大结果。pygame里实现这个的标准套路是先创建一个逻辑Surface所有绘制都在这个逻辑画布上完成最后再把它整体缩放贴到实际窗口。# 逻辑画布所有游戏绘制都往它上面画 logical_surface pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) # 实际显示窗口可以是任意大小 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.RESIZABLE) while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.VIDEORESIZE: # 窗口被拖拽后重新创建显示表面 screen pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE) logical_surface.fill(COLOR_SKY) # ... 所有游戏内容画在logical_surface上 ... # 把逻辑画布缩放贴到实际窗口 scaled pygame.transform.scale(logical_surface, screen.get_size()) screen.blit(scaled, (0, 0)) pygame.display.flip() clock.tick(FPS)这套写法的好处是游戏逻辑和显示解耦。以后你想让窗口可大可小甚至做成无边框窗口逻辑代码一行都不用改。坏处是缩放会有性能开销但1280×720到1080p这个量级现代机器完全无感。3.3 双缓冲与flip、update的区别pygame.display.flip()和pygame.display.update()都能刷新画面区别在于前者刷新整个屏幕后者可以只刷新指定区域。窗口阶段用哪个都行但一旦你的游戏有局部变化的UI比如血条、时间显示用update(rect)只重绘变化区域就能显著省性能。养成一个意识flip是全局刷新update是局部刷新选哪个看你的画面变化范围。另外如果你用了DOUBLEBUF标志画面会先在后台缓冲画好再整体翻到前台彻底消除撕裂。普通窗口模式下pygame默认也是双缓冲的所以你在窗口阶段基本不会看到撕裂一旦切全屏务必加上DOUBLEBUF否则画面快速移动时会有横向撕裂线很难看。4. 常见问题排查速查表与实战避坑4.1 高频报错一网打尽新手在窗口这一步遇到的报错其实高度集中我整理成表对着查基本能自己解决报错/现象根本原因解决办法ModuleNotFoundError: No module named pygame没装或装到了别的Python环境确认虚拟环境已激活用python -m pip install pygamepygame.error: No available video device无图形界面环境如纯命令行服务器换到有桌面的系统运行窗口一闪就关缺少主循环维持加上while running循环窗口关不掉event.get()没遍历完所有事件用for遍历事件列表画面闪烁、拖影每帧没清屏或没双缓冲每帧先fill清底全屏加DOUBLEBUFCPU占用100%没限制帧率循环末尾加clock.tick(FPS)中文标题乱码字体未指定中文字体后续用pygame.font.SysFont指定中文字体这张表里最想强调窗口关不掉和CPU飙满这两条因为它们几乎每个新手都会撞一遍而解决成本极低就是一行代码的事。知道原因之后你会觉得这也算问题但不知道时真的能卡一下午。4.2 我踩过的三个真实坑第一个坑是分辨率设成奇数。早期我图省事设过1281×721结果后面做地图瓦片tile排列时每块32像素的格子排到最右边总会剩一条半像素的缝画面边缘出现难看的黑边。像素游戏的分辨率最好是主瓦片尺寸的整数倍比如用32像素瓦片分辨率就选1280×720都是32的整数倍40列、22.5行——这里行数是半个所以后来我改成1280×736正好23行。这个细节没有任何教程会讲但做地图时能救命。第二个坑是忘了处理VIDEORESIZE。我用RESIZABLE模式做了个可拉伸窗口玩家把窗口拖小之后画面里所有内容的坐标还是按原尺寸算结果超出部分直接被裁掉。后来加了VIDEORESIZE事件重新set_mode才正常。这个教训让我明白一旦开启了可变尺寸就必须处理尺寸变化事件否则就是给自己埋雷。第三个坑是虚拟环境和系统Python混用。有一次我在系统Python里装了pygame又用虚拟环境的Python跑代码怎么都提示找不到模块排查了半小时才发现是环境错乱。现在的习惯是一个项目一个虚拟环境激活后第一件事就是验证python -c import sys; print(sys.executable)确认当前用的是哪个Python绝不偷懒。4.3 如何验证窗口代码真的没问题写完之后做三件事验收一是按ESC和点叉号都能正常退出说明事件处理完整二是拖拽窗口如果开了RESIZABLE画面不崩、不闪说明尺寸处理到位三是任务管理器里观察CPU占用应该在一个合理低位几十个百分点以内风扇不狂转说明帧率锁定生效。这三条过了第一块砖就算砸实了。最后分享一个我调试窗口阶段的小习惯在窗口标题里把当前帧率动态显示出来。虽然pygame自带的帧率显示不是默认的但你可以自己在循环里算一下pygame.display.set_caption(f{TITLE} | FPS: {clock.get_fps():.0f})这样窗口标题栏会实时刷新帧率。开发期一眼就能看出有没有掉帧比开性能监控工具方便得多。等发布时再把动态标题去掉就行。这个技巧我用了两年多几乎每个pygame项目的第一天都会加上算是窗口阶段最实用的一把小刀。
返回列表