ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Pygame实战:零基础开发外星人入侵游戏全攻略

Pygame实战:零基础开发外星人入侵游戏全攻略 如果你学Python已经有一阵子爬虫怕封号、数据分析又提不起劲想做点“能拿给朋友玩”的东西那Pygame几乎是唯一不会让你中途放弃的选择。我这两年带过不少零基础学员第一个项目都让他们做《外星人入侵》别看这个名字有点老气它恰好把游戏开发里最核心的几块东西全串起来了。这篇文章没有废话我会带着你从安装Pygame开始一步步写出一个能玩的“外星人入侵”。你不需要懂游戏引擎不需要懂面向对象的高级技巧只要会一点Python基本语法变量、if、for、def跟着敲完你就能收获一个三百行左右的完整项目以及一种“原来游戏循环是这么回事”的踏实感。1. 为什么选《外星人入侵》作为第一个游戏项目1.1 它覆盖了游戏开发的四块核心拼图游戏开发听起来高大上但零基础能接触到的核心就四样显示、输入、更新、碰撞。显示就是窗口和图片怎么画出来输入是键盘鼠标怎么被程序感知更新是游戏里所有物体的位置和状态每秒怎么变化碰撞是判断子弹有没有打中外星人、飞船有没有撞上障碍。这四个环节几乎是所有小游戏的基础骨架而《外星人入侵》每一步都不多不少地踩在这四块拼图上。更妙的是它不像贪吃蛇那样升级空间小也不像塔防那样需要大量地图数据它用到的所有机制都短小直接拆开来看每一步都符合直觉非常适合用来建立“游戏是怎么跑起来”的整体心智模型。1.2 你需要的最低Python基础其实很少很多零基础朋友看到“面向对象”就先慌其实这个项目里用到的类非常浅你只需要把它当成一个“装着属性和方法的盒子”就行。我带的学员里有只学了变量、if、for循环和def函数就开始做的也能顺利跟下来。真正重要的不是编程技巧而是你愿不愿意一行一行跟着敲。因为游戏代码最怕的是“看懂了但写不出来”你一旦完完整整敲过一遍很多语法细节就自然留在手上了。如果连函数都还没搞明白建议先花一两天写几个def练手比如写个判断闰年的函数再回来做这个项目会顺很多。1.3 项目最终形态与学习目标我们最后做出来的东西是这样的屏幕上是一艘飞船位于屏幕底部中央你可以用方向键左右移动它按空格键发射子弹。屏幕上方会一波一波地出现外星人它们排成整齐的阵型整体左右移动每次碰到屏幕边缘就会集体下移一段。你的任务是用子弹把它们全部消灭如果外星人碰到飞船或者它们降到了屏幕底部游戏就结束。每打掉一个外星人得10分完整消灭一波后新的一波会移动得更快、离底部更近。这就是一个完整可玩的街机式小游戏。学完它你会亲手写过三百行左右的Python代码而且能清晰地回答“游戏循环到底在干什么”这个问题。2. 环境与窗口先把“空画布”跑起来2.1 安装Pygame的实操细节与版本坑老生常谈但很多人在第一步就崩。Pygame支持Python 3.8及以上但我建议用3.10或3.11别追最新的3.13因为部分第三方库还没跟上你会在安装时遇到一堆奇怪的问题。安装命令非常简单pip install pygame装完以后在终端里运行python -c import pygame; print(pygame.version.ver)如果是类似“2.5.2”这样的版本号就说明装好了。这里有个容易踩的坑如果你用的是macOS或Linux自带的Pythonpip install的时候可能会提示“externally managed-environment”意思是你当前的环境受系统保护不能随便装包。解决办法是使用虚拟环境或者直接装Anaconda用conda来装。最简单的入门路线是装Anaconda然后conda install pygame这个过程中你还能顺带把spyder或jupyter这些工具搞定。就我个人经验强烈建议不要在全局环境里硬装否则后面每次开新项目都要为依赖头疼。2.2 第一段代码创建窗口与主循环我们先不引入任何图片写一个最小可运行的程序。创建一个800乘600的窗口背景色用浅灰色然后进入游戏主循环。代码如下import pygame pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(Alien Invasion) bg_color (230, 230, 230) while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() exit() screen.fill(bg_color) pygame.display.flip()这里的pygame.init()是初始化所有模块几乎每个Pygame程序都有这一行。你可以把它理解成给游戏机插上电源它会启动声音、显示、字体等底层模块。pygame.display.set_mode((800, 600))返回一个Surface对象也就是我们所说的“画布”。之后你所有绘制操作都是在这块画布上进行最后通过pygame.display.flip()把画布内容真正显示到窗口里。注意flip()这个函数名它代表“翻页”有点像动画里的帧切换因为Pygame会先在后台画好一帧再一次性显示这样可以避免画面闪烁。2.3 帧率、背景色与屏幕尺寸的直觉理解把上面代码跑起来你会发现窗口出来了但CPU占用可能居高不下这是因为我们没有限制主循环的运行速度。标准做法是引入时钟clock pygame.time.Clock() # 把下面这行放在主循环末尾 clock.tick(60)clock.tick(60)表示每秒最多循环60次也就是60帧每秒。为什么要限制帧率第一是避免CPU空转第二更重要让游戏速度在不同性能电脑上保持一致。如果你的电脑性能很好又不限帧率游戏里的物体移动速度会快到无法操作限了帧率后你每帧移动多少像素实际速度就基本固定了。背景色bg_color (230, 230, 230)是一个RGB元组范围为0-255如果你想要纯黑就写(0, 0, 0)想要深空背景可以写成(15, 20, 40)这种偏蓝黑的颜色。屏幕尺寸800x600是我熟悉的配置宽度越大横向空间越大外星人阵型可以更宽难度相对低高度越大外星人从顶部飞到飞船底部的时间越长也等于变相降低了难度。第一次做项目建议就用800x600不要一上来就追求4K。3. 让飞船动起来事件循环与精灵的诞生3.1 从加载图片到控制移动事件到底在干什么游戏窗口不会自己知道你有没有按键盘它靠“事件”来通知程序。Pygame会把鼠标移动、按键按下、窗口关闭这些动作封装成一个个事件对象放进一个事件队列里。pygame.event.get()的作用就是把这个队列里的所有事件取出来供你逐项处理。我们在主循环里用for event in pygame.event.get():遍历这些事件然后根据event.type来决定做什么。当你想控制飞船移动时最直觉的做法是监听pygame.KEYDOWN事件如果按下的是右方向键就让飞船向右挪一点。但这里有个初学者最容易踩的坑KEYDOWN事件只在按键按下那瞬间触发一次如果你只靠它来移动按一下只动一下按住不放它也不会连续移动。要做出真正游戏里“按住就一直移动”的效果我们需要用布尔变量记录“是否处于按住状态”然后在主循环的更新阶段持续检查这个状态。3.2 飞船类为什么用类而不是变量如果用一个变量ship_x存飞船的x坐标再用一个变量ship_image存图片项目起初还好但一旦要加上速度、边界、左右移动判断全局变量就开始打架代码一多就乱成一团。更优雅的做法是定义Ship类把图片、位置、移动方法都封装在同一个地方。初看代码可能觉得多写了几行但后面你会发现当你要在碰撞检测里使用飞船的矩形区域时直接用ship.rect非常顺手。代码骨架如下class Ship: def __init__(self, screen): self.screen screen self.image pygame.image.load(ship.bmp) self.rect self.image.get_rect() self.screen_rect screen.get_rect() self.rect.midbottom self.screen_rect.midbottom self.moving_right False self.moving_left Falseself.image pygame.image.load(ship.bmp)是加载飞船图片get_rect()会返回一个矩形对象该矩形默认跟图片一样大并且记录出生位置。然后我们把self.rect.midbottom设置为屏幕的midbottom也就是让飞船的底部中点对准屏幕底部中点这样一个midbottom赋值就把飞船放到了屏幕底部中央。为什么用midbottom而不是top或center因为飞船在游戏里通常是从底部升起的我们希望它能“站”在屏幕底部而不是从顶部掉下来。moving_right和moving_left就是前面说的连续移动标志它们初始为False表示没有按键。3.3 边界检测与连续移动按键状态的实战处理接下来是移动的核心逻辑。我们在类里添加一个update方法它负责根据标志“自我更新”位置。同时为了不让飞船飞出屏幕需要做边界判断。我常用的写法是这样的def update(self): self.x self.speed if self.rect.right self.screen_rect.right: self.rect.right self.screen_rect.right elif self.rect.left 0: self.rect.left 0 self.rect.x self.x注意这里的self.x是一个浮点数用来保存精确的位置。为什么不直接用self.rect.x因为rect的x坐标是整数当你设置飞船速度为2.5像素每帧时如果直接存在整数里速度的小数部分每帧都会被截断实际移动速度就会比预期慢。用浮点数存self.x然后再赋值给self.rect.x既能精确移动又不影响碰撞检测使用矩形。在主循环里事件监听部分要成对处理KEYDOWN和KEYUPfor event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() exit() elif event.type pygame.KEYDOWN: if event.key pygame.K_RIGHT: ship.moving_right True elif event.key pygame.K_LEFT: ship.moving_left True elif event.type pygame.KEYUP: if event.key pygame.K_RIGHT: ship.moving_right False elif event.key pygame.K_LEFT: ship.moving_left False这段代码的意义是按下方向键时把对应的移动标志设为True松开时设为False。这样只要键按着标志就一直是真的飞船就会持续移动。等键松开标志变假飞船停。这个“事件监听只改标志update方法根据标志移动”的模式是为了把“输入”和“逻辑更新”分开避免在事件处理阶段直接修改位置因为那样很容易出现一帧里多次移动或不同按键互相干扰的问题。边界判断不是可有可无的如果你不判断飞船的矩形会一半飞出屏幕看起来极不专业。我建议初始速度用1跑起来感觉太慢再改成3速度这个值最好也做成类的属性方便以后调整。4. 子弹与外星人游戏循环的“更新-绘制”法则4.1 子弹发射group对象和update方法的妙用没有子弹的飞船只是玩具。我们把子弹设计成一个类继承自pygame.sprite.Sprite这样它能自然融入Group机制。子弹不需要图片直接用pygame.Rect画一个矩形即可。子弹的行为很简单从飞船当前位置向上飞超过屏幕顶部后消失。定义一个Bullet类class Bullet(pygame.sprite.Sprite): def __init__(self, screen, ship): super().__init__() self.screen screen self.rect pygame.Rect(0, 0, 3, 15) self.rect.centerx ship.rect.centerx self.rect.top ship.rect.top self.y float(self.rect.y) def update(self): self.y - 2 self.rect.y self.y这里的self.rect.centerx ship.rect.centerx让子弹初始时水平居中于飞船self.rect.top ship.rect.top让它从飞船顶部出来。update里每次把self.y减2表示向上移动。为什么用self.y存浮点和飞船一样为了处理非整数速度避免位置抖动的视觉bug。发射子弹的时机放在KEYDOWN事件里按下空格键时执行bullets.add(Bullet(screen, ship))。但如果你不加限制只要按住空格子弹就会像机关枪一样喷射毫无游戏性。常见做法是限制屏幕上最多同时存在3颗子弹实现起来就一行条件判断elif event.key pygame.K_SPACE: if len(bullets) 3: bullets.add(Bullet(screen, ship))bullets是pygame.sprite.Group对象它有个“批量更新”的用法在主循环里调用bullets.update()它会自动调用每个成员的update方法之后调用bullets.draw(screen)它会用每个成员的rect位置将它们的图像或矩形绘制到屏幕上。这个“Group批量操作”的好处是你不需要手动写for循环去遍历每一颗子弹并调用方法特别是子弹数量一多这种统一的更新/绘制逻辑会让主循环异常清爽。4.2 外星人阵型嵌套循环生成与移动方向翻转外星人相比飞船多了一个维度它们是一群而且必须以阵型移动。生成阵型用嵌套循环最直观外层控制行内层控制列。假设外星人图片已经加载好宽度是alien_width高度是alien_height生成6列5行的阵型可以这样写aliens pygame.sprite.Group() for row in range(5): for col in range(6): alien Alien(screen) alien.x alien_width 2 * alien_width * col alien.y alien_height 2 * alien_height * row alien.rect.x alien.x alien.rect.y alien.y aliens.add(alien)间距为什么要用两倍宽度因为一个外星人矩形紧挨着另一个不仅视觉上太挤碰撞检测时也会粘连在一起。两倍间距既保持了阵型的紧凑感又给玩家留下了“可以瞄准”的视觉缝隙。移动阵型的核心逻辑是整群先水平移动然后检查是否有任何一个外星人碰到屏幕边缘如果碰到了整群向下移动一小段距离并掉头。这个逻辑写在Alien类的update里通常不太好因为单个外星人不知道自己处于哪个“群”更合理的做法是单独用一个函数或一个alien_flags变量来控制方向。我习惯在主循环里维护一个alien_direction变量值为1或-1然后在每次更新时for alien in aliens: alien.rect.x alien_direction * alien_speed for alien in aliens.sprites(): if alien.rect.right screen_rect.right or alien.rect.left 0: alien_direction * -1 for a in aliens: a.rect.y alien_drop_speed break这段代码先让所有外星人按统一方向移动再遍历检查边缘。一旦检测到撞边就翻转方向并让所有外星人下移一个步长然后break退出检查循环避免同一帧内多次翻转。为什么必须是“先整体移动再统一检查”如果你让每个外星人独立判断撞边那么最左边和最右边的外星人不会同时撞边导致一些移动一些不动阵型散掉。这种“群体状态统一管理”的思想是这次项目里很值得反复琢磨的一个点。4.3 碰撞检测当子弹碰到外星人时发生了什么有了子弹和外星人接下来就是游戏的核心反馈打中目标。Pygame提供了一行非常方便的碰撞检测函数collisions pygame.sprite.groupcollide(bullets, aliens, True, True)这行代码检查bullets中的子弹和aliens中的外星人是否有矩形重叠如果有两个参数为True表示碰撞后双方都被移除。返回的collisions是一个字典键是子弹值是碰撞到的外星人列表。如果你想知道打中了几只可以对这个字典的值取len如果想加分可以遍历字典把每个外星人的分数累加起来。为什么用groupcollide而不是自己写双重循环因为Pygame的碰撞检测是用C语言实现的性能远远好过你在Python里写每个子弹与每个外星人的矩形相交判断。当子弹数量多、外星人数量多的时候这个差异很明显。这里必须提一个很多零基础都会遇到的困惑明明感觉子弹没有碰到外星人为什么打中了因为Pygame默认的碰撞检测是用矩形包围盒也就是图片的整个矩形区域包括图片中透明或空白的地方。比如你用的外星人图片是一张带白色背景的方图方图里外星人只占中间一小块那矩形检测就会把空白区域也算作“身体”导致玩家觉得没碰到却碰撞了。解决思路有两个一是把图片素材修剪成贴合实际形状的透明背景图比如PNG透明背景二是升级到像素级碰撞用pygame.mask。但零基础阶段我强烈建议先用矩形碰撞把整个流程跑通素材选干净的、抠过底的等之后有精力了再回来优化碰撞精度。否则一开始就折腾碰撞精度你会很快失去做游戏的乐趣。5. 把“能跑”升级成“能玩”计分、结束与难度曲线5.1 游戏状态管理活动标志与Game Over画面游戏不可能永远进行下去。当外星人与飞船碰撞或者外星人到达屏幕底部时游戏应该结束。最简单的状态管理是用一个布尔变量game_active初始设为True。在主循环里只有当game_active为True时才更新飞船、子弹、外星人否则画面保持静止只显示Game Over信息。不要忽视这个状态分支否则游戏失败后外星人还会继续移动子弹还在飞飞船还能动画面上会一片混乱。一个简单的结束画面可以这样写if game_active: ship.update() bullets.update() aliens.update() # 检测碰撞等 else: # 显示Game Over文字重新开始的逻辑可以放在键盘事件里。比如检测到event.key pygame.K_p且game_active为False时调用一个重置函数把外星人清空、子弹清空、飞船回初始位置、重新生成波浪并把game_active设为True。注意重置函数里要记得把bullets.empty()和aliens.empty()否则旧的一波外星人和子弹仍留在内存里会和下一波叠加导致卡顿和逻辑错乱。5.2 记分板与最高分文本渲染其实很简单显示文字是另一个高频需求。Pygame里渲染文字分为三步创建字体把字符串渲染成图片再贴到屏幕上。示例代码font pygame.font.SysFont(None, 36) score_image font.render(fScore: {score}, True, (0, 0, 0)) screen.blit(score_image, (10, 10))SysFont(None, 36)表示使用系统默认字体字号36。第二个参数True表示开启抗锯齿第三个参数是文字颜色。render返回一个Surface然后blit把它贴到屏幕指定的左上角位置。这里有个容易忽略的坑因为分数每变化一次score_image都必须重新渲染如果你只在游戏开始前渲染一次之后分数永远不会变。所以要在if game_active更新完分数后每次都重新调用render。最高分同理用一个变量best_score记录每当当前得分超过它就更新。因为中文在某些系统上会显示成方块零基础阶段建议用英文显示或者把中文字体文件放到项目目录里再通过pygame.font.Font加载。做游戏时文本刷新不刷新、字体缺失都是很容易被忽视但又很好排查的小问题。5.3 难度递进如何让外星人越来越多、越来越快一个游戏如果难度不变玩家两三波之后就会觉得无聊。最简单的难度递进逻辑是每当完整消灭一波外星人新生成的一波会更快一点、更靠下一点。在代码层面就是把alien_speed和alien_drop_speed做成变量在每一波开始时按公式调整。例如alien_speed 1.0 alien_drop_speed 10 # 当消灭完一波时 alien_speed * 1.1 alien_drop_speed 2这个做法很直观但要注意倍率不能太大。如果每次涨20%玩到第四波以外星人的飞驰速度基本无法躲避。我实测下来初始1.0每波增加0.15~0.20比较合适。还有一个更微妙的进阶思路让外星人的速度随当前外星人数量动态变化。当外星人剩得少时整体移动变快制造一种“一群小家伙变成冲刺小队”的紧张感。实现方式是在更新移动速度时根据len(aliens)动态计算一个系数。这个玩法能给游戏带来起伏但需要多写几行逻辑可以作为项目完成后的优化方向。记住难度的本质是“给玩家适度的挑战”而不是单纯把数值调到最大。6. 零基础最容易踩的五个坑我从学生作业里看到的6.1 坑while循环里忘了事件捕捉导致卡死很多初学者在主循环里光顾着画图没有写for event in pygame.event.get()结果窗口一出现就白屏卡死只能强制关闭。原因很简单Pygame和操作系统之间需要不定时交换消息如果你不取走事件队列里的内容队列会被塞满系统会认为你的程序没响应。哪怕你包一个空循环只取事件不处理窗口也能正常响应关闭操作。所以写主循环的第一行永远是事件处理而不是screen.fill或pygame.display.flip。这个习惯从第一个小窗口开始就要养成。6.2 坑图片路径错误导致ModuleNotFoundError的错觉我见过很多学生报pygame.error: Couldnt open ship.bmp然后怀疑Pygame没装好。其实问题根本不在库而在于代码里写的是相对路径ship.bmp程序当前的运行目录和图片文件所在目录不一致。用IDE里的“运行”按钮时当前工作目录可能会是项目根目录也可能直接是文件所在目录但两者经常不一致。最稳妥的解决办法是用pathlib定位到当前文件目录from pathlib import Path ship_path Path(__file__).parent / images / ship.bmp self.image pygame.image.load(ship_path)__file__是Python自动提供的当前文件路径通过.parent拿到所在目录再用/拼接子目录。这样无论你从哪个目录运行图片路径都正确。另外pygame.image.load()返回的对象也可以直接使用但如果你加载的是带透明背景的PNG建议再加上convert_alpha()可以提升绘制性能。这些属于细节优化不过路径问题一定要在一开始解决。6.3 坑飞船移动“粘连”——按下就动松开还动如果飞船按下后一直运动松开还在动原因大概率是你只处理了KEYDOWN把moving_right设成True却没有处理KEYUP把标志变回False。这个标志一旦设为True就再也没有机会变假于是飞船一路冲到边界。键盘监听逻辑一定要成对出现你用KEYDOWN设置了什么标志就必须在KEYUP里清掉它。如果你用的是pygame.key.get_pressed()做连续检测就不需要手动维护布尔标志因为它每一帧都返回当前的真实按键状态数组。两种方式没有绝对优劣但它们解决的问题不同get_pressed()适合“按住持续生效”的移动KEYDOWN适合“按一下触发一次”的事件比如发射子弹。分清这两种模式就能避免很多“手感不对”的问题。6.4 坑碰撞检测用矩形近似肉眼看着“没碰上”Pygame的碰撞检测默认基于rect矩形。如果你的图片周围有大量透明或空白区域玩家就会觉得子弹明明没碰到飞船飞船却爆了。我见过有学员用一张很大的圆形图片结果方形的矩形边界已经撞到子弹但圆形图像还没接触画面观感非常奇怪。解决办法要么是裁剪图片去掉边缘空白要么是用像素级的mask碰撞。mask碰撞更精准但性能开销更大课堂项目里通常没必要。我的建议是先把矩形碰撞机制跑通理解整个游戏逻辑再去处理碰撞精度。如果一开始就纠结碰撞精度你会陷入素材处理的泥潭很难体会到游戏机制跑通那刻的快乐。6.5 坑直接把整个游戏写在一个py文件里后面改不动很多零基础教程喜欢把所有代码堆在一个main.py里初期确实很爽但一旦要加新功能几十个变量纠缠在一起动一处错一处。我的习惯是分模块settings.py放游戏配置如屏幕尺寸、背景色、飞船速度ship.py放飞船类bullet.py放子弹类alien.py放外星人类alien_invasion.py作为主程序入口。每个文件职责单一日后想调什么参数找到对应文件改一行就行。这个“模块化”思维不只在Pygame项目里有价值也是你写任何Python项目都会受益的习惯。哪怕每个文件只有十几行也胜过把三百行代码塞进一个文件。项目完成后回头看你会发现模块化让调试和扩展容易得多。我在带学生做这个项目时最常说的就是别急着加花活先把“窗口、飞船、子弹、外星人、碰撞、结束”这六件事跑通之后你哪怕把飞船换成一条狗、把外星人换成西红柿都只是换图片而已。当你对每一个参数都开始有感觉比如调大子弹宽度后手感变爽、调快外星人速度后压力变大到那时你已经真正迈进了游戏开发的大门。
返回列表