背景介绍:
临近期末python大作业要求用python写一个程序,因为小游戏比较简单并且对它比较感兴趣,所以开始在GitHub上找寻小游戏,在看了众多游戏最终选择了坦克大战。坦克大战游戏资源全部都有,并且拥有基本的功能,当比如菜单,模式、很多小细节没完善,所以毅然选择了坦克大战。
资源我放在了GitHub上了,有想法的同学可以去下载下来完善或者玩一下也行,https://github.com/fightingzxd/pygame-TankWar
好了接下来就介绍这个项目吧。
菜单选择:玩家可以根据菜单进行选择,提高了程序的交互性
多种模式:提供了关卡模式、无尽模式、单挑模式、建造模式多种模式选择
地图编辑:用户可以根据自己想法自由编辑地图,增加玩家趣味性。
简单敌人AI:敌人有简单的AI行为,如:发射子弹、漫步等。
动画效果:坦克在受到攻击时的爆破效果;敌方坦克复活的简单动画。
游戏循环:同一般软件不同,游戏需要采用主循环来更新场景状态并重绘屏幕。本程序以60FPS的帧率刷新屏幕。
墙体:不同墙体元素有不同的特性。
道具:不同的道具会触发不同的效果
多种关卡:关卡模式和无尽模式都有35种不同的关卡
双人对战:可以进行双人当成队友一起游戏,也可以当成敌人进行单挑
下面就是菜单的演示,目前关卡模式、无尽模式是根据原版制作了35关,单挑模式制作了1关。
关卡模式:可以看到右边显示敌军剩余数量和我方血量,当然这个游戏支持双人玩
无尽模式:敌军数量和我方生命无限
单挑模式:可以进行两人玩耍进行对抗
建造模式:争对上面三种模式可以进行自定义建造并开始游戏。
pygame是跨平台Python模块,专为电子游戏设计。包含图像、声音。创建在SDL基础上,允许实时电子游戏研发而无需被低端语言,如C语言或是更低端的汇编语言束缚。基于这样一个设想,所有需要的游戏功能和理念都(主要是图像方面)完全简化位游戏逻辑本身,所有的资源结构都可以由高级语言提供,如Python。我们这个游戏主要就是使用pygame的模块实现。
https://www.pygame.org/news
Pygame-menu 是一个用于创建菜单和 GUI 的 python-pygame 库。它支持多个小部件,例如按钮、颜色输入、时钟对象、放置选择器、框架、图像、标签、选择器、表格、文本输入、颜色切换等等,并具有多个自定义选项。
https://pygame-menu.readthedocs.io/en/4.2.0/
Pygame-menu提供的功能比较完善了实现也很简单,比如创建主菜单:
main_menu = pygame_menu.Menu('Main Menu', 750, 630, theme=main_theme)
image_path = r"image\logo.png"
# 下面添加自己想要的组件
main_menu.add.image(image_path,scale = (1.2,1.2))
# 按钮也可以添加想要的功能
main_menu.add.button('Level mode', level_mode_menu)
main_menu.add.button('Endless mode', endless_mode_menu)
main_menu.add.button('Heads up mode',heads_up_menu)
main_menu.add.button('Custom mode', custom_mode_menu)
main_menu.add.button('Set up', set_up)
main_menu.add.button('Quit', pygame_menu.events.EXIT)
参考:https://www.cnblogs.com/liquancai/p/13256388.html
pygame.sprite.Sprite 是 pygame 中用来实现精灵的一个类,在使用时并不需要对它实例化,只需要继承它,然后按需写出自己的类,因此非常简单、使用。
当然我们使用它主要是类的成员
1)self.image其负责显示什么图形 就是将此类用一个图片进行显示
2)self.rect其负责在哪里显示 一般来说,先用 self.rect = self.image.get_rect() 来获取 image 的矩形区域,然后给 self.rect 设定显示的位置,比如self.rect.top 、self.rect.bottom 、self.rect.left 、self.rect.right 分别表示上、下、左、右。
当程序中有大量实体的时候,操纵这些实体将会是一件相当麻烦的事,而精灵组可以作为一个实体精灵容器将这些精灵放在一起统一管理。pygame 使用精灵组来管理精灵的绘制和更新,精灵组是一个简单的容器。
函数 | 功能 |
---|---|
pygame.sprite.Group.add | 将精灵添加到该组 |
pygame.sprite.Group.remove | 从组中删除精灵 |
pygame.sprite.spritecollide | 在与另一个精灵相交的组中查找精灵。 |
pygame.sprite.collide_rect | 两个精灵之间的碰撞检测,使用矩形。 |
主要是使用上边的函数,使用精灵组以及添加、删除函数实现批量的精灵的管理,然后实现碰撞检测函数实现碰撞检测等操作。
因为主要使用的是面向对象的方法
墙体类主要是继承了pygame的精灵类,里面就是包含了类成员image图片和rect它的位置 墙体类其实是统称里面包含砖块类、石头类、树类、河流类、冰川类和基地类
地图类主要是对墙体类的每个类进行统一的放置,在游戏类中更好的进行地图的绘制
里面包括了draw的方法:就是对列表进行翻译将墙体类装入容器中
checkpoint方法就是进行选择对应的列表进行使用draw的方法进行装入(相当于就是选择关卡的函数)
地图建造类为了实时显示而加入了显示板块,通过将列表传入到Map建造地图显示,然后通过玩家操作改变列表,再传入到Map显示以此循环实现了实时显示。now_x,now_y主要是当前的位置。map_num主要就是列表,保存当前地图信息,如果觉得建造结束,将此列表传入游戏类进行地图显示。
子弹类继承了pygame的精灵类,能够实现子弹显示和碰撞检测,然后通过dir_x,dir_y指定移动的方向,speed指定移动的速度,life指定是否显示。
changeImage的方法主要是根据给的方向改变图片来实现子弹方向和移动方向的统一。
move的方法实现根据速度移动和碰撞检测
特效的实现就是将每一帧图片循环显示以达到动画效果。
特效类主要是将三个特效的每一帧图片保存在列表中,方便下面的方法更好调用图片。
道具类继承了pygame的精灵类,kind表示道具的种类,rect表示道具的位置,imgae表示道具的图片显示。类里主要是使用随机函数,实现了道具种类和道具位置的随机性。调用change的方法就是将道具种类和位置随机然后并显示到游戏中。
我方坦克类继承了pygame的精灵类,因为玩家一和玩家二坦克的区别,playerNumber表示玩家几,然后加载不同的图片。dir_x,dir_y表示指向的方向,通过组合子弹类,调用shoot的方法实现子弹的发射。move的方法就是根据方向,改变相应的图片实现方向的转向,并且碰撞检测判断是否能够进行移动。
敌方坦克类继承了pygame的精灵类,通过kind(表示等级)、isred(是否携带道具)、rect(位置)、speed(速度)等变量来表示坦克的属性,shoot是子弹发射的方法,move就是坦克移动的方法,内部同样有碰撞检测。
该类基本组合或者聚合了基本所有的类 就相当于一个棋盘(其它类相当于棋子,这样游戏就可以开始了)
游戏类有game_running和game_running_singled_out是两个相似的游戏方法,
只需要首先申明类然后调用其中一个方法就可以开始游戏,两个方法的区别就是游戏模式的不同。
其它的方法就是服务于这两个方法的 比如暂停方法,结束方法,检测键盘方法等
游戏基本逻辑就是在循环中,检测事件、检测键盘事件、检测自己定义的函数,刷新界面。
从总类图中可以看出,墙体类、道具类、子弹类、敌方坦克类、我方坦克类这些都是游戏中的基本元素,它们都继承了pygame的精灵类实现了图片的移动化以及碰撞检测等功能。我方坦克类、敌方坦克类都组合了子弹类(具体看代码)
地图类组合了墙体类实现了地图的创建、地图建造类聚合地图类实现建造地图时的实时显示。最后游戏类组合或者聚合这些元素形成了游戏的运行。
游戏类提供了两个游戏运行的方法,供界面调用最终组成游戏。
1)为什么需要动态加载界面?
因为在未加载界面时,当程序在加载关卡时需要耗费接近4-5秒时间(将图片加载到按钮的耗时),这时程序会出现一直黑屏的情况,极大影响玩家对游戏的感觉。
2)如何实现?
在网络下载动态加载图,然后弄成成一帧一帧的图片(共计105张)加载到循环中,每次循环更新3次图,这样形成了动态加载界面并且实时反应加载进度。
# 显示加载界面
surface.blit(init_image_all[now_i], (10, 25)) # 显示图片
now_i += 1 # 将图片的索引换位下一张
pygame.display.flip() # 刷新界面
我们的坦克是一张图片,那为啥在移动时会出现动态效果,难道是两种图片切换?
当然就是这样:
使用pygame.Surface.subsurface()返回一个与其新父级共享像素的新 Surface。(新 Surface 被视为原始 Surface 的子代。对任一表面像素的修改将相互影响。剪切区域和颜色键等表面信息对于每个表面都是唯一的。)这样就可以对同一张图片的不停切换,然后加入到移动函数中,实现移动就动态显示。
# 移动方法
def move(self, tankGroup, brickGroup, ironGroup,riverGroup):
# 进行移动
self.rect = self.rect.move(self.speed * self.dir_x, self.speed * self.dir_y)
# 选择相应图片(为啥是两张图片呢,subsurface是共享的 这样就是在移动中切换两种图片就会有移动效果)
if self.dir_x == 0 and self.dir_y == -1:
self.tank_R0 = self.tank.subsurface((0, 0), (48, 48))
self.tank_R1 = self.tank.subsurface((48, 0), (48, 48))
elif self.dir_x == 0 and self.dir_y == 1:
self.tank_R0 = self.tank.subsurface((0, 48), (48, 48))
self.tank_R1 = self.tank.subsurface((48, 48), (48, 48))
elif self.dir_x == -1 and self.dir_y == 0:
self.tank_R0 = self.tank.subsurface((0, 96), (48, 48))
self.tank_R1 = self.tank.subsurface((48, 96), (48, 48))
elif self.dir_x == 1 and self.dir_y == 0:
self.tank_R0 = self.tank.subsurface((0, 144), (48, 48))
self.tank_R1 = self.tank.subsurface((48, 144), (48, 48))
可能很好奇敌方坦克是怎么移动的,其实就是当碰到不能移动的东西时(比如墙壁,边界时),使用随机函数找随机一个方向然后移动,其实不用感觉就是挺笨的,后续其实可以加入其它算法让敌方更加智能(比如根据基地,我方坦克位置,我方打出子弹位置来进行最优解的移动)
# 碰撞地图边缘(随机选择方向)
if self.rect.top < 3:
self.rect = self.rect.move(self.speed * 0, self.speed * 1)
self.dir_x, self.dir_y = random.choice(([0, 1], [0, -1], [1, 0], [-1, 0]))
elif self.rect.bottom > 630 - 3:
self.rect = self.rect.move(self.speed * 0, self.speed * -1)
self.dir_x, self.dir_y = random.choice(([0, 1], [0, -1], [1, 0], [-1, 0]))
elif self.rect.left < 3:
self.rect = self.rect.move(self.speed * 1, self.speed * 0)
self.dir_x, self.dir_y = random.choice(([0, 1], [0, -1], [1, 0], [-1, 0]))
elif self.rect.right > 630 - 3:
self.rect = self.rect.move(self.speed * -1, self.speed * 0)
self.dir_x, self.dir_y = random.choice(([0, 1], [0, -1], [1, 0], [-1, 0]))
# 碰撞墙体 和坦克 和河流 (随机选择方向)
if pygame.sprite.spritecollide(self, brickGroup, False, None) \
or pygame.sprite.spritecollide(self, ironGroup, False, None) \
or pygame.sprite.spritecollide(self, tankGroup, False, None) \
or pygame.sprite.spritecollide(self, riverGroup, False, None):
self.rect = self.rect.move(self.speed * -self.dir_x, self.speed * -self.dir_y)
self.dir_x, self.dir_y = random.choice(([0, 1], [0, -1], [1, 0], [-1, 0]))
我们都知道游戏是有FPS(每秒传输帧数(Frames Per Second)),简单说就是一秒刷新多少次界面,来实现界面的动画效果。FPS越高效果越好,但是我们游戏是如何控制的呢?
我们的游戏是使用while循环来实现的,由于我们的游戏比较简单,电脑性能也不错,那么如何控制游戏的运行呢?不可能电脑好点运行就快,电脑不好运行就不快吧,这样严重影响了玩家的体验。
所以pygame提供了时钟
clock = pygame.time.Clock()
当使用下面的函数就可以设置电脑运行的帧数不可能超过60(每个while循环都是固定的时间),因为现在普遍性能肯定超过60,所以确保游戏的帧率为60,保证了游戏的流畅性。
clock.tick(60)
Pygame提供了事件的机制,就是鼠标点击事件、键盘点击事件、自定义事件、退出游戏事件等都会保存在事件队列中。然后依次取出进行响应。
for event in pygame.event.get():
# 如果鼠标单击
if event.type == pygame.MOUSEBUTTONDOWN:
self.game_pause()
# 如果退出
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# 我方子弹冷却事件
if event.type == self.MYBULLETNOTCOOLINGEVENT:
self.myTank_T1.bulletNotCooling = True
# 。。。。。。。。。。
Pygame也提供了自定义事件
# 自定义事件
# 创建敌方坦克延迟200
self.DELAYEVENT = pygame.constants.USEREVENT
pygame.time.set_timer(self.DELAYEVENT, 200)
# 创建 敌方 子弹延迟1000
self.ENEMYBULLETNOTCOOLINGEVENT = pygame.constants.USEREVENT + 1
pygame.time.set_timer(self.ENEMYBULLETNOTCOOLINGEVENT, 1000)
# 创建 我方 子弹延迟200
self.MYBULLETNOTCOOLINGEVENT = pygame.constants.USEREVENT + 2
pygame.time.set_timer(self.MYBULLETNOTCOOLINGEVENT, 200)
# 敌方坦克 静止8000
self.NOTMOVEEVENT = pygame.constants.USEREVENT + 3
pygame.time.set_timer(self.NOTMOVEEVENT, 8000)
使用set_timer创建自定义事件,然后在设置的时间进入队列进行处理。
这样可以设置坦克创建延迟的事件,每过固定时间触发一次,这样就可以检测是否敌方坦克数量减少,然后进行补充。
写完之后想给别人玩,但是除了计算机专业很少人会安装python的运行环境,于是自己想着如何打包给别人玩,最后也是实现了打包
打包问题可以参考我上一个博客
https://blog.csdn.net/qq_46470984/article/details/121988373?spm=1001.2014.3001.5501
此次项目主要是对pygame的使用,了解了pygame小游戏的总体运行情况,熟悉了python的一些语法还有一些库,提升了编程能力。
当然最重要是获得了很多不错的网站
坦克大战介绍:https://www.retrowan.com/battle-city/
画图的网站:https://mastergo.com/(那些提示图片的制作)
消除图片背景:https://www.remove.bg/zh/upload
视频转gif:https://www.apowersoft.cn/video-to-gif-online
当然还有很多。
因为时间有限,很多功能并未完善
游戏的不足:
1)音效关闭功能不完善
2)设置功能没有开通
3)游戏中还有一些小bug由于时间原因没有完善
4)未加分数机制
5)游戏结束界面比较潦草