资讯动态

Pygame实战:从零实现枪战模拟器2D射击游戏

发布时间:2026/10/1 16:58:57 来源:尧图企业网站定制
之前在给学生讲 Pygame 入门时发现大部分教程都停在“画一个窗口、移动一个方块”这种层面离真正能运行、能操作的完整小游戏还有一段距离。看了一圈网上关于《枪战模拟器》的素材要么只给代码不给思路要么只讲概念没有可复现工程。这篇文章以《枪战模拟器》为例从零实现一个可运行的 2D 射击小游戏覆盖游戏循环、精灵对象、碰撞检测、随机敌人、计分与生命系统等完整流程。无论你是刚学 Python 的在校生还是想快速体验游戏开发的转行者都可以照着动手敲一遍。1. 背景与核心概念1.1 什么是枪战模拟器枪战模拟器从游戏玩法角度看是一类以射击为核心交互方式的游戏。玩家控制角色或准星移动并发射子弹命中目标后获得分数同时需要躲避或承受敌人的攻击。本文实现的版本是一个简化版玩家在窗口底部移动按空格键向上发射子弹敌人从屏幕顶部不断向下移动子弹击中敌人则得分敌人碰到玩家则扣减生命生命归零游戏结束。从技术角度看它并不需要复杂的 3D 建模或物理引擎核心就三件事持续刷新画面形成动态效果。管理多个游戏对象的位置与状态。判断对象之间是否发生碰撞并触发对应逻辑。这三点正好对应 Pygame 游戏开发的三个基本能力事件循环、对象更新、碰撞检测。所以《枪战模拟器》很适合作为游戏开发的入门实战项目。1.2 游戏循环一切游戏的基础几乎所有实时游戏都遵循同一个循环模型处理输入 - 更新逻辑 - 渲染画面 - 等待下一帧。每秒钟循环执行多少次就是常说的帧率 FPS。比如 FPS 设置为 60表示游戏每秒刷新 60 帧画面。帧率越高画面越流畅但也会消耗更多 CPU。游戏循环中如果更新逻辑和渲染逻辑没有分开很容易出现“操作反应慢”“画面撕裂”等问题。Pygame 提供的pygame.time.Clock().tick(FPS)方法可以精确控制循环速度。调用tick(60)会让当前循环至少保持 1/60 秒一帧避免游戏跑得过快。1.3 精灵、对象与碰撞检测在 Pygame 中可以用pygame.Rect表示一个矩形区域这个矩形既定义了对象的位置x、y也定义了大小width、height。玩家、子弹、敌人都可以抽象成一个带Rect的对象。判断两个对象是否碰撞最常见的做法是矩形碰撞检测即判断两个矩形是否有交集。Pygame 内置方法colliderect()可以直接完成这个判断if bullet.rect.colliderect(enemy.rect): # 子弹命中了敌人 pass矩形碰撞实现简单、计算速度快适合《枪战模拟器》这种 2D 游戏。如果将来做更精细的碰撞可以考虑圆形碰撞或多边形碰撞但入门阶段先掌握矩形碰撞就够了。2. 环境准备与版本说明2.1 运行环境本文示例代码基于以下环境操作系统Windows 10 / 11、macOS、主流 Linux 发行版均可Python3.8 及以上版本Pygame2.0 及以上版本版本需要根据你的项目实际情况调整。Pygame 2.x 与 1.9.x 在 API 上基本兼容但如果你的项目使用了老版本 Pygame建议先升级pip install --upgrade pygame2.2 安装 Pygame在命令行中执行pip install pygame安装完成后可以执行一条简单命令验证是否成功python -m pygame.examples.aliens如果能看到一个可以操作的小游戏窗口说明 Pygame 安装成功。如果提示找不到模块请检查 Python 环境路径或者尝试使用虚拟环境。2.3 开发工具与项目结构开发这类小游戏不需要复杂 IDEVS Code、PyCharm、甚至系统自带的文本编辑器都可以。建议新建一个项目目录所有代码统一管理。预期项目结构如下gun_simulator/ ├── main.py └── README.md其中main.py是唯一的核心脚本包含全部游戏逻辑。虽然工程上不建议把所有类都写在一个文件里但对于入门项目单文件可以降低运行成本后面章节会进一步说明如何拆分。3. 核心原理拆解3.1 窗口初始化与画面刷新Pygame 创建窗口非常简单screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(枪战模拟器)set_mode接收一个宽高元组返回一个表示整个窗口的 Surface 对象。之后所有绘制操作都基于这个 screen。每一帧绘制时如果不清空上一帧内容画面会残留拖影。所以渲染的第一步通常是填充背景色screen.fill((0, 0, 0)) # 黑色背景把所有对象绘制完成后调用pygame.display.flip()将显存中的画面真正显示到窗口。这一步两行代码是整个游戏渲染的基础。3.2 面向对象的实体建模《枪战模拟器》中至少有三种实体玩家、子弹、敌人。用类封装每个实体的数据和职责代码会更清晰。每个实体类通常包含四个部分__init__初始化位置、大小、速度等属性。update根据规则更新自身状态。draw把自己绘制到屏幕上。rectPygame 自带的矩形对象用于碰撞检测和位置管理。例如玩家类持有cooldown属性用来控制射击冷却时间。每次射击后cooldown被设置为一个初始值每一帧递减当冷却值大于 0 时不允许射击。这样可以避免玩家按住空格时子弹像连珠炮一样无限制发射。3.3 碰撞检测与对象清理碰撞检测不仅要检测碰撞还要考虑碰撞后的对象处理子弹击中敌人后子弹和敌人都应该从游戏世界中消失。敌人碰到玩家后敌人消失玩家的生命减少。由于在遍历列表过程中直接删除元素容易导致漏删或崩溃稳妥做法是遍历旧列表把仍然存活的元素收集到新列表或者先遍历副本再修改原始列表。本文代码里采用“副本遍历 原始列表修改”的方式for enemy in enemies[:]: if bullet.rect.colliderect(enemy.rect): enemies.remove(enemy) score 10 breakenemies[:]创建了原列表的副本因此循环过程中修改enemies本体不会影响遍历行为。3.4 随机敌人刷新与难度控制敌人的出现位置和下落速度如果全部固定游戏很快会变得枯燥。Pygame 配合 Python 的random模块可以轻松实现随机生成enemy_x random.randint(0, SCREEN_WIDTH - ENEMY_WIDTH) enemy_speed random.randint(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX) enemies.append(Enemy(enemy_x, -ENEMY_HEIGHT, enemy_speed))注意生成位置的 y 值设置为-ENEMY_HEIGHT也就是从屏幕顶部上方出现然后逐渐移动到可视区域视觉上更自然。难度控制方面最简单的方式是控制生成间隔。间隔帧数越短敌人出现越频繁。后续可以在此基础上根据分数动态调整生成间隔实现难度递增。4. 完整实战案例4.1 创建项目目录在本地文件系统中新建目录mkdir gun_simulator cd gun_simulator然后创建main.py文件。4.2 完整代码下面给出《枪战模拟器》的完整代码可以直接复制保存为main.py运行。import random import sys import pygame # ---------- 初始化 ---------- pygame.init() # ---------- 窗口与帧率 ---------- SCREEN_WIDTH 800 SCREEN_HEIGHT 600 FPS 60 # ---------- 颜色 ---------- COLOR_BLACK (0, 0, 0) COLOR_WHITE (255, 255, 255) COLOR_GREEN (0, 255, 0) COLOR_RED (255, 0, 0) # ---------- 玩家 ---------- PLAYER_WIDTH 50 PLAYER_HEIGHT 40 PLAYER_SPEED 6 # ---------- 子弹 ---------- BULLET_WIDTH 8 BULLET_HEIGHT 4 BULLET_SPEED 12 BULLET_COOLDOWN 12 # 两次射击之间的帧间隔 # ---------- 敌人 ---------- ENEMY_WIDTH 40 ENEMY_HEIGHT 30 ENEMY_SPEED_MIN 2 ENEMY_SPEED_MAX 5 ENEMY_INTERVAL 45 # 每 45 帧生成一个敌人 # ---------- 全局游戏状态 ---------- score 0 life 5 game_over False class Player: 玩家角色负责移动、射击。 def __init__(self, x, y): self.rect pygame.Rect(x, y, PLAYER_WIDTH, PLAYER_HEIGHT) self.speed PLAYER_SPEED self.cooldown 0 def update(self, keys): 根据键盘输入更新玩家位置并限制在窗口内。 if keys[pygame.K_LEFT] or keys[pygame.K_a]: self.rect.x - self.speed if keys[pygame.K_RIGHT] or keys[pygame.K_d]: self.rect.x self.speed if keys[pygame.K_UP] or keys[pygame.K_w]: self.rect.y - self.speed if keys[pygame.K_DOWN] or keys[pygame.K_s]: self.rect.y self.speed # 边界约束让玩家不能移出窗口 self.rect.left max(0, self.rect.left) self.rect.right min(SCREEN_WIDTH, self.rect.right) self.rect.top max(0, self.rect.top) self.rect.bottom min(SCREEN_HEIGHT, self.rect.bottom) if self.cooldown 0: self.cooldown - 1 def shoot(self): 尝试生成一颗子弹处于冷却时间内则返回 None。 if self.cooldown 0: return None self.cooldown BULLET_COOLDOWN bullet_x self.rect.centerx - BULLET_WIDTH // 2 bullet_y self.rect.top - BULLET_HEIGHT return Bullet(bullet_x, bullet_y) def draw(self, screen): pygame.draw.rect(screen, COLOR_GREEN, self.rect) class Bullet: 子弹对象向上飞行越界后由上层逻辑清理。 def __init__(self, x, y): self.rect pygame.Rect(x, y, BULLET_WIDTH, BULLET_HEIGHT) def update(self): self.rect.y - BULLET_SPEED return self.rect.bottom 0 # True 表示已经飞出屏幕 def draw(self, screen): pygame.draw.rect(screen, COLOR_WHITE, self.rect) class Enemy: 敌人对象从顶部生成向下移动。 def __init__(self, x, y, speed): self.rect pygame.Rect(x, y, ENEMY_WIDTH, ENEMY_HEIGHT) self.speed speed def update(self): self.rect.y self.speed return self.rect.top SCREEN_HEIGHT # True 表示已经飞出屏幕 def draw(self, screen): pygame.draw.rect(screen, COLOR_RED, self.rect) def draw_text(screen, text, size, x, y, colorCOLOR_WHITE): 在屏幕上绘制一行文字返回文字所在的矩形区域。 font pygame.font.SysFont(None, size) surface font.render(text, True, color) rect surface.get_rect(topleft(x, y)) screen.blit(surface, rect) return rect def main(): global score, life, game_over screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(枪战模拟器) clock pygame.time.Clock() player Player(SCREEN_WIDTH // 2 - PLAYER_WIDTH // 2, SCREEN_HEIGHT - 80) bullets [] enemies [] enemy_countdown ENEMY_INTERVAL while True: clock.tick(FPS) # 1. 事件处理 for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() if event.type pygame.KEYDOWN and event.key pygame.K_r and game_over: # 游戏结束后按 R 重置 score 0 life 5 game_over False bullets.clear() enemies.clear() player.rect.topleft ( SCREEN_WIDTH // 2 - PLAYER_WIDTH // 2, SCREEN_HEIGHT - 80, ) # 2. 更新逻辑 if not game_over: keys pygame.key.get_pressed() player.update(keys) # 按住空格持续射击 if keys[pygame.K_SPACE]: bullet player.shoot() if bullet: bullets.append(bullet) # 生成敌人 enemy_countdown - 1 if enemy_countdown 0: enemy_x random.randint(0, SCREEN_WIDTH - ENEMY_WIDTH) enemy_speed random.randint(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX) enemies.append(Enemy(enemy_x, -ENEMY_HEIGHT, enemy_speed)) enemy_countdown ENEMY_INTERVAL # 更新子弹删除飞出屏幕的子弹 bullets [bullet for bullet in bullets if not bullet.update()] # 更新敌人删除飞出屏幕的敌人 enemies [enemy for enemy in enemies if not enemy.update()] # 子弹与敌人碰撞检测 surviving_bullets [] for bullet in bullets: hit_flag False for enemy in enemies[:]: if bullet.rect.colliderect(enemy.rect): enemies.remove(enemy) score 10 hit_flag True break # 一颗子弹只命中一个敌人 if not hit_flag: surviving_bullets.append(bullet) bullets surviving_bullets # 敌人与玩家碰撞检测 for enemy in enemies[:]: if enemy.rect.colliderect(player.rect): enemies.remove(enemy) life - 1 if life 0: game_over True break # 3. 绘制画面 screen.fill(COLOR_BLACK) player.draw(screen) for bullet in bullets: bullet.draw(screen) for enemy in enemies: enemy.draw(screen) draw_text(screen, f分数: {score}, 32, 10, 10) draw_text(screen, f生命: {life}, 32, SCREEN_WIDTH - 140, 10) if game_over: draw_text( screen, 游戏结束按 R 重新开始, 48, SCREEN_WIDTH // 2 - 200, SCREEN_HEIGHT // 2, COLOR_RED, ) pygame.display.flip() if __name__ __main__: main()4.3 运行方式在项目目录下执行python main.py如果一切正常会弹出一个 800x600 的黑色窗口窗口标题显示“枪战模拟器”。4.4 操作说明与预期表现进入游戏后玩家默认位于窗口底部中间是一个绿色矩形使用键盘方向键或 WASD 移动。按住空格键连续发射白色子弹。敌人是红色矩形从窗口顶部随机位置出现并向下移动。子弹击中敌人时敌人消失右上角分数增加 10 分。敌人碰到玩家或移动到屏幕底部消失时生命减少 1。生命归 0 后游戏进入结束状态屏幕中央显示“游戏结束按 R 重新开始”。按 R 键可以重置分数、生命和所有对象重新开始游戏。初始生命值为 5初始分数为 0。你可以根据实际体验调整ENEMY_INTERVAL、ENEMY_SPEED_MIN等参数来改变难度。4.5 核心代码解读重点看主循环的三个阶段事件处理阶段使用pygame.event.get()获取当前所有事件。窗口关闭事件pygame.QUIT必须处理否则点击右上角关闭按钮后程序无法退出。更新逻辑阶段先读取键盘状态再更新玩家、敌人和子弹。注意pygame.key.get_pressed()返回的是当前所有按键的状态适合处理“按住移动”“按住射击”这类持续操作。而pygame.event.get()中的KEYDOWN更适合处理“按一下触发一次”的操作比如 R 键重置游戏。列表过滤这一段bullets [bullet for bullet in bullets if not bullet.update()]这里的update()返回布尔值True表示子弹已经飞出屏幕。列表推导式会把仍然存活的子弹收集到新列表从而安全完成“删除越界对象”的操作。5. 常见问题与排查思路在实际运行过程中新手最容易遇到下面几类问题问题现象常见原因解决思路ModuleNotFoundError: No module named pygamePygame 未安装或 Python 环境不对执行pip install pygame确认和运行python main.py使用的是同一解释器窗口一闪而过主循环异常退出在入口函数外层捕获异常打印 traceback或检查代码是否提前调用了pygame.quit()画面全黑没有对象对象列表为空或绘制代码没执行在绘制循环里加打印检查bullets、enemies长度按住空格只发射一颗子弹没有使用pygame.key.get_pressed()确认射击逻辑写在“更新逻辑”阶段而不是KEYDOWN事件处理中碰撞命中了但分数没变命中后对象被删但分数更新逻辑写错位置检查score是否声明为全局变量碰撞分支里是否有score 10中文显示为方块默认字体不支持中文字符使用系统中文字体路径或改用英文显示5.1 关于 Pygame 默认字体pygame.font.SysFont(None, size)会选择一个系统默认字体但默认字体对中文的支持在不同操作系统上并不一致。如果游戏窗口中的“分数”“生命”等中文显示为方块可以把文字改成英文draw_text(screen, fScore: {score}, 32, 10, 10) draw_text(screen, fLife: {life}, 32, SCREEN_WIDTH - 140, 10)或者指定一个系统字体路径在 Windows 上可以尝试font pygame.font.SysFont(microsoftyahei, size)在 macOS 上可以尝试font pygame.font.SysFont(pingfang, size)这种方式依赖操作系统已安装的字体实际使用时要先确认字体名称存在。5.2 关于帧率和 CPU 占用如果游戏运行时 CPU 占用很高先检查主循环是否缺少clock.tick(FPS)。没有这一行游戏循环会以最快速度空转导致 CPU 占用居高不下。加上clock.tick(60)后Pygame 会把帧率控制在每秒 60 帧CPU 占用会明显下降。6. 最佳实践与工程建议6.1 对象职责单一本文的Player、Bullet、Enemy各自只负责自己的更新和绘制主循环负责整体调度。这是一个很值得保持的好习惯。如果将来增加道具、Boss、爆炸特效每个新对象都应遵循同样的“update draw”结构方便扩展。6.2 使用精灵组管理对象Pygame 提供了pygame.sprite.Sprite和pygame.sprite.Group机制。精灵组可以统一管理多个对象的更新、绘制和碰撞相比之下更贴近生产级 Pygame 项目。示例中为了降低理解成本采用了原生列表但建议进阶后迁移到精灵组from pygame.sprite import Sprite, Group class Player(Sprite): def __init__(self, x, y): super().__init__() self.image pygame.Surface((PLAYER_WIDTH, PLAYER_HEIGHT)) self.image.fill(COLOR_GREEN) self.rect self.image.get_rect(topleft(x, y))这样可以直接使用pygame.sprite.groupcollide()做批量碰撞检测。6.3 游戏状态机最简单的游戏至少应该区分“运行中”和“游戏结束”两种状态。本文用game_over布尔变量控制适用于项目早期。当游戏复杂度上来后可以引入状态机比如枚举类型定义MENU、PLAYING、PAUSED、GAME_OVER不同状态执行不同的逻辑分支可读性和可维护性会大幅提升。6.4 参数配置独立把所有魔法数值放在文件顶部集中管理是本文推荐的工程做法。PLAYER_SPEED、BULLET_COOLDOWN、ENEMY_INTERVAL等常量配置在一起调参时不需要翻找代码逻辑。再进一步可以把这些配置写入 JSON 配置文件甚至做成游戏内设置界面让玩家自己调整难度。6.5 碰撞精度与性能矩形碰撞对大多数 2D 射击游戏够用但它并不精确。如果后续要处理不规则形状对象可以缩小碰撞矩形让判定区域比视觉区域略小手感会更宽容。使用圆形碰撞计算距离并与半径之和比较。将宽碰撞拆成多个小矩形进行组合判定。性能方面当前敌人数量不多两层循环碰撞检测没有问题。如果敌人数量达到数百可以考虑网格碰撞检测或空间分区尽量减少两两比较次数。6.6 安全边界与防御性编程游戏项目同样需要防御性编程。读取玩家输入时要对按键状态做合理限制更新对象时要防止坐标越界碰撞处理时要避免在循环中反复修改同一个列表。此外生成随机敌人时如果SCREEN_WIDTH - ENEMY_WIDTH小于 0会随机失败所以常量之间的关系需要先验证。6.7 日志与调试Pygame 项目打印太多日志会影响帧率。建议只在关键节点打印比如if score % 100 0 and score 0: print(f当前分数达到 {score})更好的方案是使用 Python 内置的logging模块将调试信息输出到文件运行时窗口保持干净。7. 总结与学习路线本文围绕《枪战模拟器》这个项目完整实现了窗口创建、游戏循环、玩家移动、子弹发射、敌人随机生成、碰撞检测、计分与生命管理并且给出了可复制的 Python 代码。核心收获不只是代码本身而是“事件处理、更新、渲染”三者分离的思路以及用对象管理游戏实体的建模方法。接下来可以往以下几个方向继续深入把普通矩形替换成图片素材增加视觉表现力。加入爆炸粒子、音效和震动反馈。实现不同武器类型例如散射子弹、穿透子弹。增加关卡系统根据分数提高敌人刷新频率和移动速度。实现最高分记录保存到本地 JSON 文件。如果在运行过程中遇到问题先回到最基础的验证方式逐行检查是否安装 Pygame、是否在正确目录下启动、窗口是否能正常创建。把这些问题记录下来本身就是很好的工程积累。希望这篇文章对你有所帮助可以动手改一改参数跑出一个属于你自己的《枪战模拟器》版本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑