文章目录
前言
《坦克大战》,1985年由日本开发商南梦宫(Namco)开发,是第一款可以双打的红白机游戏。当时使用的还是小霸王。如何用python实现这个游戏
资源获取
图片音频等素材资源包括源代码(ps:源代码可能后面会改可能跟下方不一样)我放在了Gitee上了,有想法的xd可以去下载下来完善或者玩一下也行,https://gitee.com/fbozhang/python/tree/master/pygame/
有问题可以私聊我
一、项目介绍
1.pygame是什么?
Pygame是跨平台Pyth,Pygame 作者是 Pete Shinners, 协议为 GNU Lesser General Public License。
pygame是跨平台Python模块,专为电子游戏设计。包含图像、声音。建立在SDL基础上,允许实时电子 游戏研发而无需被低级语言(如机器语言和汇编语言)束缚。基于这样一个设想,所有需要的游戏功能和理念都(主要是图像方面)都完全简化为游戏逻辑本身,所有的资源结构都可以由高级语言提供,如Python。我们这个游戏主要就是使用pygame的模块实现。
2.操作指南
玩家:上下左右 移动 空格 射击
F1: 复活玩家
F2: 复活我方老鹰
F3: 复活敌方坦克
F4: 外挂模式——子弹自动追踪敌方
3.项目演示
演示视频:
演示gif(利用下面网站转换):
二、项目实现
1.安装库
pip install pygame
2.引入库
代码如下(示例):
import math
import random
import sys
import time
import pygame
3.项目代码
PS: 以下代码以尽量都注释了,如还有哪里不明白可以call me添加注解
本质上这是一个贴图游戏
3.1 主逻辑类
开始游戏,结束游戏,获取事件(比如鼠标事件、键盘事件)并处理
class MainGame():
# 游戏主窗口
window = None
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 500
# 背景
background = "images/background.gif"
# 创建我方水晶
Home = None
# 创建我方坦克
TANK_P1 = None
# 存储所有的敌方坦克
EnemyTank_List = []
# 要创建的敌方坦克的数量
EnemyTank_count = 3
# 存储我方子弹的列表
Bullet_List = []
# 存储敌方子弹的列表
Enemy_bullet_List = []
# 爆炸效果列表
Explode_List = []
# 墙壁列表
Wall_List = []
# 外挂模式
cheat = False
# 开始游戏
def startGame(self):
# 初始化显示模块
pygame.display.init()
# 创建窗口加载窗口
MainGame.window = pygame.display.set_mode([MainGame.SCREEN_WIDTH, MainGame.SCREEN_HEIGHT])
# 创建我方坦克
self.creatMyTank()
# 创建敌方坦克
self.creatEnemyTank()
# 创建水晶
self.creatHome()
# 创建墙壁
self.creatWalls()
# 设置游戏标题
pygame.display.set_caption("坦克大战" + version)
# 创建音乐对象
music = Music("audios/start.wav")
# 调用播放音乐方法
music.play()
# 让窗口持续刷新操作
'''
在纵坐标100以上的位置随机创建敌方坦克
'''
# 创建敌方坦克
def creatEnemyTank(self):
top = 100
for i in range(MainGame.EnemyTank_count):
speed = random.randint(3, 6)
left = random.randint(1, int(MainGame.SCREEN_WIDTH / 100 - 1))
eTank = EnemyTank(left * 100, top, speed)
MainGame.EnemyTank_List.append(eTank)
'''
因只是案例所以随便设置了一些墙,如果想完善ui可以在这设计墙壁
'''
# 创建墙壁
def creatWalls(self):
# 创建我方水晶
def creatHome(self):
MainGame.Home = Home()
# 将墙壁加入到窗口中
def blitWalls(self):
for wall in MainGame.Wall_List:
if wall.live:
wall.displayWall()
else:
MainGame.Wall_List.remove(wall)
# 展示背景
def blitBackground(self):
Background(MainGame.background).displayBackground()
# 将水晶标志加入到窗口中
def blitHome(self):
MainGame.Home.displayHome()
# 将敌方坦克加入到窗口中
def blitEnemyTank(self):
for eTank in MainGame.EnemyTank_List:
if eTank.live:
eTank.displayTank() # 继承父类
# 坦克移动
eTank.randMove()
# 调用坦克与墙壁的碰撞方法
eTank.hitWalls()
# 敌方坦克是否撞到我方坦克
eTank.hitMyTank()
# 调用敌方坦克的射击
eBullet = eTank.shot()
# 如果子弹为None,不加入到列表
if eBullet:
# 将子弹存储敌方子弹列表
MainGame.Enemy_bullet_List.append(eBullet)
else:
MainGame.EnemyTank_List.remove(eTank)
# 将我方子弹加入到窗口中
def blitBullet(self):
for bullet in MainGame.Bullet_List:
# 如果子弹还活着绘制出来,否则直接从列表中移除该子弹
if bullet.live:
bullet.displayBullet()
print(bullet.cheat)
bullet.cheat = MainGame.cheat
if bullet.cheat:
# 子弹追踪
if len(MainGame.EnemyTank_List) > 0:
bullet.bulletFollowMove(MainGame.EnemyTank_List[0].rect.left,
MainGame.EnemyTank_List[0].rect.top)
else:
# 让子弹移动
bullet.bulletMove()
# 判断我方子弹与敌方坦克是否碰撞
bullet.hitEnemyTank()
# 判断子弹是否碰撞到墙壁
bullet.hitWalls()
# 判断子弹是否碰撞
bullet.hitBullet()
# 判断子弹是否打到水晶
bullet.hitHome()
else:
MainGame.Bullet_List.remove(bullet)
# 将敌方子弹加入到窗口中
def blitEnemyBullet(self):
for eBullet in MainGame.Enemy_bullet_List:
# 如果子弹还活着绘制出来,否则直接从列表中移除该子弹
if eBullet.live:
eBullet.displayBullet()
# 让子弹移动
eBullet.bulletMove()
# 判断子弹是否碰撞到墙壁
eBullet.hitWalls()
# 判断子弹是否打到水晶
eBullet.hitHome()
if MainGame.TANK_P1 and MainGame.TANK_P1.live:
eBullet.hitMyTank()
else:
MainGame.Enemy_bullet_List.remove(eBullet)
# 展示爆炸效果列表
def displayExplodes(self):
for explode in MainGame.Explode_List:
if explode.live:
explode.displayExplode()
else:
MainGame.Explode_List.remove(explode)
# 获取程序运行期间所有事件(鼠标事件,键盘事件)
def getEvent(self):
# 1.获取所有事件
eventList = pygame.event.get()
# 2.对事件进行判断处理(1. 点击关闭按钮 2.按下键盘上的某个键)
for event in eventList:
# 判断event.type 是否QUIT,如果是退出的话,直接调用程序结束方法
if event.type == pygame.QUIT:
self.endGame()
# 判断事件类型是否为按键按下,如果是:继续判断按键是哪一个按键来进行对应的处理
if event.type == pygame.KEYDOWN:
# 点击F1按键让我方坦克重生
if event.key == pygame.K_F1 and not MainGame.TANK_P1:
# 调用创建我方坦克方法
self.creatMyTank()
# 点击F2按键让我方水晶重生
if event.key == pygame.K_F2 and not MainGame.Home.live:
MainGame.Home.live = True
# 点击F3按键让敌方坦克重生
if event.key == pygame.K_F3 and len(MainGame.EnemyTank_List) == 0:
self.creatEnemyTank()
if event.key == pygame.K_F4:
MainGame.cheat = True
if MainGame.TANK_P1 and MainGame.TANK_P1.live:
# 具体是哪一个按键的处理
if event.key == pygame.K_LEFT:
print("坦克向左调头, 移动")
# 修改坦克方向
MainGame.TANK_P1.direction = "L"
MainGame.TANK_P1.stop = False
# 完成移动操作
# MainGame.TANK_P1.move()
elif event.key == pygame.K_RIGHT:
print("坦克向右调头, 移动")
MainGame.TANK_P1.direction = "R"
MainGame.TANK_P1.stop = False
elif event.key == pygame.K_UP:
print("坦克向上调头, 移动")
MainGame.TANK_P1.direction = "U"
MainGame.TANK_P1.stop = False
elif event.key == pygame.K_DOWN:
print("坦克向下调头, 移动")
MainGame.TANK_P1.direction = "D"
MainGame.TANK_P1.stop = False
elif event.key == pygame.K_SPACE:
print("发射子弹")
if len(MainGame.Bullet_List) < 3:
# 产生一颗子弹
m = MainGame.TANK_P1.shot()
# 将子弹加入到子弹列表
MainGame.Bullet_List.append(m)
# 添加射击音效
music = Music("audios/fire.wav")
music.play()
else:
print("子弹数量不足")
print("当前屏幕中的子弹数量为:%d" % len(MainGame.Bullet_List))
# 判断方向键是否弹起
if event.type == pygame.KEYUP:
# 松开的如果是方向键,才更改移动开关状态
if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT or event.key == pygame.K_UP or event.key == pygame.K_DOWN:
if MainGame.TANK_P1 and MainGame.TANK_P1.live:
# 修改坦克的移动状态
MainGame.TANK_P1.stop = True
"""
# 获取键盘的第二种方法,考虑后面用该方法控制2P
key = pygame.key.get_pressed()
if MainGame.TANK_P1 and MainGame.TANK_P1.live:
if key[pygame.K_a]:
print("坦克向左调头, 移动")
MainGame.TANK_P1.direction = "L"
MainGame.TANK_P1.move()
elif key[pygame.K_w]:
print("坦克向上调头, 移动")
MainGame.TANK_P1.direction = "U"
MainGame.TANK_P1.move()
elif key[pygame.K_s]:
print("坦克向下调头, 移动")
MainGame.TANK_P1.direction = "D"
MainGame.TANK_P1.move()
elif key[pygame.K_d]:
print("坦克向右调头, 移动")
MainGame.TANK_P1.direction = "R"
MainGame.TANK_P1.move()
# 这个方法暂未解决边移动边设计
elif key[pygame.K_KP0]:
print("射击")
# 产生一颗子弹
m = Bullet(MainGame.TANK_P1)
# 将子弹加入到子弹列表
MainGame.Bullet_List.append(m)
"""
# 左上角文字绘制
def getTextSurface(self, text):
# 字体初始化
pygame.font.init()
""""如果需要改其他系统字体可以先查询,下面选择的楷体"""
# 查看系统支持的所有字体
# fontList = pygame.font.get_fonts()
# print(fontList)
# 选择一个合适的字体
font = pygame.font.SysFont("kaiti", 18) # pygame.font.SysFont("字体名称", 字号, 默认不粗体, 默认不斜体)
# 使用对应的字符完成相关内容的绘制
textSurface = font.render(text, True, COLOR_RED) # font.render(文字, 是否抗锯齿, 颜色, 背景默认没有)
return textSurface
# 游戏结束提示
def getTextSurface_Tips(self, text):
# 字体初始化
pygame.font.init()
font = pygame.font.SysFont("kaiti", 60, bold=True, italic=True) # pygame.font.SysFont("字体名称", 字号, 默认不粗体, 默认不斜体)
# 使用对应的字符完成相关内容的绘制
textSurface = font.render(text, True, COLOR_RED) # font.render(文字, 是否抗锯齿, 颜色, 背景默认没有)
return textSurface
# 结束游戏
def endGame(self):
print("谢谢使用")
# 结束python解释器
# exit()
# 结束程序
sys.exit()
3.2 背景类
加载游戏背景(也可以写成主逻辑类的方法)
class Background():
def __init__(self, image):
self.image = pygame.image.load(image)
self.rect = self.image.get_rect()
self.rect.left = 0
self.rect.top = 0
def displayBackground(self):
MainGame.window.blit(self.image, self.rect)
3.3 基类
继承精灵类,用于作为需要做碰撞测试的父类,详细参考API文档
class BaseItem(pygame.sprite.Sprite):
def __int__(self):
pygame.sprite.Sprite.__init__(self)
3.4 坦克类
坦克的移动,射击以及坦克的展示。因为需要做碰撞测试所以继承基类
class Tank(BaseItem):
def __init__(self, left, top):
self.images = {
"U": pygame.image.load("images/p1tankU.gif"),
"D": pygame.image.load("images/p1tankD.gif"),
"L": pygame.image.load("images/p1tankL.gif"),
"R": pygame.image.load("images/p1tankR.gif"),
}
# 初始方向
self.direction = "U"
self.image = self.images[self.direction]
# 坦克所在的区域
self.rect = self.image.get_rect()
# 指定坦克初始化位置 分别距x,y轴的位置
self.rect.left = left
self.rect.top = top
# 速度属性
self.speed = 5
# 坦克的移动开关
self.stop = True
# live用来记录,坦克是否活着
self.live = True
# 用来记录坦克移动之前的坐标(用于坐标还原时使用)
self.oldLeft = self.rect.left
self.oldTop = self.rect.top
"""
因为是贴图游戏所以移动即是坐标的加减变化,利用视觉暂留原理实现移动
"""
# 坦克的移动
def move(self):
self.oldLeft = self.rect.left
self.oldTop = self.rect.top
if self.direction == "L":
if self.rect.left > 0:
self.rect.left -= self.speed
elif self.direction == "R":
if self.rect.left + self.rect.width < MainGame.SCREEN_WIDTH:
self.rect.left += self.speed
elif self.direction == "U":
if self.rect.top > 0:
self.rect.top -= self.speed
elif self.direction == "D":
if self.rect.top + self.rect.height < MainGame.SCREEN_HEIGHT:
self.rect.top += self.speed
# 恢复老坐标即不可继续向前移动
def stay(self):
self.rect.left = self.oldLeft
self.rect.top = self.oldTop
# 撞到墙
def hitWalls(self):
for wall in MainGame.Wall_List:
if pygame.sprite.collide_rect(wall, self): # 监测两个精灵的矩形是否碰撞返回布尔值
self.stay()
# 射击
def shot(self):
return Bullet(self)
# 展示坦克(将坦克这个surface绘制到窗口中 blit())
def displayTank(self):
# 1.重新设置坦克的图片
self.image = self.images[self.direction]
# 2.将坦克加入到窗口中
MainGame.window.blit(self.image, self.rect)
3.5 MyTank类
我方坦克,撞到敌方坦克不能移动
class MyTank(Tank):
def __init__(self, left, top):
super(MyTank, self).__init__(left, top)
# 主动碰撞到敌方坦克
def hitEnemyTank(self):
for eTank in MainGame.EnemyTank_List:
if pygame.sprite.collide_rect(eTank, self):
self.stay()
3.6 EnemyTank类
重写了移动和射击方法,让敌方坦克随机移动射击
class EnemyTank(Tank):
def __init__(self, left, top, speed):
super(EnemyTank, self).__init__(left, top)
# self.live = True
self.images = {
"U": pygame.image.load("images/enemy1U.gif"),
"D": pygame.image.load("images/enemy1D.gif"),
"L": pygame.image.load("images/enemy1L.gif"),
"R": pygame.image.load("images/enemy1R.gif"),
}
self.direction = self.randDirection()
self.image = self.images[self.direction]
# 坦克所在的区域
self.rect = self.image.get_rect()
# 指定坦克初始化位置 分别距x,y轴的位置
self.rect.left = left
self.rect.top = top
# 新增速度属性
self.speed = speed
# 坦克的移动开关
self.stop = True
# 新增步数属性,用来控制敌方坦克随机移动
self.step = 20
def randDirection(self):
num = random.randint(1, 4)
if num == 1:
# self.direction = 'U' # 写成方法则不需要直接改变属性值
return 'U'
elif num == 2:
return 'D'
elif num == 3:
return 'L'
elif num == 4:
return 'R'
# 随机移动
def randMove(self):
if self.step <= 0:
self.direction = self.randDirection()
self.step = 20
else:
self.move()
self.step -= 1
def shot(self):
num = random.randint(1, 100)
if num <= 2:
return Bullet(self)
def hitMyTank(self):
if MainGame.TANK_P1 and MainGame.TANK_P1.live:
if pygame.sprite.collide_rect(self, MainGame.TANK_P1):
# 让敌方坦克停下来
self.stay()
3.7 子弹类
子弹的移动,碰撞以及展示子弹
class Bullet(BaseItem):
def __init__(self, tank):
# 图片
self.image = pygame.image.load("images/enemymissile.gif")
# 方向(坦克方向)
self.direction = tank.direction
# 位置
self.rect = self.image.get_rect()
if self.direction == 'U':
self.rect.left = tank.rect.left + tank.rect.width / 2 - self.rect.width / 2
self.rect.top = tank.rect.top - self.rect.height
elif self.direction == 'D':
self.rect.left = tank.rect.left + tank.rect.width / 2 - self.rect.width / 2
self.rect.top = tank.rect.top + tank.rect.height
elif self.direction == 'L':
self.rect.left = tank.rect.left - self.rect.width
self.rect.top = tank.rect.top + tank.rect.width / 2 - self.rect.height / 2
elif self.direction == 'R':
self.rect.left = tank.rect.left + tank.rect.width
self.rect.top = tank.rect.top + tank.rect.width / 2 - self.rect.height / 2
# 速度
self.speed = 7
# 用来记录子弹是否活着
self.live = True
# 记录是否开挂
self.cheat = False
# 子弹的移动
def bulletMove(self):
if self.direction == 'U':
if self.rect.top > 0:
self.rect.top -= self.speed
else:
# 如果碰壁,修改状态值
self.live = False
elif self.direction == 'D':
if self.rect.top < MainGame.SCREEN_HEIGHT - self.rect.height:
self.rect.top += self.speed
else:
self.live = False
elif self.direction == 'L':
if self.rect.left > 0:
self.rect.left -= self.speed
else:
self.live = False
elif self.direction == 'R':
if self.rect.left < MainGame.SCREEN_WIDTH - self.rect.width:
self.rect.left += self.speed
else:
self.live = False
用微分的思想使用三角函数求得子弹的新坐标(字丑勿槽)
# 子弹跟踪移动
def bulletFollowMove(self, x, y):
velocity = 10000
time = 1 / 1000
space = velocity * time
clock = pygame.time.Clock() # 创建时钟对象
clock.tick(120) # 每秒最多循环60次,即设置帧率为60
distance = math.sqrt(pow(x - self.rect.left, 2) + pow(y - self.rect.top, 2))
sina = (y - self.rect.top) / distance
cosa = (x - self.rect.left) / distance
self.rect.left = self.rect.left + space * cosa
self.rect.top = self.rect.top + space * sina
print(self.rect.left,self.rect.top, x, y)
# 展示子弹
def displayBullet(self):
MainGame.window.blit(self.image, self.rect)
# 我方子弹碰撞敌方坦克
def hitEnemyTank(self):
for eTank in MainGame.EnemyTank_List:
if pygame.sprite.collide_rect(eTank, self): # 监测两个精灵的矩形是否碰撞返回布尔值
# 产生一个爆炸效果
explode = Explode(eTank)
# 将爆炸效果加入到爆炸效果列表
MainGame.Explode_List.append(explode)
# 如果打中坦克修改状态值
self.live = False
eTank.live = False
# 敌方子弹碰撞我方坦克
def hitMyTank(self):
if pygame.sprite.collide_rect(self, MainGame.TANK_P1):
# 产生爆炸效果,并加入到爆炸效果列表中
explode = Explode(MainGame.TANK_P1)
MainGame.Explode_List.append(explode)
# 修改子弹状态
self.live = False
# 修改我方坦克状态
MainGame.TANK_P1.live = False
# 新增子弹与墙壁的碰撞
def hitWalls(self):
for wall in MainGame.Wall_List:
if pygame.sprite.collide_rect(self, wall):
# 修改子弹状态
self.live = False
wall.hp -= 1
# 没血就挂掉了
if wall.hp <= 0:
wall.live = False
# 双方子弹碰撞
def hitBullet(self):
for bullet in MainGame.Enemy_bullet_List:
if pygame.sprite.collide_rect(bullet, self): # 监测两个精灵的矩形是否碰撞返回布尔值
# 如果打中子弹修改状态值
self.live = False
bullet.live = False
# 子弹碰撞水晶
def hitHome(self):
if pygame.sprite.collide_rect(self, MainGame.Home):
# 修改子弹状态
self.live = False
# 修改我方水晶状态
MainGame.Home.live = False
3.8 爆炸类
展示子弹命中后的爆炸效果
class Explode():
def __init__(self, tank):
self.rect = tank.rect
self.step = 0
self.images = [
pygame.image.load("images/blast0.gif"),
pygame.image.load("images/blast1.gif"),
pygame.image.load("images/blast2.gif"),
pygame.image.load("images/blast3.gif"),
pygame.image.load("images/blast4.gif")
]
self.image = self.images[self.step]
self.live = True
# 展示爆炸效果
def displayExplode(self):
if self.step < len(self.images):
self.image = self.images[self.step]
MainGame.window.blit(self.image, self.rect)
time.sleep(0.03)
self.step += 1
else:
self.live = False
self.step = 0
3.9 墙壁类
展示墙壁(如想要做红白机中的砖墙和石墙的效果可以参考坦克类实现,素材中也有)
class Wall():
def __init__(self, left, top):
self.image = pygame.image.load("images/cement_wall.gif")
self.rect = self.image.get_rect()
self.rect.left = left
self.rect.top = top
# 用来判断墙壁是否应该在窗口中展示
self.live = True
# 用来记录墙壁的生命值
self.hp = 3
# 展示墙壁
def displayWall(self):
MainGame.window.blit(self.image, self.rect)
3.10 水晶类
展示我方小鸟
class Home():
def __init__(self):
self.image_symbol = pygame.image.load("images/symbol.gif")
self.image_symbol_destoryed = pygame.image.load("images/symbol_destoryed.gif")
self.rect = self.image_symbol.get_rect()
self.rect.left = MainGame.SCREEN_WIDTH / 2 - self.rect.width / 2
self.rect.top = MainGame.SCREEN_HEIGHT - self.rect.height
# 用来判断水晶还在吗
self.live = True
def displayHome(self):
if self.live:
MainGame.window.blit(self.image_symbol, self.rect)
else:
MainGame.window.blit(self.image_symbol_destoryed, self.rect)
3.11 音乐类
播放音乐
class Music():
def __init__(self, fileName):
self.fileName = fileName
# 先初始化混响器
pygame.mixer.init()
# 加载音乐文件进行播放
pygame.mixer.music.load(self.fileName)
# 开始播放音乐
def play(self):
# loops是一个可选的整数参数,默认情况下为0,表示重复音乐的次数(lopps=5,即播放一次重复5次一共6次)。如果此参数设置为-1,则音乐无限重复
pygame.mixer.music.play(loops=0)
最后调用主类的开始方法即可
if __name__ == '__main__':
MainGame().startGame()
4. 项目打包
写完之后想给别人玩,但是除了计算机专业很少人会安装python的运行环境,于是将该py文件打包为exe程序
打包问题可以参考我上一个博客,python打包exe
参考文档
官方API: https://www.pygame.org/docs/
c语言中文网: http://c.biancheng.net/pygame/sprite.html
总结
此次项目主要是了解pygame的使用,了解了pygame小游戏的总体运行情况,熟悉了python的一些语法还有一些库并且明白面向对象是什么,提升了编程能力还提高了使用Photoshop的能力。
以下是挺有用的网站:
坦克大战介绍:https://www.retrowan.com/battle-city/
画图的网站:https://mastergo.com/(那些提示图片的制作)
消除图片背景:https://www.remove.bg/zh/upload
视频转gif:https://www.apowersoft.cn/video-to-gif-online
png,jpg,gif转ico:http://www.ico51.cn/ ,https://www.butterpig.top/icopro
当然还有很多。
因为时间有限,很多功能并未完善
游戏的不足:
1)未加菜单功能
2)玩家2P功能没有开通
3)游戏中还有一些小bug由于时间原因没有完善,例如敌方复活位置刚好与我方坦克重叠会卡住双方移动(在演示中有体现)
4)未加分数机制
5)游戏ui较丑
6)地图墙壁设计太差,没有参考坦克大战的地图设计
7)未加关卡功能
8)未加奖励机制,例如吃到道具可以强化强化墙壁或者使用其他类型子弹
9)游戏结束界面比较潦草,仅仅是显示文字而已