尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python入门实战:用pygame从零开发飞机大战游戏

Python入门实战:用pygame从零开发飞机大战游戏 小时候在4399上玩飞机大战谁能想到有朝一日自己能用代码写一个出来。这两年带过不少新手入门Python每次问到想做什么练手项目十个人里有八个说想写游戏。飞机大战确实是Python入门最经典的项目之一因为它足够简单核心逻辑半天能讲完又足够完整从窗口创建、事件响应、碰撞检测到计分存档全都有一套流程走下来Python面向对象、模块化开发这些关键能力都能练到。这篇文章我会从零开始把整个开发过程完整走一遍。你不需要有游戏开发基础Python语法会个大概就行哪怕你只学了列表和字典也能跟着把这游戏做出来。整个项目用到的核心技术点是pygame库的基本使用、面向对象设计、游戏循环机制、碰撞检测原理这些都是游戏开发最底层的骨架学会了之后换任何一个游戏框架都能快速上手。1. 项目拆解与方案选型1.1 为什么选pygame而不是其他框架Python能写游戏的库其实不少有些人会用cocos2d、panda3d甚至有人硬用tkinter写游戏。但对于飞机大战这种2D小游戏pygame几乎是唯一合理的答案。pygame底层基于SDL库写成SDL本身就是专门为游戏开发的跨平台多媒体库处理窗口、事件、图片渲染、音效播放都是强项。pygame对新手最友好的一点是它的坐标系、渲染方式、事件模型都极其直观——左上角是(0, 0)x轴往右增加y轴往下增加画一个图片就是一个blit调用。你不需要理解GPU渲染管线不需要手动管理纹理显存甚至不需要知道什么叫顶点缓冲区。它把游戏开发最核心的循环结构——处理输入、更新状态、绘制画面这个循环——原原本本地暴露给你没有任何花哨的封装。相比之下用tkinter写游戏性能太差刷新率稍微调高CPU就像在跑分用cocos2d它实现了组件化架构很多游戏逻辑被框架接管了反而不利于理解游戏引擎的工作原理。作为学习项目我们要的就是裸奔一点能看清每行代码在干什么所以pygame是唯一正解。1.2 游戏机制设计的核心思路飞机大战看起来简单但游戏机制设计还是有讲究的。我先把我实现的这个版本的核心玩法拆解一下玩家控制一架飞机通过方向键或鼠标左右移动在屏幕底部区域活动敌机从屏幕顶部随机位置生成向下不断移动玩家发射子弹向上射击敌机命中后敌机爆炸消失并得分敌机碰到玩家飞机玩家飞机爆炸游戏结束血量或生命值机制可扩展双发子弹、护盾等道具这套机制的设计要平衡两点一是操作要有紧张感二是逻辑不能太复杂。我给敌机设计了三种类型普通敌机血量低移动慢、精英敌机血量高点防御强、Boss敌机血量厚体积大。每种敌机一个类继承自同一个基类共用一个框架只是在初始化的参数上区分。这样既实现了差异玩法代码量也不会爆炸。道具系统的设计也遵循这个思路。原本想做成独立的类再挂在场景里后来发现直接在敌机死亡时概率生成道具实例放一个统一的道具列表里管理更简洁。每次击杀敌机有20%概率掉落道具道具分为火力增强、护盾、恢复生命三种。1.3 技术难点与开发路线图整个项目最核心的难点在于三块碰撞检测的精度与容错、对象生命周期的管理创建与销毁、游戏帧率与更新逻辑的分离。这三块如果能处理好整个游戏开发项目就算彻底掌握了。碰撞检测难在不误判、不漏判。飞机和子弹都是矩形图片但图片本身有透明区域直接拿图片矩形来检测会让玩家觉得明明没碰到却炸了。最常用的解决方案是留边距检测区域比显示区域收缩几个像素。这个细节我在后面会单独展开讲。对象生命周期的管理难在及时销毁、避免残留。子弹飞出屏幕如果不销毁会无限累积导致游戏越来越卡敌机死亡后的爆炸动画如果不清理内存会不断增长。这个问题的标准解法是对象池技术核心思想是预先创建一批对象循环使用而不是频繁创建销毁。开发路线我建议分四步走第一步搭环境跑通一个主窗口第二步实现玩家飞机的移动和子弹发射第三步加入敌机生成和碰撞检测第四步完善UI和音效做打磨。每一步都是可运行的不要一口气写完所有代码再调试那样出问题根本定位不了。2. 开发环境准备2.1 Python与pygame的安装配置先确保电脑上装好了Python。目前pygame官方稳定版对Python 3.8到3.13都有较好的支持推荐直接用3.10或3.11版本兼容性最好。官网下载安装包时记得勾选Add Python to PATH这个选项不然命令行敲python会提示找不到命令。装好之后打开终端执行下面这行命令安装pygamepip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple后面那个镜像地址建议加上国内网络直连官方PyPI源的下载速度经常让人崩溃。装完后用python -c import pygame; print(pygame.ver)验证是否装成功能输出版本号就说明环境没问题了。需要特别提醒的是很多人会在系统里同时装了Python 2和Python 3或者通过Anaconda装了多个环境。这时候要用python -m pip install而不是直接pip install否则可能装到别的Python环境里去了。另外如果提示pip不是内部或外部命令多半是Python没添加到PATH卸载重装一次安装时勾选对应选项即可。2.2 项目结构规划与素材准备开发之前先把项目结构规划好不要把所有代码堆在一个文件里。虽然飞机大战体量不大但养成模块化开发的习惯很重要。我的项目结构如下plane_war/ ├── main.py # 游戏入口初始化并启动主循环 ├── settings.py # 全局配置窗口尺寸、颜色、帧率、难度参数 ├── player.py # 玩家飞机类 ├── enemy.py # 敌机类普通、精英、Boss ├── bullet.py # 子弹类 ├── supply.py # 道具类 ├── ui.py # 计分、血量、提示文字等UI绘制 ├── collision.py # 碰撞检测函数集 └── resources/ # 图片和音效素材文件夹素材方面新手最容易卡壳的是上哪儿找素材。其实不用急着下载素材包先用pygame的图形绘制功能画出最基本的矩形来代替飞机图片。等游戏逻辑全部跑通再去替换漂亮的图片素材。网上搜飞机大战素材很多开源资源可以下载注意别乱用商业素材就行。图片用.png格式最好支持透明通道不想要白底黑块的尴尬情况就认准png。2.3 理解pygame的核心循环机制写代码之前必须先搞清楚pygame程序是怎么运行的。pygame程序本质上是一个无限循环每一帧循环做三件事处理用户输入事件、更新游戏对象的状态、把所有对象绘制到屏幕上。这个过程专业叫法叫游戏循环或主循环。# pygame主循环的核心骨架 running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 更新游戏状态 player.update() enemies.update() bullets.update() # 绘制画面 screen.fill((0, 0, 0)) player.render(screen) enemies.render(screen) bullets.render(screen) pygame.display.flip()这里最需要理解的是pygame.event.get()这个调用。每个事件循环必须手动获取事件队列并处理如果不处理队列会不断堆积游戏会越来越卡甚至窗口会变成无响应状态。刷新画面用flip()而不是update()flip是双缓冲技术的体现——先在后台内存把画面画好然后一次性切换显示避免画面闪烁。帧率控制是通过game_clock.tick(fps)实现的。这个函数让主循环每秒最多运行指定次数比如tick(60)就锁定每秒60帧。注意帧率是独立于游戏逻辑的游戏逻辑的速度要与帧率解耦否则在高帧率的电脑上游戏会加速低帧率电脑上游戏会变慢后面我会讲到如何在统一帧率下保持逻辑一致的技巧。3. 游戏框架搭建与基类设计3.1 全局配置模块的编写配置模块是所有模块的地基。游戏窗口宽度、高度、背景颜色、初始帧率、难度参数等全部集中放在这里。这样做的好处是后期调整游戏难度时只需要改这一个文件不用满项目翻找魔法数字。# settings.py 全局配置 import pygame # 窗口定义 SCREEN_WIDTH 480 SCREEN_HEIGHT 700 # 帧率 GAME_FPS 60 # 颜色RGB值 BLACK (0, 0, 0) WHITE (255, 255, 255) RED (255, 0, 0) GREEN (0, 255, 0) BLUE (0, 0, 255) # 玩家相关 PLAYER_SPEED 5 PLAYER_HP 3 # 子弹相关 BULLET_SPEED -12 #负数代表向上移动 BULLET_FREQUENCY 10 #每帧数发射的阈值 # 敌机相关 ENEMY_MIN_SPEED 2 ENEMY_MAX_SPEED 4 ENEMY_GENERATE_INTERVAL 60 #帧数间隔注意子弹速度设置为负数是因为pygame坐标系中y轴向下为正向上移动需要负值。这种细节如果不做注释回头自己看代码都会懵。我在每个变量后面加了注释这个习惯强烈建议你养成。3.2 游戏实体基类的定义玩家飞机、敌机、子弹这三类对象有一些共通的属性和方法比如都有一张图片、都有一个在屏幕上的位置、都需要更新状态、都需要绘制到屏幕上。这种共同点抽象成父类的思想就是面向对象的核心价值。class GameObject: 游戏对象基类 def __init__(self, image_path, x, y): self.image pygame.image.load(image_path).convert_alpha() self.rect self.image.get_rect() self.rect.x x self.rect.y y self.is_alive True def update(self): 更新对象状态子类重写 pass def render(self, screen): 绘制对象到屏幕 if self.is_alive: screen.blit(self.image, self.rect)self.rect是pygame里最重要的概念之一它是每个有图像对象都拥有的一个矩形区域记录了对象的坐标和宽高。所有碰撞检测、位置移动都在操作这个rect对象。你完全可以把rect想象成一张图片在游戏世界中的占位框。convert_alpha()方法也要说一下。加载png图片后用convert_alpha()转换格式能加快blit渲染速度保留透明通道。如果不调用这个方法图片加载慢、渲染慢透明区域还会变黑很多新手在这里踩坑。3.3 玩家飞机类的完整实现玩家飞机控制有两种方案键盘控制和鼠标控制。我最终实现的是鼠标控制因为更直观手指移动到哪里飞机就跟到哪里。键盘控制需要按住方向键长时间玩很累手。class Player(GameObject): 玩家飞机类 def __init__(self, center_x, bottom_y): super().__init__(resources/player.png, 0, 0) self.rect.centerx center_x self.rect.bottom bottom_y self.hp PLAYER_HP self.score 0 self.last_shot_time 0 self.fire_power 1 #火力等级 self.shield False #是否有护盾 def update(self): 根据鼠标位置移动飞机 mouse_x, mouse_y pygame.mouse.get_pos() # 让飞机跟随鼠标但限制在屏幕范围内 self.rect.centerx max(30, min(SCREEN_WIDTH - 30, mouse_x)) self.rect.centery max(SCREEN_HEIGHT - 200, min(SCREEN_HEIGHT - 50, mouse_y)) def fire(self, current_tick): 发射子弹带冷却时间 if current_tick - self.last_shot_time BULLET_FREQUENCY: self.last_shot_time current_tick return True # 调用方创建子弹 return False注意飞机的移动范围被限制在屏幕底部的一个区间内y轴不低于SCREEN_HEIGHT-200不高于SCREEN_HEIGHT-50这样玩家不会飞进敌机堆里也不会飞出屏幕外。3.4 配置文件与坐标系统的理解很多刚开始接触游戏开发的人会被pygame的坐标系绕晕。这里再强调一遍屏幕原点在左上角x轴向右增长y轴向下增长。这在数学课上学的坐标系是反过来的一开始非常容易出错。rect对象的centerx、centery、left、right、top、bottom这些属性都是可以读写的理解它们能让定位代码变得非常简单。比如让飞机水平居中self.rect.centerx SCREEN_WIDTH // 2让飞机贴屏幕底部self.rect.bottom SCREEN_HEIGHT - 20。这些属性本质上都是根据x、y、width、height计算出来的快捷方式用起来非常方便。4. 游戏核心功能实现4.1 子弹系统对象管理范本子弹是游戏中生成和销毁最频繁的对象。如果每发子弹都单独创建一个实例发射一次销毁一次Python对象频繁创建销毁的性能开销叠加起来会让游戏掉帧。这是第一个需要引入性能优化思路的地方。class Bullet(GameObject): 子弹类 def __init__(self, x, y, speedBULLET_SPEED): super().__init__(resources/bullet.png, x, y) self.rect.centerx x self.rect.centery y self.speed speed def update(self): self.rect.y self.speed # 飞出屏幕就销毁 这是生命周期管理的核心 if self.rect.bottom 0: self.is_alive False关键代码是update里的判断子弹飞出屏幕顶部就把is_alive置为False。每帧更新完成后主循环需要把所有is_alive为False的对象从列表里移除# 在主循环中清理死亡对象 bullets [b for b in bullets if b.is_alive]这行代码看着简单但它是防止内存泄漏的关键。如果不清理每秒钟发射10发子弹运行10分钟列表里就有6000个没用的对象占着内存。虽然单个对象很小但积少成多游戏运行一段时间后会明显卡顿。4.2 敌机生成机制与难度曲线敌机不能乱生成需要有节奏感否则玩家要么闲得没事干要么被虐哭。我实现了一个基于帧数的生成器每隔ENEMY_GENERATE_INTERVAL帧生成一个敌机随着游戏时间推移生成间隔逐渐减小敌机的速度逐渐增加。class EnemyGenerator: 敌机生成器 def __init__(self): self.frame_count 0 self.spawn_interval 60 self.min_interval 20 def update(self): self.frame_count 1 # 随着时间推移生成间隔逐渐缩短 if self.frame_count 3000: self.spawn_interval max(self.min_interval, self.spawn_interval - 1) if self.frame_count % self.spawn_interval 0: self.spawn_enemy()这里设计了一个简单的难度曲线游戏运行越久生成频率越高。进阶的方向是按关卡划分每过一关整体提升敌机速度和生成频率再加一点新的敌机类型。我做的版本里精英敌机的生成概率也随着时间上升后期关卡密集时满屏强者。4.3 碰撞检测从像素级到矩形级的性能取舍碰撞检测是整个游戏逻辑中最需要精雕细琢的部分。pygame提供了两种内置的碰撞检测方法rect.colliderect(other_rect)检测两个矩形是否重叠pygame.sprite.spritecollide()精灵组碰撞检测对于飞机大战这个量级直接用矩形碰撞检测就足够了。它的原理很简单把两个对象都看成规则的矩形判断四边是否相交。优点是速度极快缺点是精度一般特别是形状不规则的图片会存在视觉误差。我的做法是碰撞区域收缩处理。比如玩家飞机的图片是60x60但真正有效碰撞区域只有中心40x40的区域。定义一个碰撞用的缩水矩形def get_collision_rect(obj, shrink_ratio0.7): 获取一个收缩后的碰撞区域 width obj.rect.width * shrink_ratio height obj.rect.height * shrink_ratio col_rect pygame.Rect(0, 0, width, height) col_rect.center obj.rect.center return col_rect这样碰撞区域比图片区域小一圈玩家的体验是我的飞机翅膀擦到敌人不会死机身碰到才算体感会好很多。如果反过来把碰撞区域设置得比图片大玩家会觉得很冤枉我明明离得那么远怎么就没了这是交互设计上很基础的玄机。4.4 计分系统与游戏状态管理游戏有一个全局游戏状态最简单的方法是用枚举或常量定义# 游戏状态 GAME_READY 0 # 准备开始 GAME_RUNNING 1 # 游戏进行中 GAME_OVER 2 # 游戏结束计分系统包含击杀得分和最佳分数存档。得分逻辑放在碰撞检测里处理当子弹与敌机碰撞时根据敌机类型加分普通100分、精英300分、Boss 1000分同时更新UI上显示的分数。最佳分数存档用JSON文件存在本地游戏结束时读取并更新最高分。这是所有游戏都会有的持久化存储机制用Python的json模块几行代码就搞定了def save_high_score(score): try: with open(high_score.json, r) as f: data json.load(f) except FileNotFoundError: data {high_score: 0} if score data[high_score]: data[high_score] score with open(high_score.json, w) as f: json.dump(data, f)4.5 音效与UI界面的实现音效素材我用的是网上免费的音效资源。游戏过程中的背景音乐用pygame.mixer模块播放循环音乐。音效和背景音乐注意要分两个通道加载否则音效会把背景音乐打断。pygame.mixer默认支持多个通道但调用时频道分配不当会互相干扰。UI界面这块用pygame的字体模块绘制文字def draw_text(screen, text, size, x, y, colorWHITE): 在屏幕上绘制文字 font pygame.font.SysFont(simhei, size) text_surface font.render(text, True, color) text_rect text_surface.get_rect() text_rect.center (x, y) screen.blit(text_surface, text_rect)SysFont指定simhei是黑体字体Windows系统一般都带。如果要在游戏打包后发给别人最好把字体文件也一起打包否则对方的系统没有这个字体文字会显示成乱码或方块。5. 完整代码调试与运行5.1 主循环的完整编写现在把所有模块组装成主循环。这里有一个非常重要的设计原则每帧的update和render要严格分开。update只处理逻辑状态render只负责绘制。如果混在一起比如在update里顺手画了一笔后面的绘制逻辑会完全乱套。def run_game(): 游戏主函数 pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(飞机大战) game_clock pygame.time.Clock() # 游戏对象 player Player(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 100) bullets [] enemies [] supplies [] generator EnemyGenerator() # 音效 shoot_sound pygame.mixer.Sound(resources/shoot.wav) explode_sound pygame.mixer.Sound(resources/explode.wav) running True game_state GAME_READY while running: game_clock.tick(GAME_FPS) # 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN: if game_state GAME_READY: game_state GAME_RUNNING elif game_state GAME_OVER: game_state GAME_RUNNING # 重置游戏 player Player(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 100) bullets.clear() enemies.clear() if game_state GAME_RUNNING: # 更新逻辑 player.update() if player.fire(game_clock.get_ticks()): bullets.append(Bullet(player.rect.centerx, player.rect.top)) shoot_sound.play() generator.update() enemies [e for e in enemies if e.is_alive] for enemy in enemies: enemy.update() # 子弹移动与碰撞检测 for bullet in bullets: bullet.update() for enemy in enemies: if bullet.is_alive and enemy.is_alive: if bullet.rect.colliderect(get_collision_rect(enemy)): enemy.hp - 1 bullet.is_alive False if enemy.hp 0: enemy.is_alive False player.score enemy.score_value explode_sound.play() bullets [b for b in bullets if b.is_alive] # 检查玩家被撞 for enemy in enemies: if enemy.is_alive and enemy.rect.colliderect(get_collision_rect(player)): if player.shield: player.shield False enemy.is_alive False else: player.hp - 1 enemy.is_alive False if player.hp 0: game_state GAME_OVER save_high_score(player.score) # 绘制画面 screen.fill(BLACK) if game_state GAME_READY: draw_text(screen, 飞机大战, 64, SCREEN_WIDTH//2, SCREEN_HEIGHT//3) draw_text(screen, 点击开始游戏, 32, SCREEN_WIDTH//2, SCREEN_HEIGHT//2) elif game_state GAME_RUNNING: player.render(screen) for bullet in bullets: bullet.render(screen) for enemy in enemies: enemy.render(screen) draw_text(screen, f分数: {player.score}, 24, 60, 30) draw_text(screen, f血量: {player.hp}, 24, SCREEN_WIDTH-60, 30) elif game_state GAME_OVER: draw_text(screen, 游戏结束, 64, SCREEN_WIDTH//2, SCREEN_HEIGHT//3) draw_text(screen, f得分: {player.score}, 32, SCREEN_WIDTH//2, SCREEN_HEIGHT//2) draw_text(screen, 点击重新开始, 28, SCREEN_WIDTH//2, SCREEN_HEIGHT//2 80) pygame.display.flip() pygame.quit()这段代码是整个游戏的骨架。你可以看到所有对象都是列表存储每帧遍历更新和绘制。当前版本为控制篇幅简化了道具和Boss管理但这些扩展万变不离其宗你可以在课后尝试把道具系统加回去。5.2 用对象池优化子弹与敌机生成前面提过对象生命周期管理真正做游戏开发的时候还有一个更系统的优化方案对象池。对象池的核心思想是预先创建一批对象放在池子里需要时从池子取用完还回池子而不是反复创建销毁。class BulletPool: 子弹对象池 def __init__(self, pool_size50): self.pool [Bullet(-100, -100, 0) for _ in range(pool_size)] self.active_bullets [] def fire_from(self, x, y): 从指定位置发射一颗子弹 if self.pool: bullet self.pool.pop() bullet.rect.centerx x bullet.rect.centery y bullet.is_alive True self.active_bullets.append(bullet) def update(self): 更新所有活跃子弹并回收飞出屏幕的子弹 for bullet in self.active_bullets: if bullet.is_alive: bullet.update() if not bullet.is_alive: # 飞出屏幕回收 self.pool.append(bullet) self.active_bullets [b for b in self.active_bullets if b.is_alive]对于飞机大战这种简单的游戏对象池带来的性能提升不一定能感受到但这是一个非常重要的编程思想。真实商业游戏里特别是射击类游戏子弹、敌人、特效动辄成千上万频繁创建销毁对象的GC压力非常大。提前理解对象池套路对以后接触大型开发项目有很大帮助。5.3 精灵组pygame内置的对象管理方案pygame其实内置了精灵Sprite和精灵组Group机制专门用来解决我上面说的问题。pygame.sprite.Sprite是一个轻量级的游戏对象基类Group是一个类似列表的容器但它能一行代码完成碰撞检测# 使用精灵组的碰撞检测 from pygame.sprite import Sprite, Group, groupcollide class PlayerSprite(Sprite): def __init__(self): super().__init__() self.image pygame.image.load(resources/player.png) self.rect self.image.get_rect() all_sprites Group() bullets_grp Group() enemies_grp Group() # 碰撞检测一行代码 crashes groupcollide(bullets_grp, enemies_grp, True, False)groupcollide会同时检测两组精灵之间的碰撞参数分别控制碰撞后是否销毁两个对象。这个功能非常实用。不过我在文章里刻意先用列表手写实现就是想让你彻底弄清碰撞检测背后的原理再用pygame的精灵组方法时就能真正理解每一步在干什么。5.4 游戏帧率与逻辑一致性的细节这里有一个很多新手都会踩的深坑游戏逻辑速度跟帧率到底怎么解耦我的版本中player.update使用鼠标位置实时赋值天然跟帧率无关。但敌机移动使用self.rect.y self.speed如果游戏运行在120帧的显示器上敌机速度是60帧时的两倍玩家会明显感觉游戏变得难了很多。单机游戏最稳妥的做法是固定帧率运行前面代码里已经用Clock.tick(GAME_FPS)锁定了60帧。但更专业的做法是用增量时间Delta Time控制# 用delta time控制移动速度 def update(self, delta_time): self.rect.y self.speed * delta_time * 60 # 以60帧为基准的缩放主循环里计算每帧的耗时然后所有与速度相关的位移都乘以delta_time系数这样游戏的运行速度就与帧率无关了。做3D游戏、做联机游戏时这个技巧几乎是必须的。飞机大战虽然用固定帧率就能搞定但理解这个思路对后续进阶很重要。6. 常见问题与实战排错技巧6.1 环境与安装常见报错开发飞机大战的过程中新手在环境准备阶段就会碰到不少问题这里把最常遇到的整理成表报错现象可能原因解决方案ModuleNotFoundError: No module named pygamepygame没正确安装到当前Python环境用python -m pip install pygame重新安装pygame无法初始化窗口显卡驱动不支持或SDL版本异常更新显卡驱动尝试从源码重新安装pygame图片加载失败图片路径错误或文件不存在检查文件名和路径推荐用绝对路径调试中文显示为方块字体不支持中文字符指定支持中文的字体文件如simhei.ttf游戏运行CPU占用100%主循环没有限制帧率确认代码里调用了pygame.time.Clock().tick()图片路径的坑必须要单独强调pygame加载资源时使用的是相对路径这取决于运行终端的工作目录。如果从项目根目录运行main.py路径resources/player.png没问题但如果换个目录运行就会加载失败。最稳妥的写法是用os.path来构建绝对路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) PLAYER_IMG os.path.join(BASE_DIR, resources, player.png)6.2 游戏运行时的常见逻辑Bug第一类bug是子弹穿透敌人。明明子弹画上去和敌机重叠了但碰撞检测不生效。这个问题的常见原因是子弹的位移过大比如speed-20一帧移动20像素前一帧在敌机上方后一帧已经跑到敌机下方去了根本没有和敌机矩形重叠过。解决方案叫连续碰撞检测或扫掠检测即把移动轨迹分割成小段逐段检测。对于飞机大战最简单的办法是限制单帧位移不超过敌机高度的一半或者把子弹速度控制在合理范围内。第二类bug是子弹穿透敌人后依然存在。前面提到清理列表的代码发现有些人在循环遍历中移除元素会导致跳过一个元素# 错误的遍历删除方式 for b in bullets: if not b.is_alive: bullets.remove(b) # 删除会导致后续元素前移跳过检查Python的for循环是基于索引迭代的迭代过程中删除元素会改变后续元素的位置导致一些元素被跳过。正确做法是用列表推导式创建新列表或者反向遍历删除。这也是为什么我在代码里不厌其烦地强调不要边遍历边删要么新列表要么倒着删。第三类bug是游戏画面闪屏。这个问题几乎都是因为没有双缓冲。pygame初始化窗口时默认会显示一个未完成绘制的窗口需要在set_mode时设置双缓冲相关的参数或者使用pygame.display.flip()。如果你的代码里用了pygame.display.update()在高频刷新下可能会闪烁改成flip()试试。6.3 性能问题排查与资源管理游戏运行时间长了卡顿排查顺序是这样的先检查对象列表长度打印len(bullets)、len(enemies)看是否持续增长没有清理检查素材大小有些新手放了一张2048x2048的大图缩放成50x50使用每次blit都在处理大图非常浪费检查音效文件wav格式的音频文件特别大尽量用ogg或mp3格式加载时不要一次性全部加载到内存用pygame的profile工具pygame.sprite.profiler或Python的cProfile定位性能瓶颈实际测试中飞机大战中最典型的性能问题是图片素材处理不当。游戏策划为了高清放了840x840的图片缩放成80x80用。pygame的transform.scale每次缩放都要计算全像素一个子弹还好100发子弹就会出现掉帧。解决方法是素材图片应该提前用工具缩好代码里尽量避免每帧做scale操作。6.4 为飞机大战增加外设控制支持扩展玩法的话支持游戏手柄会让游戏更有意思。pygame的joystick模块天生支持手柄输入pygame.joystick.init() joysticks [pygame.joystick.Joystick(i) for i in range(pygame.joystick.get_count())] for joy in joysticks: joy.init() # 在主循环中读取摇杆 axis_x joysticks[0].get_axis(0) axis_y joysticks[0].get_axis(1) player.rect.centerx int(axis_x * PLAYER_SPEED) player.rect.centery int(axis_y * PLAYER_SPEED)这是跟前面delta time解耦思想结合得很好的扩展方向。加手柄支持并不意味着代码复杂多少但整个游戏的操控体验会上升一个量级。带着手柄去聚会给朋友玩你写的游戏那个成就感是任何作业都给不了的。7. 项目扩展与进阶方向7.1 从游戏玩法层面迭代第一版做完之后可以做大量的玩法扩展。我这里罗列了几个方向供参考由易到难增加Boss战设计一个有阶段变化的Boss每掉一定血量就进入下一个攻击模式这是横版射击游戏最核心的玩法逻辑引入多兵种联动小型机成群结队出现精英机在后方开火Boss在特定波次登场道具系统完善火力从单发升双发、三发加导弹、激光等特殊攻击加一个连击系统连续击杀敌人不掉链子时分数倍增刺激玩家持续进攻局域网联机对战用socket编程做双人模式一人控制飞机攻击一人控制敌机生成加碰撞爆炸粒子特效把爆炸做成粒子系统这是图形编程里很有趣的分支技术维度上所有这些扩展都以现有的框架为基础重点是面向对象、事件驱动、状态管理等编程思想的应用。7.2 从工程化层面优化游戏本身做完之后你还可以把工程化能力练一练。比如把游戏打包成独立可执行文件用PyInstaller工具一行命令搞定pyinstaller --onefile --windowed --add-data resources;resources main.py--onefile打包成单个exe文件--windowed表示运行时不弹黑色控制台窗口--add-data把素材文件夹一起打进去。做完这些你就可以把游戏发给朋友直接玩了。配置管理方面可以用configparser或纯yaml文件管理游戏数值然后做一个独立的数值调试面板在游戏运行时实时调整难度、速度等参数。这是游戏策划岗位会接触到的数值调优工作流。7.3 把项目写进简历与作品集飞机大战这个项目虽然看起来太经典但把它做深了其实很有价值。面试官不会因为你写了个飞机大战就高看你但如果你的介绍里能说出我通过对象池优化了子弹生成将帧率从30提升到60稳定运行或项目结构上我用模块化设计把碰撞检测从游戏逻辑中抽离便于单元测试这就完全不是一个入门项目了这是你能拿得出手的工程能力证明。你可以围绕项目写一份简短的技术文档把架构图、核心模块职责、性能优化方案、扩展计划描述清楚。整理成Markdown或PDF放在GitHub仓库里这就是一份非常漂亮的个人作品。比起抄一个别人写过的电商网站demo飞机大战项目对你了解游戏循环、事件系统、碰撞检测这些底层原理其实含金量更高。8. 最终调试和发布心得整个项目从空目录到能玩、能计分、能存档大概需要两天左右的业余时间。我目前这个版本在Windows 10、Python 3.10环境上运行稳定帧率稳定60fps内存占用恒定在80MB左右没有任何明显的性能问题。游戏打包成exe后大小约120MB主要占用来自pygame自带的SDL库和字体文件。几个我自己踩过坑之后总结的经验拿出来分享第一素材文件的规范命名一定要坚持。刚开始我图省事图片命名随意什么11.png、航班1.png后来代码里根本认不出哪张是哪张。后来全部改成player.png、enemy_normal.png、enemy_elite.png、bullet_1.png这种格式代码可读性瞬间提升。第二写代码的过程中会忍不住一路加功能。今天看到别人的飞机大战有导弹明天看到一个好看的特效想加进来。我建议你第一版死守范围功能全部简化能跑通就好。用git管理项目每完成一个阶段就提交一次。等核心逻辑稳定了再开新分支做扩展这样既能保持主版本可用又能放心大胆地试各种新玩法。第三pygame官方文档是你最好的老师。虽然英文文档读着累但pygame的文档质量在开源项目里算是相当高的每个模块都有详细说明和示例代码。遇到问题先查官方文档比到处搜博客效率高得多。另外pygame的GitHub仓库里的examples目录有不少现成示例比如chimp、alien invaders这些经典案例都是非常好的学习素材。最后如果你在开发过程中卡在任何一步欢迎在评论区留下你的问题我会尽量回复。也可以把你做出来的效果截图发出来大家一起交流碰撞。游戏开发这条路入门不难但坚持做完一个项目、不断优化到满意的状态才是真正拉开差距的地方。希望这篇文章能帮你迈出第一步做出一款属于你自己的飞机大战。
返回列表