适合初学者超详细Python制作俄罗斯方块教程
- 背景
- 实现过程
- 绘制窗口
- 添加控件与文本
- 添加画布
- 绘制一小方块
- 俄罗斯方块定义
- 游戏开始前的初始化
- 功能模块设计
- 计算分数
- 显示下一个方块
- 联合
- 画出方块组合(俄罗斯方块)
- 下降
- 可移动判断
- 消行
- 闪烁
- 旋转
- 加速
- 左右移动
- 直接下落
- 按钮功能
- 按键交互
- 添加背景图
- 成果展示
背景
我刚学Python三天,但不想一直看书,于是在网上学习了一些用Python制作俄罗斯方块的代码,有的代码没有注释,并且比较难理解,这里我找到了一个博主的代码,思路很清晰,游戏运行后也很顺畅,但代码没有注释,所以我就自己拿来学习并做了一定的修改。为了方便和我一样想拿小游戏练习巩固基础知识的小伙伴,我将自己的学习收获分享给大家,希望大家学习代码中能快速理解!
实现过程
过程主要分为以下几部分:
- 窗口设计
- 画布设计
- 方块定义与设置
- 方块功能方法设计
- 控件功能方法设计
大致过程就分为这几部分,具体的流程见下面详细分析,原理啥的都会放在具体的部分中讲解
绘制窗口
在Python中,我们可以用tkinter模块进行窗口设计。为了使用此模块,我们首先需要import它:
from tkinter import *
import time
import random
import math
from tkinter import messagebox
from PIL import ImageTk, Image
在这里,我一下子把所有要导入的库块全贴出来了,下面对其解释:
from tkinter import * :导入tkinter库中所有的类、变量、函数等信息。
import time : 导入时间库(因为在代码中用到了sleep函数)
import random :导入随机数生成库
from tkinter import messagebox : 导入messagebox弹窗库
from PIL import ImageTk, Image :从PIL导入ImageTk、Image库,用于添加背景图片
前四个都是Python自带的库,不需要我们自己安装,但是最后一个需要我们安装PIL库(PIL是Python处理图像的库),安装方法在这里也给出:
(1)找到pip.exe所在的文件夹,我的是在D:\Python37\Scripts (一般pip.exe都在Python安装目录中Scripts文件夹里)
(2)运行cmd,将路径跳转到pip.exe所在的文件夹(我的是Scripts),输入pip install pillow , 电脑将会自动搜索pillow并进行安装,安装成功将会看到Successfully字样:
此时,Pillow就安装成功了。
之后,我们进行窗口绘制,代码如下:
self.win = Tk() #创建窗口self.win.title("俄罗斯方块_by ZSQ") self.win.geometry('450x610+400+100') #self.win.geometry('w×h+x+y'),w:窗口宽度,h:窗口长度,x,y是窗口在屏幕上的位置self.win.resizable(0, 0) #窗口的长宽不可改变self.win.mainloop() #让窗口运行起来
这样我们一个制作了一个窗口,效果如下:
添加控件与文本
下面我们添加四个按钮,分别是开始、暂停、重新开始、退出。多个文本,比如:等级、分数,对应代码如下:
self.pauseBut = Button(self.win, text="暂停", bg='#7B8E9C', height=1, width=10, font=(10), command=self.pause)self.pauseBut.place(x=338, y=473)self.startBut = Button(self.win, text="开始",bg = '#DEC8B0', height=1, width=10, font=(10), command=self.startgame)self.startBut.place(x=338, y=430)self.restartBut = Button(self.win, text="重新开始",bg = '#EBB78C',height=1, width=10, font=(10), command=self.restart)self.restartBut.place(x=338, y=516)self.quitBut = Button(self.win, text="退出", height=1,bg = '#CA4F53' ,width=10, font=(10), command=self.quitgame) self.quitBut.place(x=338, y=559)self.lab_score = Label(self.win, text="分数:0", font=("宋体",16,"normal"))self.lab_score.place(x=335, y=50)self.lab_scoreEx = Label(self.win, text = "每200升级一次", font = ("宋体",8,"normal"))self.lab_scoreEx.place(x=335,y=80)self.lab_grade = Label(self.win, text="等级:1", font=("宋体",16,"normal"))self.lab_grade.place(x=335, y=115)self.lab_next = Label(self.win, text = "下一个:",font=("宋体",16,"normal"))self.lab_next.place(x=335,y=180)
这里Button、Label都是控件,括号里是设置他们的参数,不懂的小伙伴可以自行百度(其实很好理解的),其中四个Button中的command是触发,即点击了按钮就触发相应方法(后面讲述)。添加控件后的效果如下:
添加画布
绘制好窗口,我们利用Canvas组件添加画布,我们这里添加两个画布,分别是下落画布(huabu)和下一块显示画布(huabu_right),对应代码如下:
self.huabu = Canvas(self.win ,bg ='#DEF6FF' ,height=600, width=COLUMN * (BIANCHANG+1), takefocus=True) #方块下落画布self.huabu.place(x=2, y=2) #画布的位置self.huabu_right = Canvas(self.win,bg = '#DEF6FF',height=100, width=100) #下一个方块预览画布self.huabu_right.place(x=335,y=210)
因为其中涉及到了一些变量的使用(例如COLUMN, BIANCHANG等),这些都是事先声明好的全局变量,在这里贴出声明代码:
BIANCHANG = 19 #一小方块边长COLOR = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', '#00C5CD', '#00EE76', '#388E8E', '#556B2F', '#6B8E23','#8B2252', '#8B6969', '#A0522D', '#BC8F8F', '#BC8F3F', 'black'] #颜色COLUMN = 16 #列数ROW = 30 #行数imgpath = 'menglong_.gif' #背景图片 img = Image.open(imgpath) #获取背景图片
回到画布上,刚刚添加完画布后,整体效果如下:
至此,窗口、画布的添加到此完毕,其中细节我没有过多讲解(比如Canvas里的参数含义,这些都可以自行搜索,讲的都很详细),接下来就是设计方块以及实现各种功能。
绘制一小方块
我们定义一个类(fangk)来在画布上具体位置上画一小块,代码如下:
class fangk: def __init__(self, huabu, col, row): #属于对象的数据成员, #self参数必须是第一个形参,它代表对象本身,在类的实例方法中访问实例属性时,需要以self为前缀self.huabu = huabu self.col, self.row = col, row #col-->第几行的行,row-->第几列的列self.color = COLOR[self.row % 16] #方块颜色self.havafk = False #界面上有无方块的标志def setvisible(self, statu): #设置显示与否if statu > 0:x = self.col * (BIANCHANG+1) y = 582 - (ROW - self.row - 1) * (BIANCHANG + 1) self.fk = self.huabu.create_rectangle(x, y, x + BIANCHANG, y + BIANCHANG, fill = self.color) #画块#(creat_rectangle(x1,y1,x2,y2):画一个矩形(对角顶点坐标为(x1,y1),(x2,y2))self.line1 = self.huabu.create_line(x, y, x, y + BIANCHANG, fill = 'white') #边线加白#create_line() :画线self.line2 = self.huabu.create_line(x, y, x + BIANCHANG, y, fill = 'white')self.havefk = Trueelif statu ==0 and self.havefk: self.huabu.delete(self.fk)#detele()删除对变量的引用self.huabu.delete(self.line2)self.huabu.delete(self.line1)self.havefk = Falseelse: return -1def set_color(self, color): #设置颜色self.color = colorreturn self
这段代码的作用是在画布上第row行,第col列绘制一个方块,并设置其颜色,再通过setvisible方法设置是否可见。即:
在这段代码中,因为y轴正方向与行数增加顺序是相反的,行数所对应的y值为582 - (ROW - self.row - 1) * (BIANCHANG + 1) ,
之后,我们在elsfk类中遍历整个画布,就能画出所有方块,只需要一行代码:
self.fangkuai_map = [ [fangk(self.huabu, i, j) for i in range(COLUMN)] for j in range(ROW) ]
俄罗斯方块定义
我们知道,俄罗斯方块一共有19种,每个都是由四个小方块组成,我们选其中一小块当参考(我们叫它原块,坐标为(0,0)),如下所示:
我们用一个列表(fk_type)存储每个类型的方块,代码如下:
def __init__(self):self.fk_type = [[(0, 0, 1, 1), (0, 1, 0, 1)], # 正方形 [(0, 0, 0, 0), (1, 0, -1, -2)], # 长条 [(-1, 0, 1, 2), (0, 0, 0, 0)], # [(0, 1, 0, -1), (0, 1, 1, 0)], # S [(0, -1, -1, 0), (0, 1, 0, -1)], [(0, -1, 0, 1), (0, 1, 1, 0)], # Z [(0, 1, 1, 0), (0, 1, 0, -1)], [(0, 0, -1, 1), (0, 1, 0, 0)], # T型 [(0, 0, 0, 1), (0, 1, -1, 0)],[(0, 1, 0, -1), (0, 0, -1, 0)],[(0, 0, -1, 0), (0, 1, 0, -1)],[(0, 1, 1, -1), (0, -1, 0, 0)], # J [(0, 1, 0, 0), (0, 1, 1, -1)],[(0, -1, -1, 1), (0, 1, 0, 0)],[(0, 0, 0, -1), (0, 1, -1, -1)],[(0, 1, 1, -1), (0, 1, 0, 0)], # L [(0, -1, 0, 0), (0, 1, 1, -1)],[(0, -1, -1, 1), (0, -1, 0, 0)],[(0, 0, 0, 1), (0, 1, -1, -1)]]
其中[(),()]第一个括号里是横坐标,第二个括号里是对应的纵坐标。
游戏开始前的初始化
初始化的作用是在游戏开始前对一些变量和窗口的控件属性进行初始赋值操作,并且生成下一个物块。
代码:
def initgame(self):self.map = [[0] * COLUMN for _ in range(ROW)] #初始化画布self.map_before = [[0] * COLUMN for _ in range(ROW)] self.base_map = [[0] * COLUMN for _ in range(ROW)] self.color_map = [[0] * COLUMN for _ in range(ROW)] self.score = 0 #分数self.grade = 1 #等级#self.speed = 20 #速度self.next_fangk_type = random.randrange(0, 19) #下一个方块类型self.next_color = random.randrange(0, 17) #下一个方块的颜色self.lab_score.config(text ="分数:0")self.lab_grade.config(text = "等级:1")self.lab_next.config(text = "下一个:")self.lab_scoreEx.config(text = "说明:每200升级一次")self.lock_operation = False #锁,类似于生产者消费者问题中的锁self.last_row = 1 #上次消除的行数#self.sum_row = 0 #总共消除的行数self.interval = 0 #下降间隔
map是一个列表,存储的是每一行有无物块的标志,1代表有物块,0则代表没有物块:
map_before:存储前一个map数据
base_map: 底层(基础)数据,比如某个方块移动时其他的map数据
color_map: 颜色数据,每一个地方放的不再是0或1 ,而是具体的颜色16进制值
功能模块设计
讲解都放在了注释上面,大家看我的注释即可。
计算分数
def cal_score(self, row): #row -->此次消除的行数'''计算分数'''self.score = self.score + [row * 10 , int(row * 10 * (1 + row/10))][self.last_row < row] #这次消除行数比上次的多即可得到加成#[a,b][c]-->c成立选b,c不成立选aself.lab_score.config(text="分数:" + str(self.score))self.last_row = row #self.sum_row += row self.grade = self.score //200 +1 #分数每升高200增加一级self.lab_grade.config(text = "等级:" + str(self.grade))
计算分数采用激励机制,此次消除行数大于上次,可以得到额外加分。
显示下一个方块
def next_fk(self):'''显示下一个方块'''self.cur_color = self.next_colorself.cur_fk_type = self.next_fangk_typeself.next_color = random.randrange(0, 17)self.next_fangk_type = random.randrange(0, 19)for i in self.huabu_right.find_all(): #清空上次显示画布self.huabu_right.delete(i)for i in range(4):fangk(self.huabu_right, 2 + self.fk_type[self.next_fangk_type][0][i], #列2 - self.fk_type[self.next_fangk_type][1][i]).set_color(COLOR[self.next_color]).setvisible(1)# 位置 颜色 进行显示 #y轴正方向与行数增加方向相反self.cur_fk = self.fk_type[self.cur_fk_type]self.cur_location = [{'x': 7, 'y':1},{'x': 7, 'y': 0}][self.cur_fk_type in (2, 11, 17)] #根据当前物块类型选择初始坐标, x对应列数,y对应行数self.combind() self.draw_map() if not self.test_map(): afterFail = messagebox.askquestion("失败", "游戏失败,点击\"是\"重新开始,否则退出游戏") #返回yes或noif afterFail == 'yes':self.lock_operation = True self.restart()else:self.win.destroy()
联合
def combind(self):'''组合,坐标与行列之间的映射,确定显示方块的位置与颜色 '''self.map = [a[:] for a in self.base_map] for i in range(len(self.cur_fk[1])): #坐标和行列之间的映射x = self.cur_location['x'] + self.cur_fk[0][i]y = self.cur_location['y'] - self.cur_fk[1][i]self.map[y][x] = 1 #y高度对应行数,x对应列数,map画布就是起到决定那些位置显示方块self.color_map[y][x] = self.cur_color #对应位置设置当前颜色
画出方块组合(俄罗斯方块)
def draw_map(self):'''画出刚出来的俄罗斯方块'''for i in range(ROW):for j in range(COLUMN):if self.map[i][j] != self.map_before[i][j]:self.fangkuai_map[i][j].set_color(COLOR[self.color_map[i][j]]).setvisible(self.map[i][j])#把所有方块画布上的方块全设成一个颜色,但是只有map画布上为1的才显示self.map_before = [i[:] for i in self.map] #保存当前的显示情况self.win.update() #窗口更新
下降
def drop(self):'''物块下降'''self.cur_location['y'] += 1 #纵坐标增大if self.cur_location['y'] - min(self.cur_fk[1]) < ROW and self.test_map(): #没有顶部并且可以下落self.combind()self.draw_map()return Trueelse:self.cur_location['y'] -= 1 #判断不能下降后,纵坐标-1self.base_map = [i[:] for i in self.map]self.delete_row() #判断是否可以消行self.draw_map() self.next_fk()return False
可移动判断
def test_map(self):'''可移动判断'''for i in range(len(self.cur_fk[0])):x = self.cur_location['x'] + self.cur_fk[0][i]y = self.cur_location['y'] - self.cur_fk[1][i] #这里self.cur_location['y'],self.cur_location['x'] 会提前加减(为了判断)if self.base_map[y][x] > 0: return False # >0说明此位置有物块,不可移动return True
消行
def delete_row(self):'''删行'''del_row = []for i in range(max(self.cur_fk[1]) - min(self.cur_fk[1]) + 1): #求遍历的行数if self.base_map[self.cur_location['y'] - min(self.cur_fk[1]) - i ] == [1] * COLUMN: #判断所一物块下落完后的行是否全为1del_row.append(self.cur_location['y'] - min(self.cur_fk[1]) - i) #存储要删除的行(第几行)if not del_row == []: #del_row不为空说明有行要删除self.flash(del_row) #闪烁要删的行self.base_map = [r for r in self.base_map if not r == [1] * COLUMN] #把不是全1的行抽出来(相当于删行)self.base_map = ([[0] * COLUMN ] * (30 - len(self.base_map))) + self.base_map #填0行#每消一行,相当于顶部多一行全0, 所以必须是[0,0,0,...0] + self.base_mapself.cal_score(len(del_row)) #计算分数
闪烁
def flash(self, del_rows):'''闪烁消除的行'''self.lock_operation = Truefor times in range(6): #闪烁次数for j in del_rows: for i in self.fangkuai_map[j]: #fangkuai_map[j] ---> 取出第j行i.setvisible(int(0.5 + times % 2 *0.5)) #闪烁效果, times为偶数,int(0)=0,times为奇数,int(1)=1self.win.update()time.sleep(0.2) #设置闪烁间隔(其实就是刷新间隔)self.lock_operation = False
旋转
def rotate(self, event):'''旋转'''if not self.lock_operation:if not self.cur_fk_type ==0 :temp = self.cur_fk_typeself.cur_fk_type = [(self.cur_fk_type - 7) // 4 * 4 + self.cur_fk_type % 4 + 7,(self.cur_fk_type - 1) // 2 * 2 + self.cur_fk_type % 2 + 1][self.cur_fk_type in range(1,7)]#长条(1,2)、Z(3,4)、S型(5,6)方块变换都只有一种,变换的公式为# 变换后的类型序号 = (当前序号-1)//2 * 2 + 当前序号 % 2 + 1 # 1-->2, 2-->1; 3-->4, 4-->3; 5-->6, 6-->5#L, J, T型的变换公式:变换后的类型序号 = (当前序号-7)//4 * 4 + 当前序号 % 4 + 7# T型:10-->9-->8-->7-->10self.cur_fk = self.fk_type[self.cur_fk_type]if self.cur_location['x'] + min(self.cur_fk[0]) + 1 <= 0 or self.cur_location['x'] + max(self.cur_fk[0]) >= COLUMN or not self.test_map() or self.cur_location['y'] + min(self.cur_fk[1]) + 1 <= 0: #看旋转后是否出界,出界则不旋转print('can\'t rotate')self.cur_fk_type = temp #还原self.cur_fk = self.fk_type[self.cur_fk_type]#四种情况不能旋转:# 旋转后会出左边界 ---> self.cur_location['x'] + min(self.cur_fk[0]) + 1 <= 0 # 旋转后会出右边界 ---> self.cur_location['x'] + max(self.cur_fk[0]) >= COLUMN# 直接旋转会和已有的物块重合 ---> self.test_map()# 旋转后会出上边界 ---> self.cur_location['y'] + min(self.cur_fk[1]) + 1 < 0self.combind()self.draw_map()
加速
def quick_drop(self, event):if not self.lock_operation: #没有上锁,可按键self.drop()
左右移动
def move_left(self, event):if not self.lock_operation:self.cur_location['x'] -= 1if self.cur_location['x'] + min(self.cur_fk[0]) + 1 > 0 and self.test_map(): #判断是否可以左移,以及左移后是否出了左边界self.combind()self.draw_map()else:self.cur_location['x'] += 1def move_right(self, event):if not self.lock_operation:self.cur_location['x'] += 1if self.cur_location['x'] + max(self.cur_fk[0]) < COLUMN and self.test_map(): #判断是否可以右移,以及右移后是否出了右边界self.combind()self.draw_map()else:self.cur_location['x'] -= 1
直接下落
def down_straight(self, event):while not self.lock_operation and self.drop(): pass #利用循环让它自动下落,因为循环时间极短,所以人眼看的就有直接到底的感觉
按钮功能
def pause(self):messagebox.showinfo("暂停", "游戏暂停中,点击确认返回游戏")def restart(self):for i in self.huabu.find_all(): self.huabu.delete(i) self.initgame() #重新开始后需要初始photo = ImageTk.PhotoImage(img) #重新加载背景图片self.huabu.create_image(160,305,image = photo)self.startgame() #初始完毕后开始游戏def quitgame(self):isQuit = messagebox.askquestion('退出', "确定要退出吗?")if isQuit == 'yes': self.win.destroy() #用quit()方法开始后退不出去def startgame(self):self.startBut.config(state = DISABLED) #点击完开始按钮后不可再点击self.next_fk() #下一俄罗斯方块 while not self.lock_operation: time.sleep(0.05) if self.interval == 0: self.drop()self.interval = (self.interval + 1) % (22 - self.grade *4) #每隔一段时间自动掉落self.win.update() #刷新窗口界面
按键交互
按键交互模块也比较简单,设置固定按键对应方法即可。
self.huabu.bind_all('<KeyPress-a>', self.move_left) #字母(例如A键是KeyPress-a而不能写成Keypress-A)必须小写self.huabu.bind_all('<KeyPress-d>', self.move_right)self.huabu.bind_all('<KeyPress-s>', self.quick_drop)self.huabu.bind_all('<KeyPress-w>', self.rotate)self.huabu.bind_all('<Left>', self.move_left)self.huabu.bind_all('<Right>', self.move_right)self.huabu.bind_all('<Down>', self.quick_drop)self.huabu.bind_all('<Up>', self.rotate)self.huabu.bind_all('<KeyPress-space>', self.down_straight)
添加背景图
为了美观,我们可以给画布添加背景图,因为在之前定义全局变量时已经引入了图片,所以我们只需在elsfk类__init__方法中加入如下代码:
photo = ImageTk.PhotoImage(img)self.huabu.create_image(160,302,image = photo)
成果展示
至此我们的整个俄罗斯方块的设计就算完成了,希望我的教程能够帮助大家快速理解学习。如果大家想下载全部代码,可以进入我的资源进行下载。但是我希望大家能自己敲一遍吖,锻炼自己的思考与逻辑能力!