前言:
划水了一个多月后, 想用pygame写一个俄罗斯方块试试!
运行结果在最下方
思路:
首先根据游戏界面来创建一个数据字典, 游戏界面为(10 * 20)的一个大小!
数据字典中的key值为坐标值, value值为0或者1, 代表这个坐标上有无方块!有方块则为1没有则为0!
数据字典大概如图这样, 游戏视图坐标的外部坐标值全部先预设为1, 用来防止方块超出游戏界面!
游戏的思路是这样的, 首先生成一个方块对象, 然后该方块固定速度落下(这里需要用来子线程来辅助, 防止因为等待方块的落下而阻塞了主线程), 用户控制方块的左右移动、改变形状和快速下落!方块下落前, 预测方块对象下落后是否发生碰撞(下落的坐标位置数据字典中的值是否为1),碰撞后再判断是否游戏结束, 没有则在数据字典中将当前方块的坐标键的值设为1,并生成一个新的方块对象, 继续循环以上步骤, 每次方块移动完后, 都需要根据当前数据字典, 将值为1的坐标全部绘制方块, 并判断是否有填满方块的一行, 如果有则将该行的值重置为0, 并让数据字典中该行坐标上面的值为1的坐标全部下降一格, 实现俄罗斯方块的得分!
实现:
首先创建一个Block类, 该类继承threading.Thread类, 以便用来开启一个属于当前方块对象的子线程用来控制方块对象的固定速度下落!
方块的种类从列表BLOCKLIST = ["TBLOCK", "SBLOCK", "ZBLOCK", "IBLOCK", "OBLOCK", "JBLOCK", "LBLOCK"]中随机获取!
class Block(threading.Thread):
def __init__(self, screen):
'''构造方法'''
#调用父类的构造方法
super(Block, self).__init__()
#设置方块的贴图
self.blue =
#当前方块坐标
self.pos = [5*30, -30]
#设置当前游戏窗口
self.screen = screen
#当前方块下落的速度
self.speed = 0.5
#随机获取当前的方块种类
self.block = BLOCKLIST[random.randint(0, len(BLOCKLIST)-1)]
#当前方块的形状(默认为形状1)
self.type = 1
#是否需要产生新的方块(是否碰撞)
self.__IsNewBlock = False
def GetIsNewBlock(self):
'''__IsNewBlock属性的接口函数'''
return __IsNewBlock
def GetBlock(self):
'''该方法用来得到当前方块形状的方块列表'''
pass
def Move(self):
'''当前方块对象的事件监听+移动方法, 包括左右移动、改变形状、快速下落'''
pass
def Collition(self):
'''当前方块对象的碰撞检测方法'''
pass
def Update(self):
'''当前方块的更新方法, 用来最终将当前方块打印到屏幕上'''
pass
def run(self):
'''该方法用来创建一个子线程, 该子线程以固定的速度控制方法的下落'''
pass
这个Block类可以创建出一个方块对象, 随机的方块种类,
GetBlock方法:可以根据当前方块的形状返回对应的坐标列表!
Move方法:可以监听当前的键盘事件, 控制方块的移动、改变形状、 快速下落!
Collition方法:用来进行碰撞检测!
Update方法:用来最终将方块显示到游戏界面中!
run方法:用来创建一个子线程控制方块的固定速度下落(为了防止方块的固定时间降落阻塞了主线程)!
游戏的整体流程:
1.绘制游戏界面
2.是否生成方块对象(同时启动子线程)
------------子线程(1.当前方块对象碰撞检测 2.当前方块对象下落)
3.当前方块对象移动检测
4.当前方块对象视图更新
5.游戏界面刷新
游戏的整体流程已经明确了, 现在我们来一块块实现它们
分块实现:
游戏界面
def GameScreen(screen, ):
'''绘制游戏界面的方法'''
global font, font_height
screen.fill((255, 255, 255))
pygame.draw.line(screen, (0, 0, 255), (301, 0), (301, 600))
这个方法目前只是对游戏界面进行简单的填充和划线,线的左边为游戏视图, 右边为辅助视图, 后面可以进行扩展, 游戏界面的大小为300, 600, 我们需要的10列 * 20行, 所以每一个方块的大小为30 * 30, 数据字典中的坐标与游戏视图中的坐标比例为1 :30
方块的移动方法
在写方块的移动方法先, 首先需要完成GetBlock方法
GetBlock方法:
def GetBlock(self):
'''根据当前的方块形状, 返回对应的形状列表'''
#T形状
if self.block == "TBLOCK":
if self.blocktype == 1:
return [(self.pos[0]-30, self.pos[1]), (self.pos[0], self.pos[1]), (self.pos[0]+30, self.pos[1]), (self.pos[0], self.pos[1]-30)]
elif self.blocktype == 2:
return [(self.pos[0], self.pos[1] - 30), (self.pos[0], self.pos[1]), (self.pos[0] + 30, self.pos[1]), (self.pos[0], self.pos[1] + 30)]
elif self.blocktype == 3:
return [(self.pos[0] - 30, self.pos[1]), (self.pos[0], self.pos[1]), (self.pos[0] + 30, self.pos[1]),(self.pos[0], self.pos[1] + 30) ]
elif self.blocktype == 4:
return [(self.pos[0] - 30, self.pos[1]), (self.pos[0], self.pos[1]), (self.pos[0], self.pos[1] + 30),(self.pos[0], self.pos[1] - 30)]
#I形状
elif self.block == "IBLOCK":
if self.blocktype == 1 or self.blocktype == 3:
return [(self.pos[0]- 30, self.pos[1]), (self.pos[0], self.pos[1]), (self.pos[0] +30, self.pos[1]), (self.pos[0] + 60, self.pos[1]) ]
elif self.blocktype == 2 or self.blocktype == 4:
return [(self.pos[0], self.pos[1] -30), (self.pos[0], self.pos[1]), (self.pos[0], self.pos[1] +30), (self.pos[0], self.pos[1] +60)]
#O形状
elif self.block == "OBLOCK":
return [(self.pos[0] -30, self.pos[1]), self.pos, (self.pos[0] - 30, self.pos[1]-30), (self.pos[0], self.pos[1] - 30)]
#S形状
elif self.block == "SBLOCK":
if self.blocktype == 1 or self.blocktype == 3:
return [(self.pos[0]+30, self.pos[1]), self.pos, (self.pos[0], self.pos[1] + 30), (self.pos[0]-30, self.pos[1] + 30)]
elif self.blocktype == 2 or self.blocktype == 4:
return [(self.pos[0], self.pos[1] - 30), self.pos, (self.pos[0] + 30, self.pos[1]), (self.pos[0] + 30, self.pos[1] + 30)]
#Z形状
elif self.block == "ZBLOCK":
if self.blocktype == 1 or self.blocktype == 3:
return [(self.pos[0] - 30, self.pos[1]), self.pos, (self.pos[0], self.pos[1]+30), (self.pos[0] + 30, self.pos[1] +30)]
elif self.blocktype == 2 or self.blocktype == 4:
return [(self.pos[0], self.pos[1] - 30), self.pos, (self.pos[0] - 30, self.pos[1]), (self.pos[0] - 30, self.pos[1] + 30)]
#L形状
elif self.block == "LBLOCK":
if self.blocktype == 1:
return [(self.pos[0], self.pos[1]-30), self.pos, (self.pos[0], self.pos[1] + 30), (self.pos[0] + 30, self.pos[1] + 30)]
elif self.blocktype == 2:
return [(self.pos[0] - 30, self.pos[1] +30), (self.pos[0] - 30, self.pos[1]), self.pos, (self.pos[0] + 30, self.pos[1])]
elif self.blocktype == 3:
return [(self.pos[0] - 30, self.pos[1] - 30), (self.pos[0], self.pos[1] - 30), self.pos, (self.pos[0], self.pos[1] + 30)]
elif self.blocktype == 4:
return [(self.pos[0] -30, self.pos[1]), self.pos, (self.pos[0] + 30, self.pos[1]), (self.pos[0]+30, self.pos[1]-30)]
#J形状
elif self.block == "JBLOCK":
if self.blocktype == 1:
return [(self.pos[0], self.pos[1]-30), self.pos, (self.pos[0], self.pos[1] + 30), (self.pos[0]-30, self.pos[1] + 30)]
elif self.blocktype == 2:
return [(self.pos[0] - 30, self.pos[1] - 30), (self.pos[0] - 30, self.pos[1]), self.pos, (self.pos[0] + 30, self.pos[1])]
elif self.blocktype == 3:
return [(self.pos[0] + 30, self.pos[1] - 30), (self.pos[0], self.pos[1] - 30), self.pos, (self.pos[0], self.pos[1] + 30)]
elif self.blocktype == 4:
return [(self.pos[0] - 30, self.pos[1]), self.pos, (self.pos[0] + 30, self.pos[1]), (self.pos[0] + 30, self.pos[1] + 30)]
return None
该方法可以返回当前方块的坐标列表, 这些坐标列表都是通过一个self.pos坐标衍生出来的, 也就是说控制方块移动的时候, 只需要控制self.pos这一个方块坐标移动即可, 现在来实现Move方法
Move方法
def Move(self):
'''BLOCK的移动函数, 用来控制方块的移动
#该方法用来监听键盘事件, 控制游戏的退出和方块的左右移动、形状改变和快速下落'''
for event in pygame.event.get():
# 监听退出事件
if event.type == QUIT:
OVER = True
exit()
# 监听键盘事件
elif event.type == KEYDOWN:
# 形状改变事件
if event.key == K_UP:
# 预测下当形状发生改变时, 会不会与存在的方块重复, 如果会则禁止改变形状
currentType = self.blocktype
IsMove = True
# 先改变当前的形状, 1 --> 2 --> 3 --> 4, 4 --> 1
self.blocktype = 1 if (self.blocktype == 4) else self.blocktype + 1
for w, h in self.GetBlock():
if (GlobalDict[w / 30, h / 30] == 1):
IsMove = False
break
# 如果禁止改变形状, 则变回原来的形状
self.blocktype = self.blocktype if (IsMove) else currentType
# 方块快速下落事件
if event.key == K_DOWN:
# 改变子线程的阻塞时间, 即可实现方块的快速下落
self.speed = 0.01
# 左右移动事件
if event.key == K_LEFT:
# 预测方块向左移动时是否存在方块, 如果为True则无法移动
IsMove = True
for w, h in self.GetBlock():
if (GlobalDict[w / 30 - 1, h / 30] == 1):
IsMove = False
break
if (IsMove):
self.pos[0] -= 30
elif event.key == K_RIGHT:
# 预测方块向右移动时是否存在方块, 如果为True则无法移动
IsMove = True
for w, h in self.GetBlock():
if (GlobalDict[w / 30 + 1, h / 30] == 1):
IsMove = False
break
if (IsMove):
self.pos[0] += 30
方块的更新方法
方块的更新方法用来最终将方块显示在游戏界面中, 在所有的事件判断完后需要调用该方法来显示当前的方块对象
def Update(self):
'''这是方块的更新函数, 用来显示方块'''
#从GetBlock()函数中获取当前方块形状的坐标列表, 并遍历显示到游戏界面上
for b in self.GetBlock():
self.screen.blit(self.blue, b)
方块的子线程(方块的下落方法)
def run(self):
'''
子线程类, 方块的下落由该线程控制, 根据self.speed来控制方块的下落速度
防止在主线程中由于等待下落时间而使主线程卡顿
'''
global GAMEOVER
while True:
# 当游戏结束时, 结束该子线程
if GAMEOVER:
break
# 碰撞检测
self.Collition()
# 如果发生了碰撞,则结束该子线程
if self.GetIsNewBlock():
break
self.pos[1] += 30
# 控制方块下落速度
time.sleep(self.speed)
接下来我们实现碰撞检测方法
Colliction方法
def Collition(self):
'''BLock的碰撞检测'''
global IsNewBlock, GAMEOVER
#遍历当前方块的x坐标和y坐标
for w, h in self.GetBlock():
#预测当前方块对象是否触碰到其他方块
if(GlobalDict[w/30, h/30 +1] == 1):
#判断是否有方块超出顶部
if(h/30 < 0):
#游戏结束
GAMEOVER = True
break
#遍历每块方块, 在数据字典中记录其坐标为1
for w, h in self.GetBlock():
GlobalDict[int(w / 30), int(h / 30)] = 1
#记录发生碰撞
self.__IsNewBlock = True
break
游戏界面刷新
这是整个流程的最后一步, 现在我们来实现它
def FlushScreen(screen, blue):
'''该方法用来在屏幕上将所有GlobalDict中值为1的坐标填充方块, 并判断是否得分'''
#满的方块判断
for h in range(20):
#从最底层开始, 一行行的判断
c_h = 19 - h #当前行数
count = 0
for w in range(10):
count += GlobalDict[w, c_h]
#当这一行的方块满了, 重置这一行的值
if count == 10:
for w in range(10):
GlobalDict[w, c_h] = 0
#将这一行上面的方块全部下降一格
for h1 in range(c_h):
c_h1 = c_h - 1 - h1 #当前行数
for w1 in range(10):
if GlobalDict[w1, c_h1] == 1:
#下降一格
GlobalDict[w1, c_h1 + 1] = 1
#当前坐标的方块值清空
GlobalDict[w1, c_h1] = 0
#将数据字典中值为1的坐标填充方块
for h in range(20):
for w in range(10):
if GlobalDict[w, h] == 1:
screen.blit(blue, (w*30,h*30))
到这里整个游戏流程的方法都实现了, 现在来编写main方法
'''全局变量'''
#俄罗斯方块的数据字典
GlobalDict = {}
#判断游戏是否结束
GAMEOVER = True
#声明一个方块形状列表
BLOCKLIST = ["TBLOCK", "SBLOCK", "ZBLOCK", "IBLOCK", "OBLOCK", "JBLOCK", "LBLOCK"]
# 初始化pygame
pygame.init()
# 设置一个字体模块
font = pygame.font.SysFont("MicrosoftYaHei", 36)
# 计算该字体所占一行的高度
font_height = font.get_linesize()
def main():
global GlobalDict, GAMEOVER
#创建一个screen
screen = pygame.display.set_mode((600, 600), 0, 32)
# 设置贴图对象
blue = pygame.image.load("img/blue.png").convert()
#设置游戏帧率
GameFrame(60)
#游戏主线程
while True:
#游戏进行时
if not GAMEOVER:
#每当需要新建一个新的方块时需要执行的语句块
if(BLOCK.GetIsNewBlock()):
# 创建一个新的方块对象
BLOCK = Block(screen)
# 启动方块对象的子线程
BLOCK.start()
#绘制游戏界面
GameScreen(screen, )
# 方块的移动检测
BLOCK.Move()
#更新当前方块的位置
BLOCK.Update()
#刷新游戏界面
FlushScreen(screen, blue)
#当游戏结束时
else:
screen.fill((255, 255, 255))
screen.blit(font.render("Please Enter 'SPACE' Start The Game", True, (0, 0, 0)), (50, 250))
for event in pygame.event.get():
if event.type == QUIT:
exit()
if event.type == KEYDOWN:
print(event.key)
if event.key == K_SPACE:
GAMEOVER = False
initGame()
# 新建一个方块对象
BLOCK = Block(screen)
# 开始当前方块对象的子线程
BLOCK.start()
#将贴图更新到主程序中
pygame.display.update()
很随意的给游戏结尾了, 当用户按下空格键即可重新开始游戏, initGame()方法用来重置数据字典
def initGame():
'''初始化俄罗斯方块数据字典'''
#游戏视图内坐标
for h in range(20):
for w in range(10):
GlobalDict[w, h] = 0
#边界
for h in range(23):
GlobalDict[-1, h-2] = 1
GlobalDict[10, h-2] = 1
#边界, 这里顶部设置为无边界, 防止刚生成方块就游戏结束
for w in range(14):
GlobalDict[w-2, -1] = 0
GlobalDict[w-2, 20] = 1
运行结果
这次的练习暂时到这里就结束了, 有BUG或者不足的地方希望各位大佬指出