ARTICLE DETAIL

资讯详情

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

Python飞机大战游戏开发:从零到一掌握Pygame与面向对象编程

Python飞机大战游戏开发:从零到一掌握Pygame与面向对象编程 1. 项目缘起为什么用Python写飞机大战是绝佳的入门实践如果你刚接触Python或者学了一阵子语法却感觉无处下手不知道如何把那些变量、循环、函数组合成一个看得见摸得着的程序那么亲手写一个“飞机大战”游戏绝对是你打通任督二脉的最佳选择。这听起来可能有点老套毕竟这个游戏题材已经存在了几十年但恰恰是这种经典让它成为了检验编程基本功的“试金石”。我当年就是从这个小游戏开始真正理解了面向对象、事件循环和游戏逻辑的协同工作。今天我就带你从零开始用Python和Pygame库一步步构建一个属于你自己的、可玩性不错的飞机大战游戏。整个过程你会遇到很多新手必然踩的坑我也会把我当年趟过的雷、总结的技巧毫无保留地分享给你。为什么是飞机大战首先它的核心逻辑清晰玩家控制一个单位飞机躲避或消灭不断出现的敌人敌机同时可能有道具、分数、生命值等元素。这个模型几乎涵盖了所有游戏的基础框架。其次它的视觉反馈即时。你写的每一行代码无论是移动、射击还是碰撞检测都能立刻在屏幕上看到效果这种成就感是学习编程最强的驱动力。最后它所需的Python知识恰到好处类与对象、列表管理、事件处理、基本的数学计算这些都是核心但又不涉及过于复杂的算法非常适合巩固基础并迈向应用。我们将要完成的目标是一个窗口化的游戏玩家飞机可以用键盘方向键自由移动按空格键发射子弹屏幕上会随机生成不同种类的敌机它们以不同的速度和方式移动子弹击中敌机后敌机爆炸并消失玩家得分敌机碰到玩家飞机或抵达屏幕底部玩家损失生命值游戏有分数显示、生命值显示和简单的游戏结束逻辑。麻雀虽小五脏俱全。下面我们就从搭建战场环境开始。2. 战场搭建Pygame初始化与游戏窗口的创建万事开头难但配置Pygame环境其实并不复杂。很多人卡在第一步不是因为步骤繁琐而是因为对其中几个关键概念理解模糊导致代码跑起来黑屏或者直接报错。我们一步一步来我会把每个参数的意义和常见的坑点讲清楚。2.1 Pygame的安装与“导包”的讲究首先你需要确保安装了Python。建议使用Python 3.6及以上版本太老的版本可能会遇到库兼容性问题。安装Python时务必记得勾选“Add Python to PATH”这样才可以在命令行中直接使用python和pip命令。这是第一个容易踩的坑很多新手安装后无法在命令行调用Python就是因为漏了这一步。安装好Python后打开你的命令行Windows上是CMD或PowerShellMac/Linux上是Terminal安装Pygame。命令非常简单pip install pygame如果下载速度慢可以使用国内镜像源例如pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple看到“Successfully installed pygame-xx.x.x”就表示成功了。接下来我们创建一个新的Python文件比如命名为plane_war.py。文件开头我们需要导入必要的模块。这里就有个细节import pygame import sys import random from pygame.locals import *import pygame: 导入主模块。import sys: 提供访问Python解释器相关的变量和函数我们主要用它的sys.exit()来优雅地退出游戏。import random: 用来生成随机数控制敌机出现的位置、类型等。from pygame.locals import *: 这是一步关键的简化操作。它导入了Pygame中所有的常量比如键盘事件K_UP、K_SPACE退出事件QUIT等。这样我们在后面写代码时就不用写成冗长的pygame.K_SPACE直接写K_SPACE即可代码更简洁。有些人喜欢显式地导入每一个常量但对于我们这个小项目全部导入更省事。2.2 初始化Pygame与创建游戏窗口导入模块后第一件必须做的事就是初始化Pygamepygame.init()这行代码启动了Pygame的所有子系统显示、字体、声音等。不执行这一步后续任何与Pygame相关的操作都会失败。接着我们来定义游戏窗口的大小。这不仅仅是定义两个数字那么简单它关系到后面所有游戏元素的坐标计算。我建议使用全局常量来定义方便后续调整SCREEN_WIDTH 480 SCREEN_HEIGHT 700为什么是480x700这是一个比较接近早期竖版射击游戏的屏幕比例宽度适中高度足够提供纵深的移动感。你可以根据自己的喜好调整但要注意后续所有关于位置比如飞机初始位置、敌机生成范围的代码都需要基于这个尺寸进行计算。现在创建游戏窗口screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))pygame.display.set_mode()函数接收一个元组(width, height)返回一个Surface对象我们把它赋值给变量screen。这个screen就是我们的“画布”之后所有的图像绘制、更新显示都是在这个对象上进行的。为了让我们的游戏窗口有个标识我们给它设置一个标题pygame.display.set_caption(‘Python飞机大战 - 入门实战’)到这里一个基本的窗口就创建好了。但是如果你现在运行代码会发现窗口一闪而过。这是因为程序执行完这些代码后就立刻结束了。为了让窗口保持显示我们需要引入游戏中最核心的概念——游戏主循环。2.3 理解游戏主循环与事件处理游戏主循环是游戏的心脏它每秒运行几十甚至上百次取决于帧率在每一次循环中它主要做四件事处理事件Event Handling检查有没有发生什么事情比如用户按了键盘、点了鼠标、点了窗口关闭按钮。更新游戏状态Update根据事件和游戏逻辑更新所有游戏对象的位置、状态等。比如让敌机向下移动检查子弹是否击中了敌机。绘制画面Render把更新后的所有游戏对象按照正确的层次比如背景在最底下飞机在中间子弹和爆炸效果在最上面画到screen这个画布上。控制帧率Clock Tick控制循环的速度让游戏不会在一秒内运行太多次导致CPU占用过高或太慢导致游戏卡顿。我们先搭建一个最简化的主循环框架仅仅为了保持窗口打开并能响应关闭事件clock pygame.time.Clock() # 创建一个时钟对象用于控制帧率 running True # 一个标志变量控制循环是否继续 while running: # 1. 处理事件 for event in pygame.event.get(): # 获取当前帧发生的所有事件 if event.type QUIT: # 如果事件类型是“退出”用户点了窗口关闭按钮 running False # 将标志设为False循环将在下一次判断时退出 # 后续可以在这里添加键盘按下、松开等事件的处理 # 2. 更新游戏状态 (目前暂无内容) # 3. 绘制画面 (目前先填充一个背景色) screen.fill((0, 0, 0)) # 用RGB颜色(0,0,0)即黑色填充整个屏幕 # 4. 更新显示 pygame.display.update() # 将画布上的内容更新到显示器上 # 5. 控制帧率 clock.tick(60) # 将循环频率限制在每秒60帧 # 退出游戏 pygame.quit() sys.exit()现在运行代码你会看到一个黑色的窗口标题是我们设置的点击关闭按钮可以正常退出。这就是我们游戏的“舞台”和“引擎”。clock.tick(60)意味着我们试图让循环每秒运行60次即游戏以60FPS运行。这是一个流畅的标准帧率。注意screen.fill()必须在绘制所有其他图像之前调用。你可以把它理解为在画新的一帧之前先把整个画布擦干净涂上背景色。如果忘了这一步之前画的内容会残留导致画面拖影、混乱。3. 核心演员登场用面向对象思维设计飞机、子弹与敌机游戏里的一切活动元素我们都可以称之为“精灵”Sprite。在Pygame中虽然有一个pygame.sprite.Sprite基类可以继承来方便地管理但为了彻底理解其原理我们前期先自己用普通的Python类来实现。这能让你更清楚地知道每一行代码在做什么。3.1 玩家飞机类控制与边界我们先来创建玩家控制的飞机。创建一个Player类。class Player: def __init__(self): # 1. 加载飞机图片 self.image pygame.image.load(‘player.png’).convert_alpha() # 2. 获取飞机的矩形区域用于定位和碰撞检测 self.rect self.image.get_rect() # 3. 设置飞机的初始位置屏幕底部居中 self.rect.centerx SCREEN_WIDTH // 2 self.rect.bottom SCREEN_HEIGHT - 10 # 4. 设置飞机的移动速度 self.speed 5 def update(self, keys): 根据按键更新飞机位置 # 左右移动 if keys[K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[K_RIGHT] and self.rect.right SCREEN_WIDTH: self.rect.x self.speed # 上下移动 (增加游戏自由度) if keys[K_UP] and self.rect.top 0: self.rect.y - self.speed if keys[K_DOWN] and self.rect.bottom SCREEN_HEIGHT: self.rect.y self.speed def draw(self, screen): 将飞机绘制到指定的屏幕上 screen.blit(self.image, self.rect)关键点解析图片加载pygame.image.load()用于加载图片。.convert_alpha()方法非常关键它优化了图片的显示速度并且对于带有透明背景的PNG图片我们的飞机图片应该是去背景的PNG它能保留透明度信息。如果只用.convert()透明部分可能会变成黑色。矩形Rect对象这是Pygame中用于定位和碰撞检测的核心。image.get_rect()会返回一个覆盖图片表面的矩形。这个rect对象有x, y, top, bottom, left, right, centerx, centery等属性修改它们就能移动图片。我们所有的位置判断和碰撞检测都基于这个rect。边界检测在update方法中我们检查按键状态并在移动前判断rect的边界是否超出了屏幕范围left 0,right SCREEN_WIDTH。这是防止飞机“飞出”屏幕的必要逻辑。blit方法这是Pygame的绘制方法意为“位块传输”。screen.blit(image, rect)的意思是把image这个图像绘制到screen画布的rect所指定的位置。现在在主循环之前实例化玩家飞机并在主循环的更新和绘制阶段调用它player Player() while running: # ... 事件处理部分 ... # 获取当前所有按键的持续按压状态与单次事件不同 keys pygame.key.get_pressed() # 更新游戏状态 player.update(keys) # 传入按键状态 # 绘制画面 screen.fill((0, 0, 0)) player.draw(screen) # 绘制玩家飞机 # ... 更新显示和控制帧率 ...运行代码你应该能用方向键控制一架飞机在黑色窗口里移动了注意这里我们用了pygame.key.get_pressed()来获取按键的持续状态这比在事件循环里处理KEYDOWN事件更适合需要持续移动的场景。3.2 子弹类发射与生命周期管理子弹比飞机简单它通常只需要垂直向上移动飞出屏幕后就应该被销毁以节省资源。我们同样用一个类来管理。class Bullet: def __init__(self, x, y): # 子弹可以是一个小矩形也可以是一张图片。这里先用矩形演示更高效。 self.rect pygame.Rect(0, 0, 3, 15) # 宽3像素高15像素的矩形 self.rect.centerx x # 初始x位置由传入参数决定飞机中心 self.rect.top y # 初始y位置是飞机的顶部 self.speed 10 # 子弹速度比飞机快 self.color (255, 255, 0) # 黄色 def update(self): 子弹向上移动 self.rect.y - self.speed # 如果子弹完全飞出屏幕顶部标记为待删除 if self.rect.bottom 0: return True # 返回True表示该子弹应该被移除 return False def draw(self, screen): 绘制子弹矩形 pygame.draw.rect(screen, self.color, self.rect)子弹的管理需要一个列表因为同时可能存在多发子弹。我们在主循环外创建一个空列表bullets []。发射子弹的逻辑放在事件处理部分响应单次按键而更新和绘制所有子弹的逻辑放在主循环的更新和绘制阶段。修改主循环的事件处理部分和更新部分bullets [] # 在主循环前定义 while running: for event in pygame.event.get(): if event.type QUIT: running False # 监听空格键按下事件单次触发 elif event.type KEYDOWN: if event.key K_SPACE: # 创建一颗新子弹位置在玩家飞机顶部中央 new_bullet Bullet(player.rect.centerx, player.rect.top) bullets.append(new_bullet) keys pygame.key.get_pressed() player.update(keys) # 更新所有子弹 bullets_to_remove [] # 创建一个临时列表存放需要移除的子弹 for bullet in bullets: if bullet.update(): # 如果update返回True说明子弹该移除了 bullets_to_remove.append(bullet) # 从原列表中移除待删除的子弹 for bullet in bullets_to_remove: bullets.remove(bullet) # 绘制 screen.fill((0, 0, 0)) player.draw(screen) for bullet in bullets: bullet.draw(screen)这里引入了一个重要的编程模式在遍历列表时不要直接删除列表中的元素。这会导致迭代器错乱可能引发程序崩溃或逻辑错误。正确的做法是在遍历时记录下需要删除的元素放入另一个列表bullets_to_remove遍历结束后再统一删除。这是一个非常经典的坑。3.3 敌机类多样性与随机生成敌机是游戏的挑战来源。为了让游戏更有趣我们可以设计多种敌机比如小型、中型、大型它们有不同的生命值、速度、分数。这里我们先实现一种基础敌机但把结构设计成易于扩展的。class Enemy: def __init__(self): # 随机选择敌机类型这里先模拟一种后续可扩展 # 假设我们有两种敌机图片 ‘enemy1.png‘ 和 ’enemy2.png‘ enemy_type random.choice([‘enemy1.png‘, ’enemy2.png‘]) self.image pygame.image.load(enemy_type).convert_alpha() self.rect self.image.get_rect() # 敌机从屏幕顶部随机位置出现 self.rect.x random.randint(0, SCREEN_WIDTH - self.rect.width) self.rect.y random.randint(-100, -40) # 从屏幕上方之外随机高度开始 # 根据类型设置不同属性 if enemy_type ‘enemy1.png‘: self.speed random.randint(2, 4) # 速度慢 self.score 100 # 击毁得分 else: self.speed random.randint(4, 6) # 速度快 self.score 200 def update(self): 敌机向下移动 self.rect.y self.speed # 如果敌机完全移出屏幕底部标记为待删除 if self.rect.top SCREEN_HEIGHT: return True return False def draw(self, screen): screen.blit(self.image, self.rect)敌机的生成需要定时器。我们不能在主循环的每一帧都生成敌机那样就太多了。Pygame提供了pygame.time.set_timer()来创建自定义事件。我们在初始化后设置一个定时器# 创建一个自定义事件常量 ENEMY_SPAWN_EVENT pygame.USEREVENT 1 # 设置定时器每500毫秒0.5秒触发一次ENEMY_SPAWN_EVENT事件 pygame.time.set_timer(ENEMY_SPAWN_EVENT, 500)然后在主循环的事件处理部分监听这个事件并生成敌机enemies [] # 在主循环前定义敌机列表 while running: for event in pygame.event.get(): if event.type QUIT: running False elif event.type KEYDOWN: if event.key K_SPACE: new_bullet Bullet(player.rect.centerx, player.rect.top) bullets.append(new_bullet) # 监听敌机生成事件 elif event.type ENEMY_SPAWN_EVENT: new_enemy Enemy() enemies.append(new_enemy)更新和绘制敌机的逻辑与子弹类似也需要管理移除列表。至此我们的舞台上就有了可以移动的玩家、可以发射的子弹、以及不断从天而降的敌机。但它们现在还只是各自移动彼此之间没有互动。接下来我们要让游戏“活”起来实现碰撞与交互。4. 游戏灵魂碰撞检测、分数系统与状态管理一个游戏之所以好玩在于其内部的规则和反馈。对于飞机大战核心规则就是“子弹打中敌机得分敌机撞到玩家扣血”。4.1 碰撞检测的实现与优化碰撞检测是游戏开发中的核心算法。Pygame提供了非常方便的矩形碰撞检测方法rect.colliderect(other_rect)它检查两个矩形是否重叠。对于我们的需求需要检测“每一颗子弹”和“每一个敌机”是否碰撞以及“玩家”和“每一个敌机”是否碰撞。这是一个典型的双重循环问题。我们把它放在主循环的更新阶段在更新了所有对象的位置之后进行。# 在主循环的更新部分player.update()之后 # 子弹与敌机的碰撞检测 hit_enemies [] # 记录被击中的敌机 hit_bullets [] # 记录击中了敌机的子弹需要移除 for bullet in bullets: for enemy in enemies: if bullet.rect.colliderect(enemy.rect): # 碰撞发生 hit_enemies.append(enemy) hit_bullets.append(bullet) # 注意一颗子弹可能击中多个敌机如果敌机重叠一个敌机也可能被多颗子弹击中。 # 这里我们先做简单处理一颗子弹击中一个敌机后子弹和敌机都消失。 # 所以一旦检测到碰撞可以立即跳出内层循环处理下一颗子弹。 break # 这颗子弹已经击中了一个敌机不再检查其他敌机 # 移除被击中的敌机和子弹 for enemy in hit_enemies: if enemy in enemies: # 防止重复移除 enemies.remove(enemy) # 这里可以增加得分我们稍后处理 for bullet in hit_bullets: if bullet in bullets: bullets.remove(bullet) # 玩家与敌机的碰撞检测 for enemy in enemies: if player.rect.colliderect(enemy.rect): # 玩家被撞到 # 1. 移除撞到的敌机 enemies.remove(enemy) # 2. 玩家生命值减1我们稍后实现生命值系统 # 3. 可以添加一个无敌时间或闪烁效果避免连续碰撞 # 我们先简单处理打印信息 print(“玩家被击中”) break # 一帧内只处理一次碰撞避免连续扣血注意上面的碰撞检测逻辑在敌机数量很多时比如上百个性能会成为一个问题因为它是O(n*m)的复杂度。这就是为什么很多大型游戏会使用空间划分算法如四叉树、网格划分来优化碰撞检测。但对于我们这个小游戏敌机和子弹数量有限双重循环完全够用。这是一个重要的认知不要过早优化先让功能跑起来。4.2 分数与生命值系统的搭建游戏需要给玩家正反馈得分和风险警示生命值。我们需要创建两个全局变量来跟踪它们并在屏幕上显示出来。# 在主循环之前初始化 score 0 lives 3 # 初始生命值 # 加载字体如果系统没有指定字体Pygame有默认字体但可能不支持中文 # 为了简单我们使用Pygame的默认字体 font pygame.font.Font(None, 36) # None表示使用默认字体36是字号在碰撞检测击中敌机时增加分数# 在“移除被击中的敌机”循环内 for enemy in hit_enemies: if enemy in enemies: enemies.remove(enemy) score enemy.score # 使用敌机自身的分数属性在玩家与敌机碰撞时减少生命值并判断游戏是否结束# 在“玩家与敌机碰撞检测”部分 for enemy in enemies: if player.rect.colliderect(enemy.rect): enemies.remove(enemy) lives - 1 print(f“被击中剩余生命{lives}”) if lives 0: print(“游戏结束”) running False # 结束游戏主循环 break现在我们需要在每一帧把分数和生命值画到屏幕上。在绘制部分的最后pygame.display.update()之前添加# 绘制分数和生命值文本 score_text font.render(f‘Score: {score}‘, True, (255, 255, 255)) # 白色文字 lives_text font.render(f‘Lives: {lives}‘, True, (255, 255, 255)) screen.blit(score_text, (10, 10)) # 左上角 screen.blit(lives_text, (SCREEN_WIDTH - lives_text.get_width() - 10, 10)) # 右上角font.render()方法接收三个主要参数文本内容、抗锯齿True/False、颜色RGB元组。它返回一个Surface对象然后用blit绘制到屏幕上。get_width()方法用于获取文本表面的宽度方便我们进行右对齐定位。4.3 游戏状态的细化开始、进行中、结束目前我们的游戏只有一个“进行中”的状态。一个完整的游戏应该有开始界面和结束界面。我们可以用一个简单的状态变量game_state来控制比如‘start‘, ’playing‘, ’game_over‘。我们在主循环前定义状态game_state ‘start‘ # 初始为开始状态然后主循环的结构需要根据状态来分支while running: # 事件处理是通用的无论什么状态都需要能退出游戏 for event in pygame.event.get(): if event.type QUIT: running False # 在开始状态按任意键进入游戏 if game_state ‘start‘ and event.type KEYDOWN: game_state ‘playing‘ # 重置游戏数据 score 0 lives 3 bullets.clear() enemies.clear() player.rect.centerx SCREEN_WIDTH // 2 player.rect.bottom SCREEN_HEIGHT - 10 # 在游戏结束状态按任意键回到开始 elif game_state ‘game_over‘ and event.type KEYDOWN: game_state ‘start‘ # 根据状态更新和绘制 screen.fill((0, 0, 0)) # 每一帧都先清屏 if game_state ‘start‘: # 绘制开始界面 title_font pygame.font.Font(None, 64) title title_font.render(‘PYTHON PLANE WAR‘, True, (255, 215, 0)) hint_font pygame.font.Font(None, 36) hint hint_font.render(‘Press Any Key to Start‘, True, (255, 255, 255)) screen.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, SCREEN_HEIGHT//3)) screen.blit(hint, (SCREEN_WIDTH//2 - hint.get_width()//2, SCREEN_HEIGHT//2)) elif game_state ‘playing‘: # 原有的游戏逻辑全部放在这里 keys pygame.key.get_pressed() if keys[K_SPACE]: # 这里可以改为连续发射但需要控制发射频率否则子弹太密集 # 我们用一个简单的计数器来控制 pass # 连续发射逻辑稍后添加 player.update(keys) # ... 子弹、敌机的更新、碰撞检测、绘制 ... # 绘制玩家、子弹、敌机 player.draw(screen) for bullet in bullets: bullet.draw(screen) for enemy in enemies: enemy.draw(screen) # 绘制分数和生命值 score_text font.render(f‘Score: {score}‘, True, (255, 255, 255)) lives_text font.render(f‘Lives: {lives}‘, True, (255, 255, 255)) screen.blit(score_text, (10, 10)) screen.blit(lives_text, (SCREEN_WIDTH - lives_text.get_width() - 10, 10)) # 判断游戏是否结束 if lives 0: game_state ‘game_over‘ elif game_state ‘game_over‘: # 绘制结束界面 over_font pygame.font.Font(None, 72) over_text over_font.render(‘GAME OVER‘, True, (255, 0, 0)) final_score_font pygame.font.Font(None, 48) final_score_text final_score_font.render(f‘Final Score: {score}‘, True, (255, 255, 255)) restart_hint font.render(‘Press Any Key to Restart‘, True, (200, 200, 200)) screen.blit(over_text, (SCREEN_WIDTH//2 - over_text.get_width()//2, SCREEN_HEIGHT//3)) screen.blit(final_score_text, (SCREEN_WIDTH//2 - final_score_text.get_width()//2, SCREEN_HEIGHT//2)) screen.blit(restart_hint, (SCREEN_WIDTH//2 - restart_hint.get_width()//2, SCREEN_HEIGHT*2//3)) pygame.display.update() clock.tick(60)这样我们就实现了一个简单的状态机游戏有了基本的开始和结束流程。代码看起来变长了但逻辑更清晰更接近一个完整的产品。5. 打磨与优化音效、连续发射、难度提升与性能基础功能完成后我们可以添加一些“调味料”让游戏体验更上一层楼。同时也要关注一些潜在的优化点。5.1 添加音效与背景音乐声音是游戏体验不可或缺的一环。Pygame处理音频也很方便。首先准备一些音效文件.wav或.ogg格式比如shoot.wav射击音效、explosion.wav爆炸音效、bgm.ogg背景音乐。在主循环前加载音效# 加载音效 shoot_sound pygame.mixer.Sound(‘shoot.wav‘) explosion_sound pygame.mixer.Sound(‘explosion.wav‘) # 设置音效音量可选 shoot_sound.set_volume(0.3) explosion_sound.set_volume(0.5) # 加载并循环播放背景音乐 pygame.mixer.music.load(‘bgm.ogg‘) pygame.mixer.music.set_volume(0.2) # 背景音乐音量调低些 pygame.mixer.music.play(-1) # -1表示无限循环然后在适当的地方播放音效发射子弹时在创建新子弹的代码后添加shoot_sound.play()。敌机被击中时在从hit_enemies列表中移除敌机并加分后添加explosion_sound.play()。玩家被击中时也可以播放一个不同的音效。注意音效文件不宜过大.wav文件最好是单声道、低采样率的以减小内存占用。背景音乐可以用.ogg这种压缩格式。如果游戏卡顿可以检查是否是音效加载或播放占用了过多资源。5.2 实现连续发射与发射间隔控制之前我们按一次空格发射一颗子弹这不够爽快。我们可以改成按住空格连续发射但需要控制发射频率否则子弹会过于密集像一道激光失去乐趣。我们引入一个“发射冷却时间”的概念。在Player类或全局变量中增加一个计数器# 在主循环前定义 last_shot_time 0 # 上一次发射的时间用pygame.time.get_ticks()获取 shot_cooldown 250 # 发射冷却时间单位毫秒250ms发射一次在主循环的playing状态中处理连续发射if game_state ‘playing‘: keys pygame.key.get_pressed() current_time pygame.time.get_ticks() # 连续发射逻辑 if keys[K_SPACE]: if current_time - last_shot_time shot_cooldown: new_bullet Bullet(player.rect.centerx, player.rect.top) bullets.append(new_bullet) shoot_sound.play() # 播放音效 last_shot_time current_time # 更新上次发射时间 # ... 其他更新逻辑 ...这样玩家按住空格键每250毫秒0.25秒就会自动发射一颗子弹既保持了连发的爽快感又不会过于变态。5.3 动态难度与敌机多样性随着游戏进行如果敌机永远是一样的速度和频率游戏会变得单调。我们可以让游戏随着时间或分数增加而变难。加快敌机生成频率我们可以根据分数来调整ENEMY_SPAWN_EVENT的定时器间隔。但注意pygame.time.set_timer()是全局设置调整它会影响到所有后续事件。一个更灵活的方法是不改变定时器而是在生成敌机时根据分数有一定概率生成更快的敌机或者一次生成多个。增加敌机种类我们之前设计的Enemy类已经有了类型区分。我们可以创建更多种类比如会左右摇摆移动的敌机、会发射子弹的Boss敌机等。这需要扩展Enemy类的update方法和碰撞逻辑。敌机血量可以为某些敌机如Boss设置hp属性需要被击中多次才会爆炸。在碰撞检测时不立即移除敌机而是减少其hp直到hp0。这里提供一个简单的动态难度思路根据分数提高敌机的基础速度。class Enemy: def __init__(self, base_speed_increase0): # ... 加载图片等初始化 ... # 在计算速度时加上一个基于游戏进程的增量 self.speed base_speed_increase # ...在生成敌机时传入一个根据分数计算出的增量# 在生成敌机的代码处 speed_increment min(score // 1000, 5) # 每得1000分速度1最大5 new_enemy Enemy(speed_increment)5.4 性能考量与代码组织建议当游戏元素越来越多时需要注意性能。图片加载优化所有图片pygame.image.load()应该在游戏初始化时一次性加载好并存储在变量中而不是在每一帧的循环里加载。我们之前的代码在Enemy的__init__里加载图片如果敌机生成频繁这会成为性能瓶颈。正确的做法是创建一个图片字典。# 游戏初始化部分 IMAGES { ‘player‘: pygame.image.load(‘player.png‘).convert_alpha(), ‘enemy1‘: pygame.image.load(‘enemy1.png‘).convert_alpha(), ‘enemy2‘: pygame.image.load(‘enemy2.png‘).convert_alpha(), ‘bullet‘: pygame.image.load(‘bullet.png‘).convert_alpha(), # 如果用图片子弹 } # 在类初始化时使用 self.image IMAGES[‘player‘]对象池Object Pooling对于频繁创建和销毁的对象如子弹、敌机反复的append和remove操作以及Python的垃圾回收会带来开销。对象池是一种高级优化技术预先创建一批对象放入“池”中使用时从池中取出激活不用时放回池中休眠而不是真正销毁。对于初学者项目如果帧率没有明显下降可以暂不考虑但这是一个重要的优化思想。代码模块化现在所有代码都写在一个文件里会显得很长。更好的做法是将Player、Bullet、Enemy类分别放到单独的文件如player.py、bullet.py、enemy.py中然后用from player import Player等方式导入。主文件main.py只负责游戏主循环、事件分发和状态管理。这会让代码更清晰易于维护和扩展。走到这一步你已经拥有了一个功能完整、可玩性不错的Python飞机大战游戏。从零到一的过程你实践了游戏循环、事件处理、面向对象编程、碰撞检测、状态机等核心概念。这远不止是一个小游戏它是一个坚实的起点从这里出发你可以尝试添加更多功能比如关卡设计、道具系统、更华丽的粒子爆炸效果甚至将其打包成可执行文件分享给朋友。编程的乐趣就在于这种从无到有、不断打磨创造的成就感。希望这个详细的构建过程能成为你Python实践路上的一块坚实垫脚石。
返回列表