ARTICLE DETAIL

资讯详情

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

Python Pygame实战:从零开发双人坦克对战游戏

Python Pygame实战:从零开发双人坦克对战游戏 最近在开发一个简单的2D坦克对战游戏时发现很多教程要么过于复杂要么只讲理论缺少完整代码。为了让新手也能快速上手我决定整理一份从零开始的《坦克对决赛2》实战开发指南。本文将带你一步步用Python的Pygame库实现一个功能完整的双人对战坦克游戏涵盖地图绘制、坦克控制、子弹发射、碰撞检测和胜负判定等核心模块。无论你是刚学完Python基础想找项目练手还是想了解2D游戏开发的基本流程这篇文章都能提供一套可直接运行的代码和清晰的实现思路。1. 游戏核心概念与设计思路在开始写代码之前我们先明确一下这个“坦克对决赛2”游戏要做什么。本质上它是一个双人本地对战的2D射击游戏。两名玩家分别控制一辆坦克在一个有障碍物的地图上移动、射击目标是击毁对方的坦克。核心游戏机制包括双人控制两个玩家使用不同的键盘按键控制各自的坦克。物理与碰撞坦克不能穿过墙壁和地图边界子弹击中墙壁会消失击中对方坦克则造成伤害。战斗系统每辆坦克有生命值被子弹击中会扣血血量为零则被摧毁游戏结束。障碍物系统地图上存在不可摧毁的墙壁用于增加游戏的策略性和趣味性。为什么选择PygamePygame是一个基于SDL库的Python游戏开发模块它封装了图像、声音、事件处理等底层操作让开发者能更专注于游戏逻辑。对于这种2D小游戏来说Pygame轻量、易学且完全能满足我们的需求。游戏状态流程图简化游戏初始化 - 显示开始界面 - 进入游戏主循环 主循环内 处理键盘事件 - 更新坦克位置/状态 - 更新子弹位置 - 检测所有碰撞 根据碰撞结果更新生命值、子弹状态等 - 绘制当前帧所有元素 判断是否有一方生命值归零 - 是则跳转到结束界面否则继续循环2. 开发环境准备与项目结构2.1 环境与工具操作系统Windows 10/11, macOS 或 Linux均可。Python版本建议使用 Python 3.8 及以上版本。本文示例在 Python 3.9 上测试通过。核心库pygame。我们将仅依赖这个库。IDE或编辑器任何你喜欢的即可如 PyCharm, VS Code, 甚至记事本。2.2 安装Pygame打开你的终端或命令提示符使用pip安装pip install pygame安装完成后可以通过python -m pygame.examples.aliens命令运行一个示例游戏来验证安装是否成功。2.3 项目目录结构在开始前我们先规划好项目文件夹这有助于代码管理tank_duel_2/ │ ├── main.py # 游戏主程序入口 ├── game_config.py # 游戏参数配置如屏幕大小、颜色、速度等 ├── sprites/ │ ├── __init__.py │ ├── tank.py # 坦克类 │ ├── bullet.py # 子弹类 │ └── wall.py # 墙壁类 ├── assets/ # 资源文件夹 │ ├── images/ # 存放坦克、墙壁等图片可选初期可用色块代替 │ └── sounds/ # 存放音效可选 └── maps/ # 地图配置文件可选初期可硬编码初期为了简化我们可以不使用图片和音效用Pygame的绘图功能绘制简单的色块来代替。game_config.py集中管理所有可调参数方便后续调整游戏难度和体验。3. 核心类与游戏逻辑拆解我们将游戏中的主要元素抽象为类Class这是面向对象编程在游戏开发中的典型应用。3.1 游戏配置 (game_config.py)首先创建一个集中管理所有常量的配置文件。# game_config.py import pygame # 屏幕设置 SCREEN_WIDTH 800 SCREEN_HEIGHT 600 FPS 60 # 帧率控制游戏速度 # 颜色定义 (RGB) BLACK (0, 0, 0) WHITE (255, 255, 255) RED (255, 0, 0) GREEN (0, 255, 0) BLUE (0, 120, 255) YELLOW (255, 255, 0) GRAY (128, 128, 128) LIGHT_BLUE (173, 216, 230) # 坦克设置 TANK_WIDTH 40 TANK_HEIGHT 40 TANK_SPEED 5 TANK_HEALTH 100 # 子弹设置 BULLET_WIDTH 6 BULLET_HEIGHT 6 BULLET_SPEED 10 BULLET_DAMAGE 25 # 墙壁设置 WALL_WIDTH 50 WALL_HEIGHT 50 # 玩家控制键位玩家1WASD空格 玩家2方向键右Ctrl PLAYER1_KEYS { up: pygame.K_w, down: pygame.K_s, left: pygame.K_a, right: pygame.K_d, fire: pygame.K_SPACE } PLAYER2_KEYS { up: pygame.K_UP, down: pygame.K_DOWN, left: pygame.K_LEFT, right: pygame.K_RIGHT, fire: pygame.K_RCTRL }3.2 坦克类 (sprites/tank.py)坦克是游戏的核心单位需要处理移动、转向、射击和受伤逻辑。# sprites/tank.py import pygame from game_config import * class Tank(pygame.sprite.Sprite): def __init__(self, x, y, color, control_keys, player_num): super().__init__() self.player_num player_num # 玩家编号1或2 self.color color self.control_keys control_keys # 创建坦克图像一个矩形加一个炮管 self.image pygame.Surface((TANK_WIDTH, TANK_HEIGHT), pygame.SRCALPHA) pygame.draw.rect(self.image, self.color, (0, 0, TANK_WIDTH, TANK_HEIGHT), border_radius5) # 绘制炮管矩形在坦克前方 pygame.draw.rect(self.image, BLACK, (TANK_WIDTH//2 - 2, 0, 4, TANK_HEIGHT//2)) self.rect self.image.get_rect(center(x, y)) self.direction pygame.math.Vector2(0, -1) # 初始方向向上 self.speed TANK_SPEED self.health TANK_HEALTH self.last_shot_time 0 # 用于控制射击冷却 self.shot_cooldown 500 # 发射子弹的冷却时间毫秒 def update(self, keys_pressed, walls_group, all_sprites, bullets_group): 根据按键更新坦克状态 move_vec pygame.math.Vector2(0, 0) # 处理移动 if keys_pressed[self.control_keys[up]]: move_vec.y - 1 if keys_pressed[self.control_keys[down]]: move_vec.y 1 if keys_pressed[self.control_keys[left]]: move_vec.x - 1 if keys_pressed[self.control_keys[right]]: move_vec.x 1 # 标准化移动向量防止斜向移动更快 if move_vec.length() 0: move_vec move_vec.normalize() # 根据移动方向更新坦克朝向这是一个简化处理更复杂可以旋转图片 self.direction move_vec.copy() # 更新炮管绘制这里简化实际需要根据方向旋转图像 self._redraw_tank() # 尝试移动并检测与墙壁的碰撞 self.rect.x move_vec.x * self.speed self._handle_collision(move_vec.x, 0, walls_group) self.rect.y move_vec.y * self.speed self._handle_collision(0, move_vec.y, walls_group) # 处理射击 current_time pygame.time.get_ticks() if keys_pressed[self.control_keys[fire]] and current_time - self.last_shot_time self.shot_cooldown: self.shoot(all_sprites, bullets_group) self.last_shot_time current_time def _handle_collision(self, dx, dy, walls_group): 处理与墙壁的碰撞使坦克无法穿过 for wall in pygame.sprite.spritecollide(self, walls_group, False): if dx 0: # 向右移动时撞上 self.rect.right wall.rect.left elif dx 0: # 向左移动时撞上 self.rect.left wall.rect.right if dy 0: # 向下移动时撞上 self.rect.bottom wall.rect.top elif dy 0: # 向上移动时撞上 self.rect.top wall.rect.bottom def _redraw_tank(self): 根据当前方向重绘坦克图像简化版仅改变炮管方向 self.image.fill((0, 0, 0, 0)) # 透明背景 pygame.draw.rect(self.image, self.color, (0, 0, TANK_WIDTH, TANK_HEIGHT), border_radius5) # 根据方向向量计算炮管绘制位置和角度这里简化只画一个方向的矩形 # 更高级的实现应使用pygame.transform.rotate 炮管_length TANK_HEIGHT // 2 炮管_end_x TANK_WIDTH // 2 self.direction.x * 炮管_length 炮管_end_y TANK_HEIGHT // 2 self.direction.y * 炮管_length pygame.draw.line(self.image, BLACK, (TANK_WIDTH//2, TANK_HEIGHT//2), (炮管_end_x, 炮管_end_y), 4) def shoot(self, all_sprites, bullets_group): 从炮管口创建一颗子弹 from .bullet import Bullet # 避免循环导入 # 子弹起始位置在炮管口 bullet_start_x self.rect.centerx self.direction.x * (TANK_HEIGHT//2 5) bullet_start_y self.rect.centery self.direction.y * (TANK_HEIGHT//2 5) bullet Bullet(bullet_start_x, bullet_start_y, self.direction, self.player_num) all_sprites.add(bullet) bullets_group.add(bullet) def take_damage(self, damage): 坦克受到伤害 self.health - damage if self.health 0: self.kill() # 从所有精灵组中移除 return True # 返回True表示坦克被摧毁 return False3.3 子弹类 (sprites/bullet.py)子弹需要直线飞行并检测与坦克和墙壁的碰撞。# sprites/bullet.py import pygame from game_config import * class Bullet(pygame.sprite.Sprite): def __init__(self, x, y, direction, owner_num): super().__init__() self.owner_num owner_num # 子弹属于哪个玩家用于避免误伤自己 self.direction direction self.image pygame.Surface((BULLET_WIDTH, BULLET_HEIGHT)) self.image.fill(YELLOW) self.rect self.image.get_rect(center(x, y)) self.speed BULLET_SPEED def update(self, walls_group, tanks_group): 更新子弹位置并检测碰撞 # 移动 self.rect.x self.direction.x * self.speed self.rect.y self.direction.y * self.speed # 检查是否飞出屏幕 if (self.rect.right 0 or self.rect.left SCREEN_WIDTH or self.rect.bottom 0 or self.rect.top SCREEN_HEIGHT): self.kill() return # 检测与墙壁的碰撞 if pygame.sprite.spritecollideany(self, walls_group): self.kill() # 击中墙壁子弹消失 return # 检测与坦克的碰撞排除发射者自己 hit_tanks pygame.sprite.spritecollide(self, tanks_group, False) for tank in hit_tanks: if tank.player_num ! self.owner_num: # 不能打自己 tank.take_damage(BULLET_DAMAGE) self.kill() # 击中坦克子弹消失 break3.4 墙壁类 (sprites/wall.py)墙壁是简单的障碍物不可穿越。# sprites/wall.py import pygame from game_config import * class Wall(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image pygame.Surface((WALL_WIDTH, WALL_HEIGHT)) self.image.fill(GRAY) # 可以给墙壁加个边框看起来更立体 pygame.draw.rect(self.image, BLACK, self.image.get_rect(), 2) self.rect self.image.get_rect(topleft(x, y))4. 完整游戏主程序与实战现在我们将所有模块组合起来编写游戏的主循环。4.1 主程序入口 (main.py)这是游戏的启动文件负责初始化、游戏循环和状态管理。# main.py import pygame import sys from game_config import * from sprites.tank import Tank from sprites.wall import Wall from sprites.bullet import Bullet # 初始化Pygame pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(坦克对决赛2 - 双人本地对战) clock pygame.time.Clock() # 字体用于显示生命值和信息 font pygame.font.SysFont(None, 36) large_font pygame.font.SysFont(None, 72) def create_map(): 创建游戏地图墙壁布局 walls pygame.sprite.Group() # 创建边界墙 for x in range(0, SCREEN_WIDTH, WALL_WIDTH): Wall(x, 0).add(walls) Wall(x, SCREEN_HEIGHT - WALL_HEIGHT).add(walls) for y in range(WALL_HEIGHT, SCREEN_HEIGHT - WALL_HEIGHT, WALL_HEIGHT): Wall(0, y).add(walls) Wall(SCREEN_WIDTH - WALL_WIDTH, y).add(walls) # 创建地图中间的障碍物一个简单的“田”字形布局 center_x, center_y SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 # 左上障碍块 for x in range(center_x - 150, center_x - 50, WALL_WIDTH): for y in range(center_y - 150, center_y - 50, WALL_HEIGHT): Wall(x, y).add(walls) # 右下障碍块 for x in range(center_x 50, center_x 150, WALL_WIDTH): for y in range(center_y 50, center_y 150, WALL_HEIGHT): Wall(x, y).add(walls) # 中间横向障碍墙 for x in range(center_x - 100, center_x 100, WALL_WIDTH): Wall(x, center_y).add(walls) return walls def draw_health_bar(surface, x, y, health, max_health): 绘制生命值条 bar_width 100 bar_height 10 fill (health / max_health) * bar_width outline_rect pygame.Rect(x, y, bar_width, bar_height) fill_rect pygame.Rect(x, y, fill, bar_height) color GREEN if health max_health * 0.5 else YELLOW if health max_health * 0.2 else RED pygame.draw.rect(surface, color, fill_rect) pygame.draw.rect(surface, WHITE, outline_rect, 2) def game_loop(): 游戏主循环 # 创建精灵组 all_sprites pygame.sprite.Group() walls create_map() tanks pygame.sprite.Group() bullets pygame.sprite.Group() # 将墙壁添加到总精灵组 all_sprites.add(walls) # 创建两辆坦克 tank1 Tank(SCREEN_WIDTH * 0.25, SCREEN_HEIGHT * 0.5, BLUE, PLAYER1_KEYS, 1) tank2 Tank(SCREEN_WIDTH * 0.75, SCREEN_HEIGHT * 0.5, RED, PLAYER2_KEYS, 2) tanks.add(tank1, tank2) all_sprites.add(tank1, tank2) running True game_over False winner None while running: # 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False if event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: running False if game_over and event.key pygame.K_r: # 按R键重新开始 return True # 返回True表示重新开始游戏 if not game_over: # 获取当前所有按键状态 keys pygame.key.get_pressed() # 更新坦克传入按键状态和碰撞组 tank1.update(keys, walls, all_sprites, bullets) tank2.update(keys, walls, all_sprites, bullets) # 更新子弹 bullets.update(walls, tanks) # 检查游戏是否结束 if not tank1.alive(): game_over True winner 红方坦克 (玩家2)! elif not tank2.alive(): game_over True winner 蓝方坦克 (玩家1)! # 绘制 screen.fill(LIGHT_BLUE) # 背景色 all_sprites.draw(screen) bullets.draw(screen) # 绘制生命值条和信息 draw_health_bar(screen, 20, 20, tank1.health, TANK_HEALTH) draw_health_bar(screen, SCREEN_WIDTH - 120, 20, tank2.health, TANK_HEALTH) # 绘制玩家提示 p1_text font.render(玩家1: WASD移动, 空格射击, True, BLUE) p2_text font.render(玩家2: 方向键移动, 右Ctrl射击, True, RED) screen.blit(p1_text, (20, SCREEN_HEIGHT - 60)) screen.blit(p2_text, (SCREEN_WIDTH - p2_text.get_width() - 20, SCREEN_HEIGHT - 60)) # 如果游戏结束显示胜利者 if game_over: overlay pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) # 半透明黑色遮罩 screen.blit(overlay, (0, 0)) win_text large_font.render(f{winner} 胜利!, True, YELLOW) restart_text font.render(按 R 键重新开始ESC 键退出, True, WHITE) screen.blit(win_text, (SCREEN_WIDTH//2 - win_text.get_width()//2, SCREEN_HEIGHT//2 - 50)) screen.blit(restart_text, (SCREEN_WIDTH//2 - restart_text.get_width()//2, SCREEN_HEIGHT//2 30)) # 刷新屏幕 pygame.display.flip() clock.tick(FPS) # 控制帧率 return False # 返回False表示退出游戏 def main(): 游戏主函数处理游戏重启 restart True while restart: restart game_loop() pygame.quit() sys.exit() if __name__ __main__: main()4.2 运行与验证确保你的项目目录结构正确并且所有文件都已创建。在终端中进入项目根目录tank_duel_2。运行命令python main.py如果一切正常你将看到一个游戏窗口。两名玩家可以分别使用WASD空格和方向键右Ctrl控制坦克移动和射击。蓝色坦克生命值归零则红方胜利反之亦然。游戏结束后按R键重新开始按ESC键退出。预期效果两个不同颜色的坦克出现在地图两侧。坦克可以朝八个方向移动炮管会朝向移动方向。按下射击键会从炮管口发射黄色子弹。子弹击中对方坦克会扣除其生命值生命条颜色会随之变化。坦克和子弹都无法穿过灰色的墙壁。一方生命值归零后游戏暂停并显示胜利者。5. 常见问题与调试技巧在开发或运行过程中你可能会遇到以下问题问题现象可能原因解决方案导入错误 (ModuleNotFoundError)1. 未安装pygame。2. 文件路径或模块导入语句错误。1. 运行pip install pygame。2. 检查sprites文件夹下是否有__init__.py文件。检查from .bullet import Bullet这类相对导入语句是否正确。坦克或子弹图像不显示1.self.image创建失败或为None。2. 图像被绘制在屏幕外。3. 未将精灵添加到all_sprites组或未调用draw方法。1. 确保在__init__中正确创建了pygame.Surface。2. 打印self.rect的坐标检查是否在屏幕(0,0)到(SCREEN_WIDTH, SCREEN_HEIGHT)范围内。3. 确认all_sprites.add(sprite)和all_sprites.draw(screen)被正确调用。坦克可以穿过墙壁碰撞检测逻辑_handle_collision有误或碰撞检测顺序不对。1. 确保在update中先移动再调用碰撞检测和修正。2. 检查pygame.sprite.spritecollide的调用是否正确walls_group是否传递正确。3. 调试在碰撞发生时打印日志查看dx, dy和rect的变化。子弹无法击中对方坦克1. 子弹的owner_num判断逻辑错误导致打中自己时也消失。2. 碰撞检测组tanks_group未包含所有坦克。3. 子弹和坦克的rect大小或位置计算有偏差。1. 检查bullet.py中if tank.player_num ! self.owner_num:这行代码。2. 确保tanks_group包含了tank1和tank2。3. 可以临时将坦克和子弹的图像画大一些或者绘制它们的rect边框直观查看碰撞框。游戏运行非常卡顿1. 每帧绘制的内容太多或太复杂。2. 在循环内创建了大量临时对象如Surface,Font。3. 未使用clock.tick(FPS)限制帧率。1. 确保图像、字体等在循环外初始化__init__或游戏开始前。2. 使用clock.tick(FPS)控制帧率避免过度消耗CPU。3. 对于固定不动的背景或墙壁可以考虑只绘制一次并缓存。按射击键没有反应或射速过快1. 射击冷却时间shot_cooldown设置过长或过短。2. 按键检测逻辑keys_pressed和KEYDOWN事件混淆。3.last_shot_time初始化或更新逻辑错误。1. 调整shot_cooldown值单位是毫秒。500表示0.5秒。2. 确认使用的是keys_pressed pygame.key.get_pressed()进行持续按键检测而不是KEYDOWN事件。3. 检查current_time的获取和比较逻辑。通用调试建议使用print()输出在关键位置如update、碰撞发生时打印变量值这是最直接的调试方法。绘制调试信息在屏幕上用pygame.draw.rect(screen, (255,0,0), sprite.rect, 1)绘制精灵的碰撞框可视化查看问题。简化问题如果遇到复杂bug先注释掉部分代码构建一个最小可复现场景。6. 功能扩展与工程化建议一个基础版本完成后你可以从以下几个方向扩展游戏这也能让你学习到更多的游戏开发技巧和工程化思想。6.1 功能扩展点子加入音效和背景音乐在assets/sounds/文件夹存放.wav或.ogg文件。使用pygame.mixer.Sound(assets/sounds/shoot.wav).play()在射击时播放音效。使用pygame.mixer.music.load(assets/sounds/bg_music.mp3)和pygame.mixer.music.play(-1)循环播放背景音乐。使用真实的图片资源为坦克和墙壁准备.png图片带透明通道。使用self.image pygame.image.load(assets/images/tank_blue.png).convert_alpha()加载。实现坦克旋转self.rotated_image pygame.transform.rotate(self.original_image, angle)注意要更新rect保持中心点。设计更多地图将地图数据墙壁坐标写入文本文件或JSON文件如maps/level1.json。在主程序中读取文件并动态生成墙壁实现多关卡。增加游戏道具创建PowerUp类随机在地图上生成如加血包、加速、连发。坦克碰到后触发相应效果道具消失。实现AI对手创建一个AITank类继承自Tank。在update方法中实现简单的AI逻辑如寻找玩家位置朝玩家方向移动和射击。6.2 工程化与代码优化建议资源管理创建一个ResourceManager单例类统一加载和管理所有图片、音效、字体避免重复加载浪费内存。状态机管理游戏场景将“开始菜单”、“游戏进行中”、“游戏结束”、“关卡选择”等做成不同的状态Scene。主循环根据当前状态调用不同场景的handle_events,update,draw方法。这使得代码结构更清晰易于扩展新场景。配置数据外部化将所有可调参数如坦克速度、子弹伤害、冷却时间从game_config.py进一步迁移到config.json文件中。游戏启动时读取方便非程序员如策划调整游戏平衡性。异常处理与日志在资源加载如图片文件不存在时使用try...except并提供默认的色块替代避免游戏崩溃。使用Python的logging模块记录游戏运行中的关键事件和错误便于后期排查线上问题。性能优化脏矩形渲染对于只有部分区域变化的画面可以只更新变化的部分而不是全屏重绘。Pygame 支持screen.blit时指定dirty_rect。精灵组优化对于静止的墙壁可以使用单独的组并且只在初始化或地图变化时绘制而不是每帧都绘制。代码可读性与维护为复杂的函数和类添加文档字符串Docstring。保持函数功能单一一个函数只做一件事。将魔法数字如5,10定义为有意义的常量。这个《坦克对决赛2》项目麻雀虽小五脏俱全涵盖了游戏循环、精灵管理、碰撞检测、用户输入、状态渲染等2D游戏开发的核心概念。通过动手实现它你不仅能巩固Python编程基础更能对游戏开发的基本流程建立起直观的认识。接下来你可以选择上述任何一个扩展方向进行深入研究或者用它作为框架开发属于自己的全新游戏。
返回列表