文章目录
完整项目链接:下载链接
1 前言
在计算机科学和软件开发领域,面向对象编程是一种重要的编程范式,它通过将现实世界中的事物抽象成对象,并建立对象之间的关系和交互,为软件的设计和开发提供了一种强大的工具。然而,将面向对象的原则和设计模式应用于实际项目中,并不是一件轻松的事情。本文将深入探讨一个坦克大战游戏项目的重构过程,展示如何通过面向对象的设计思想,优化和重构现有代码,从而使得项目结构更加清晰,代码更加可维护,功能更加强大。
通过这个项目,我们将深入了解如何将现有的混乱的代码组织重新设计,如何利用设计模式来解决实际问题,以及如何通过面向对象的原则来构建一个更加健壮的软件系统。从项目的分析、设计、实现到测试,我们将逐步展示每一步的思考和决策过程。通过这个过程,我们不仅可以提升自己在面向对象设计和软件开发方面的能力,也可以从中获得关于项目重构的实际经验。
这篇文章不仅是一次技术分享,更是对于在软件开发中持续学习和不断改进的态度的体现。无论您是初学者还是有经验的开发者,我相信本文都将为您提供有价值的见解和启发。让我们一起踏上这个有趣的重构之旅,探索如何将面向对象的思想应用于实际项目中,创造出更加优雅、可维护的代码和更出色的软件。
2 功能需求
回顾坦克大战的经典规则,引入新的功能需求:
(1)双人模式
除了经典的单人模式外,我们还将实现双人模式,玩家之间可以互相对战。
(2)动画与音效
通过Pygame的动画和音效功能,为游戏增添更加生动和丰富的体验。
(3)特殊场景元素
引入草丛、河面和冰面等特殊场景元素,它们将影响坦克的行动和能力。
(3)各种各样的道具
实现多种道具,如增强坦克攻击力、临时无敌状态等,为游戏增加策略性和乐趣。
3 游戏规则总览
欢迎来到坦克大战游戏,这是一个充满策略和挑战的多人游戏。在这个丰富多彩的游戏世界中,你将面临各种挑战和冒险。以下是游戏的详细规则:
- 玩家控制:玩家一使用WASD控制坦克方向,空格键开火;玩家二使用上下左右键控制方向,小键盘数字0键开火。
- 生命与等级:每位玩家初始拥有3个生命值,初始坦克等级为0。通过收集道具可以升级坦克等级。若被敌方子弹击中且坦克等级为0,则坦克爆炸减少生命;否则降低坦克等级。当生命值耗尽时,坦克无法重生,玩家死亡。若所有玩家坦克死亡,则游戏结束。
- 关卡挑战:成功击败当前关卡的敌方坦克可进入下一关。游戏内关卡无限制,每次随机选择不同关卡。电脑方坦克分为三类:快速、重型和普通。不同类型坦克有不同的速度和防御能力,需要不同数量的子弹来击毁。
- 场景元素:子弹可以摧毁红砖块,升级后的坦克子弹可以摧毁铁砖。坦克可以躲藏在草丛,滑行在冰面上,但无法穿越河面。
- 道具系统:道具可以增加我方坦克生命、使敌方坦克静止、提升我方坦克等级、破坏敌方坦克等。我方子弹击中高等级敌人的几率更大,越高等级的敌人携带道具的可能性越高。
- 子弹与发射:每个坦克有发射间隔,子弹必须撞击实体、场景元素或边界后才能再次发射。坦克等级越高,子弹飞行速度越快。
- 电脑方坦克:电脑方坦克分为三种类型,分别是快速、重型和普通。快速坦克速度快但防御力低,重型坦克防御强但速度慢,普通坦克则介于两者之间。第一个关卡有20辆电脑方坦克,每关敌方坦克数量逐渐增加。
4 游戏设计概览
这款坦克大战游戏基于PyGame框架开发,框架提供了完善的游戏开发解决方案。游戏核心是基于事件循环,允许用户自定义事件并响应。以下是游戏的设计要点:
框架和元素管理:游戏使用PyGame框架,其中pygame.Sprite.Sprite代表游戏中的元素,pygame.Sprite.Group用于管理元素集合。这为实现碰撞检测等操作提供了基础。
事件驱动:游戏的核心在于事件循环,玩家和元素都可以产生事件,并在主循环中响应。这允许有效的游戏逻辑控制和动画展示。
帧率控制:在主循环中,可以控制游戏的帧率,确保不同机器上的动画效果一致。这有助于游戏的稳定性和可预测性。
访问控制:在Python中,使用_前缀表示受保护方法,使用__前缀表示私有方法。获取和设置方法使用@property和@var.setter装饰器来实现。
这款坦克大战游戏将在PyGame框架的支持下,通过事件循环和元素管理,为玩家带来无与伦比的游戏体验。同时,访问控制的应用将有助于代码的结构化和可维护性,确保游戏的稳定性和流畅性。
5 TankGame类和游戏状态管理
游戏实例类 TankGame 被设计为单例模式,它在内部保存着关键的游戏状态信息,如当前关卡、退出游戏标志、多人模式选项等。同时,它与界面管理类 ViewManager 直接交互,根据不同状态判断展示哪个界面,以及在游戏结束时作出最外层的响应。
以下是关于 TankGame 类的一些关键要点:
单例模式:TankGame 被设计为单例模式,确保整个游戏过程只有一个实例,避免状态混乱。
关键状态信息:TankGame 内部存储了游戏的重要状态信息,如当前关卡、是否退出游戏、多人模式选项等。
与ViewManager的交互:TankGame 与界面管理类 ViewManager 直接交互,根据当前状态决定展示哪个界面,以及在游戏结束时进行响应。
初始化标志:_init_flag 表示游戏是否加载完成的状态,用于判断游戏是否应该退出。
配置文件和关卡列表:__config 是游戏的配置文件,__levels 是游戏关卡列表,这些都有助于游戏的设置和管理。
通过这样的设计,TankGame 类能够在游戏中维护关键的状态信息,管理游戏的流程和界面展示,从而为玩家提供流畅、精彩的游戏体验。
6 ViewManager类和游戏界面管理
ViewManager 是一个界面管理类,负责管理游戏中的不同界面,包括游戏加载界面 SwitchLevelView、游戏开始界面 GameStartView、游戏结束界面 GameOverView、游戏关卡界面等。这些界面类都实现了 AbstractView 抽象接口。
以下是关于 ViewManager 类的关键要点:
界面预加载:在实例化时,界面会自动进行预加载。这意味着那些不需要重复加载或初始化的资源会被提前加载到内存中,以提高后续显示界面的效率。
抽象接口:游戏中的各个界面类都实现了 AbstractView 抽象接口,这可以确保它们有一致的方法和属性,使得 ViewManager 能够统一管理它们的加载和显示。
显示管理:通过调用界面的 show() 方法,ViewManager 控制何时显示哪个界面。这有助于游戏流程的管理和用户体验的优化。
通过 ViewManager 类,游戏能够有效地管理不同的界面,根据需要进行预加载和显示,从而构建起流畅的游戏体验。
7 GameLevelView游戏关卡类
GameLevelView 类是坦克大战游戏的核心,负责管理每个关卡的状态和事件。在每次调用 show() 方法时,该类会加载关卡文件和初始化状态,然后在事件循环中执行一系列操作来控制游戏的进行。关键的操作包括:
退出游戏事件捕捉:监听退出游戏事件,当用户点击关闭按钮时,游戏可以正常退出。
敌方坦克生成:定期生成敌方坦克,保持游戏的挑战性和趣味性。
处理用户按键:监听用户的按键操作,根据用户输入来控制我方坦克的移动和开火。
碰撞检测:检测实体与场景、实体与实体之间的碰撞,确保游戏中的物体交互符合规则。
更新实体并绘制界面:在事件循环中,更新实体的状态(移动、道具生成、湮灭等),然后根据更新后的状态绘制游戏界面。
判断游戏胜利或失败:通过判断我方坦克和敌方坦克的存活情况,决定游戏是否胜利或失败,并结束当前关卡。
GameLevelView 类的设计使得每个关卡能够独立运行并管理自己的状态,从而实现了游戏的流程控制和界面渲染。
注:敌方坦克 AI 是在绘制了所有其他的实体后进行的
def _draw_interface(self):
screen = TankGame().screen
screen.fill((0, 0, 0))
screen.blit(self.__background_img, (0, 0))
self.__scene_elements.draw(screen, 1)
self.__entities.draw(screen, 1)
self.__scene_elements.draw(screen, 2)
self.__home.draw(screen)
self.__entities.draw(screen, 2)
self.__draw_game_panel()
pygame.display.flip()
def _main_loop(self):
clock = pygame.time.Clock()
while self.__has_next_loop:
# 游戏退出事件捕捉
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# 敌方坦克生成
elif event.type == self.__generate_enemies_event:
......
# 用户按键处理
self.__dispatch_player_operation()
# 碰撞检测
self.__dispatch_collisions()
# 更新并绘制实体
self.__entities.update(self.__scene_elements, self.__home)
# 绘制界面
self._draw_interface()
# 我方坦克都挂了
if len(self.__entities.player_tanks) == 0:
self.__is_win_flag = False
self.__has_next_loop = False
# 敌方坦克都挂了
if self.__total_enemy_num <= 0:
self.__is_win_flag = True
self.__has_next_loop = False
clock.tick(60)
8 关卡中的元素
在坦克大战的游戏关卡中,所有游戏中的元素被分为两个主要类别:实体(子弹、坦克、道具)和场景元素(家、砖块、铁块、河流、草丛)。道具作为一种特殊类型的实体,继承了场景元素的属性,但在实现上合并到了实体的管理类 EntityGroup 中。
SceneElementGroup 类采用了类似合成模式的设计,用于集中管理所有场景元素。它提供了统一的 add() 和 draw() 方法,用于控制将不同类型的场景元素添加到具体的 pygame.sprite.Group 中并进行绘制。创建场景元素需要通过 SceneFactory 工厂进行创建。
EntityGroup 类则采用了类似适配器模式的设计,它与 SceneElementGroup 功能相似,但在创建坦克时需要经过 TankFactory 工厂进行创建。该类提供了一致的方法,如 add()、draw()、remove() 和 update(),用于管理实体的创建、绘制、删除和更新。
这样的设计使得关卡中的元素能够被有效地管理和控制,提高了代码的组织性和可维护性,同时也符合游戏的设计逻辑。不同元素的创建和管理被封装在不同的工厂类中,提供了良好的可扩展性。
9 坦克
坦克是游戏中重要的角色,其设计基于坦克工厂类和抽象的 Tank 类。Tank 类定义了坦克的通用接口和行为,包括射击、移动、降级等操作。具体来说:
- shoot() 方法判断射击的冷却状态以及子弹数量是否满足要求,如果可以射击则返回一个子弹对象。
- move() 方法处理与场景元素(包括家)和其他坦克的碰撞,以及边界检测,判断是否可以进行移动。
- update() 方法根据游戏状态更新坦克的状态。
Tank 类还包括了子弹相关的通用属性和方法,如子弹配置、当前子弹数量、射击冷却状态等。
不同等级的坦克拥有不同的状态和图像资源,这些在 _level_images 和其他属性中进行了定义。
玩家坦克类 PlayerTank 除了继承了 Tank 类的通用属性和方法外,还具有特有的属性,如生命值和无敌状态。敌人坦克类 EnemyTank 则具有额外的属性,如携带的道具和静止状态。
这样的设计使得坦克在游戏中可以方便地进行管理和扩展。不同类型的坦克具有不同的行为和特性,通过抽象类和继承的方式实现了坦克的层次结构,提高了代码的可读性和可维护性。
10 子弹
子弹在游戏中具有重要作用,它们实现了坦克的攻击功能。每个子弹都附属于一个坦克,用 tank 字段表示其所属坦克。子弹还具有以下特性:
- enhanced 字段标识子弹是否能够打破铁砖。
- move() 方法处理子弹的飞行,并检测是否飞出游戏边界。在子弹飞出边界后,会自动调用 kill() 方法,将子弹从游戏中移除。
子弹与实体(坦克、道具)和场景元素(砖块、铁砖等)的碰撞检测位于游戏关卡界面类中的 __dispatch_collisions() 方法中。这里会检测子弹与各种对象之间的碰撞,包括子弹与砖块、子弹与家、敌方子弹与玩家坦克、玩家子弹与敌方坦克等碰撞情况。
在碰撞检测过程中,如果子弹击中了砖块,会根据子弹是否能够打破铁砖来判断是否消失。如果子弹击中敌人坦克,会根据敌人坦克的状态(是否携带道具、是否被降级)来判断击毁情况。同样,如果敌方子弹击中玩家坦克,会根据玩家坦克的状态(是否有保护状态、是否降级)来判断坦克的表现。
子弹与家的碰撞会导致游戏失败,这也在碰撞检测中进行了判断。
通过子弹的设计,游戏实现了坦克的攻击和交互效果,增加了游戏的趣味性和挑战性。
def __dispatch_collisions(self):
collision_results = {
'group': {},
'sprite': {},
'foreach_sprite': {},
}
for (collision, args) in self.__collisions['group'].items():
collision_results['group'][collision] = groupcollide(*args)
for (collision, args) in self.__collisions['sprite'].items():
collision_results['sprite'][collision] = spritecollide(*args)
for (collision, args) in self.__collisions['foreach_sprite'].items():
arg_list = list(args)
sprite_list = arg_list[0]
for sprite in sprite_list:
arg_list[0] = sprite
args = tuple(arg_list)
collision_results['foreach_sprite'][sprite] = spritecollide(*args)
for bullet in self.__entities.player_bullets:
collision_result = spritecollide(bullet, self.__scene_elements.iron_group, bullet.enhanced, None)
if collision_result:
bullet.kill()
...
# --我方子弹撞敌方坦克
for tank in self.__entities.enemy_tanks:
if collision_results['foreach_sprite'][tank]:
if tank.food:
self.__entities.add(tank.food)
tank.clear_food()
if tank.decrease_level():
self.__play_sound('bang')
self.__total_enemy_num -= 1
# --敌方子弹撞我方坦克
for tank in self.__entities.player_tanks:
if collision_results['foreach_sprite'][tank]:
if tank.protected:
self.__play_sound('blast')
else:
if tank.decrease_level():
self.__play_sound('bang')
if tank.health < 0:
self.__entities.remove(tank)
if collision_results['sprite']['PlayerBulletWithHome'] or collision_results['sprite']['EnemyBulletWithHome']:
self.__is_win_flag = False
self.__has_next_loop = False
self.__play_sound('bang')
self.__home.destroyed = True
if collision_results['group']['PlayerTankWithTree']:
self.__play_sound('hit')
11 道具
游戏中的道具被称为 Foods,它们可以为玩家坦克提供各种效果和能力。每个道具都有一个标识符(例如 BOOM、IRON)和存在时间(exist_time)。update() 方法负责倒计时并在一定时间后将道具从实体组中移除。
道具的效果处理在游戏关卡界面类中的 __dispatch_food_effects() 方法中实现。当玩家坦克与道具碰撞时,将会触发对应的效果。根据道具的类型,不同的效果会被触发,例如清空敌方坦克、冻结敌方坦克、提升玩家坦克等级、创建铁砖等。
道具的处理逻辑在碰撞检测中进行,当玩家坦克与道具发生碰撞时,调用 __dispatch_food_effect() 方法处理道具的效果。根据道具的类型,会触发不同的效果,如清空敌方坦克、冻结敌方坦克、提升玩家坦克等级、创建铁砖等。效果处理完成后,道具实例会从实体组中移除,从而不再显示在游戏中。
通过道具的设计,游戏增加了一些特殊能力和策略性,玩家可以通过获取道具来获得优势,增加了游戏的多样性和挑战性。
def __dispatch_food_effect(self, food: Foods, player_tank: PlayerTank):
self.__play_sound('add')
if food.type == Foods.BOOM:
for _ in self.__entities.enemy_tanks:
self.__play_sound('bang')
self.__total_enemy_num -= len(self.__entities.enemy_tanks)
self.__entities.clear_enemy_tanks()
elif food.type == Foods.CLOCK:
for enemy_tank in self.__entities.enemy_tanks:
enemy_tank.set_still()
elif food.type == Foods.GUN:
player_tank.improve_level()
elif food.type == Foods.IRON:
for x, y in self.__home.walls_position:
self.__scene_elements.add(
self.__scene_factory.create_element((x, y), SceneFactory.IRON)
)
elif food.type == Foods.PROTECT:
player_tank.protected = True
elif food.type == Foods.STAR:
player_tank.improve_level()
player_tank.improve_level()
elif food.type == Foods.TANK:
player_tank.add_health()
self.__entities.foods.remove(food)
def __dispatch_collisions(self):
...
for player_tank in self.__entities.player_tanks:
for food in self.__entities.foods:
collision_result = collide_rect(player_tank, food)
if collision_result:
self.__dispatch_food_effect(food, player_tank)
...
12 UMLs
Statechart diagram
Use case diagram
Class diagram
13 改进总结
- TankGame 单例优化:将游戏资源路径参数存储在配置文件中,并通过 TankGame
的实例来访问这些配置,避免在多个类之间传递参数,提高代码的整洁性和可维护性。同时,将游戏窗口的唯一引用存储在 TankGame
中,避免在各个画面类中都存储一个窗口引用,减少冗余。 - 画面抽象和管理:引入抽象的 AbstractView 定义画面的通用行为模式,通过 ViewManager
单例来管理所有画面的预加载和显示,使画面之间的切换和加载变得更加清晰和高效。 - 坦克抽象和工厂:通过创建 Tank 类对玩家坦克和敌方坦克进行抽象,避免了重复代码和冗余参数传递。使用 TankFactory
来统一创建坦克,减少了坦克的初始化参数,并将资源路径的配置从各个坦克对象中分离出来。 - 场景元素和实体的区分:引入场景元素和实体的概念,使用 SceneElementFactory 来创建场景元素,使用
SceneElementsGroup 来管理场景元素的聚集,提高了代码的结构化和可扩展性。 - 分配处理逻辑:将原本集中在主循环的处理逻辑分配到不同的游戏关卡类函数中,使每个函数只关注特定的职责,增强了代码的内聚性和可读性,降低了代码的复杂度。
- 这些改进方案共同促进了代码的重构和优化,使得代码更加清晰、易于维护和扩展。每个改进点都在特定的方面解决了原有设计中存在的问题,从而提升了游戏的质量和开发效率。
14 界面截图
Choose Multi-Player
Loading a Level
Playing in a Level
Game Over
15 结语
总结来说,这篇文章详细地介绍了一个坦克大战游戏项目的重构过程,通过运用面向对象设计原则和设计模式,使得原本组织较为混乱的项目逐步演变为更具结构性和可维护性的游戏工程。在这个过程中,我们不仅对项目进行了功能上的优化和改进,还在代码组织、设计思路和代码质量上进行了提升。
从项目的结构、类的设计、关卡逻辑到资源管理,每一步都经过仔细的思考和规划。通过单例模式、工厂模式、组合模式等设计模式的运用,我们成功地将游戏的不同模块解耦,使得代码更加清晰,功能分工更加明确。在对项目中存在的问题进行剖析的同时,我们展示了如何改进现有代码,使其更符合面向对象的原则,提高代码的可读性和可扩展性。
这个重构过程展现了在软件开发领域的深刻思考和不断学习的精神。通过这个项目,我们不仅获得了面向对象设计的实际应用经验,还加深了对设计模式的理解和运用。这篇文章不仅是一次技术分享,更是对于坚持不断学习和改进的精神的体现,相信这些经验和思考将在未来的开发之路上发挥重要作用。