pyqt5实现俄罗斯方块游戏
本章我们要制作一个俄罗斯方块游戏。
tetris
译注:称呼:方块是由四个小方格组成的
俄罗斯方块游戏是世界上最流行的游戏之一。是由一名叫alexey pajitnov的俄罗斯程序员在1985年制作的,从那时起,这个游戏就风靡了各个游戏平台。
俄罗斯方块归类为下落块迷宫游戏。游戏有7个基本形状:s、z、t、l、反向l、直线、方块,每个形状都由4个方块组成,方块最终都会落到屏幕底部。所以玩家通过控制形状的左右位置和旋转,让每个形状都以合适的位置落下,如果有一行全部被方块填充,这行就会消失,并且得分。游戏结束的条件是有形状接触到了屏幕顶部。
方块展示:
pyqt5是专门为创建图形界面产生的,里面一些专门为制作游戏而开发的组件,所以pyqt5是能制作小游戏的。
制作电脑游戏也是提高自己编程能力的一种很好的方式。
开发
没有图片,所以就自己用绘画画出来几个图形。每个游戏里都有数学模型的,这个也是。
开工之前:
- 用qtcore.qbasictimer()创建一个游戏循环
- 模型是一直下落的
- 模型的运动是以小块为基础单位的,不是按像素
- 从数学意义上来说,模型就是就是一串数字而已
代码由四个类组成:tetris, board, tetrominoe和shape。tetris类创建游戏,board是游戏主要逻辑。tetrominoe包含了所有的砖块,shape是所有砖块的代码。
#!/usr/bin/python3 # -*- coding: utf-8 -*- """ zetcode pyqt5 tutorial this is a tetris game clone. author: jan bodnar website: zetcode.com last edited: august 2017 """ from pyqt5.qtwidgets import qmainwindow, qframe, qdesktopwidget, qapplication from pyqt5.qtcore import qt, qbasictimer, pyqtsignal from pyqt5.qtgui import qpainter, qcolor import sys, random class tetris(qmainwindow): def __init__(self): super().__init__() self.initui() def initui(self): '''initiates application ui''' self.tboard = board(self) self.setcentralwidget(self.tboard) self.statusbar = self.statusbar() self.tboard.msg2statusbar[str].connect(self.statusbar.showmessage) self.tboard.start() self.resize(180, 380) self.center() self.setwindowtitle('tetris') self.show() def center(self): '''centers the window on the screen''' screen = qdesktopwidget().screengeometry() size = self.geometry() self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2) class board(qframe): msg2statusbar = pyqtsignal(str) boardwidth = 10 boardheight = 22 speed = 300 def __init__(self, parent): super().__init__(parent) self.initboard() def initboard(self): '''initiates board''' self.timer = qbasictimer() self.iswaitingafterline = false self.curx = 0 self.cury = 0 self.numlinesremoved = 0 self.board = [] self.setfocuspolicy(qt.strongfocus) self.isstarted = false self.ispaused = false self.clearboard() def shapeat(self, x, y): '''determines shape at the board position''' return self.board[(y * board.boardwidth) + x] def setshapeat(self, x, y, shape): '''sets a shape at the board''' self.board[(y * board.boardwidth) + x] = shape def squarewidth(self): '''returns the width of one square''' return self.contentsrect().width() // board.boardwidth def squareheight(self): '''returns the height of one square''' return self.contentsrect().height() // board.boardheight def start(self): '''starts game''' if self.ispaused: return self.isstarted = true self.iswaitingafterline = false self.numlinesremoved = 0 self.clearboard() self.msg2statusbar.emit(str(self.numlinesremoved)) self.newpiece() self.timer.start(board.speed, self) def pause(self): '''pauses game''' if not self.isstarted: return self.ispaused = not self.ispaused if self.ispaused: self.timer.stop() self.msg2statusbar.emit("paused") else: self.timer.start(board.speed, self) self.msg2statusbar.emit(str(self.numlinesremoved)) self.update() def paintevent(self, event): '''paints all shapes of the game''' painter = qpainter(self) rect = self.contentsrect() boardtop = rect.bottom() - board.boardheight * self.squareheight() for i in range(board.boardheight): for j in range(board.boardwidth): shape = self.shapeat(j, board.boardheight - i - 1) if shape != tetrominoe.noshape: self.drawsquare(painter, rect.left() + j * self.squarewidth(), boardtop + i * self.squareheight(), shape) if self.curpiece.shape() != tetrominoe.noshape: for i in range(4): x = self.curx + self.curpiece.x(i) y = self.cury - self.curpiece.y(i) self.drawsquare(painter, rect.left() + x * self.squarewidth(), boardtop + (board.boardheight - y - 1) * self.squareheight(), self.curpiece.shape()) def keypressevent(self, event): '''processes key press events''' if not self.isstarted or self.curpiece.shape() == tetrominoe.noshape: super(board, self).keypressevent(event) return key = event.key() if key == qt.key_p: self.pause() return if self.ispaused: return elif key == qt.key_left: self.trymove(self.curpiece, self.curx - 1, self.cury) elif key == qt.key_right: self.trymove(self.curpiece, self.curx + 1, self.cury) elif key == qt.key_down: self.trymove(self.curpiece.rotateright(), self.curx, self.cury) elif key == qt.key_up: self.trymove(self.curpiece.rotateleft(), self.curx, self.cury) elif key == qt.key_space: self.dropdown() elif key == qt.key_d: self.onelinedown() else: super(board, self).keypressevent(event) def timerevent(self, event): '''handles timer event''' if event.timerid() == self.timer.timerid(): if self.iswaitingafterline: self.iswaitingafterline = false self.newpiece() else: self.onelinedown() else: super(board, self).timerevent(event) def clearboard(self): '''clears shapes from the board''' for i in range(board.boardheight * board.boardwidth): self.board.append(tetrominoe.noshape) def dropdown(self): '''drops down a shape''' newy = self.cury while newy > 0: if not self.trymove(self.curpiece, self.curx, newy - 1): break newy -= 1 self.piecedropped() def onelinedown(self): '''goes one line down with a shape''' if not self.trymove(self.curpiece, self.curx, self.cury - 1): self.piecedropped() def piecedropped(self): '''after dropping shape, remove full lines and create new shape''' for i in range(4): x = self.curx + self.curpiece.x(i) y = self.cury - self.curpiece.y(i) self.setshapeat(x, y, self.curpiece.shape()) self.removefulllines() if not self.iswaitingafterline: self.newpiece() def removefulllines(self): '''removes all full lines from the board''' numfulllines = 0 rowstoremove = [] for i in range(board.boardheight): n = 0 for j in range(board.boardwidth): if not self.shapeat(j, i) == tetrominoe.noshape: n = n + 1 if n == 10: rowstoremove.append(i) rowstoremove.reverse() for m in rowstoremove: for k in range(m, board.boardheight): for l in range(board.boardwidth): self.setshapeat(l, k, self.shapeat(l, k + 1)) numfulllines = numfulllines + len(rowstoremove) if numfulllines > 0: self.numlinesremoved = self.numlinesremoved + numfulllines self.msg2statusbar.emit(str(self.numlinesremoved)) self.iswaitingafterline = true self.curpiece.setshape(tetrominoe.noshape) self.update() def newpiece(self): '''creates a new shape''' self.curpiece = shape() self.curpiece.setrandomshape() self.curx = board.boardwidth // 2 + 1 self.cury = board.boardheight - 1 + self.curpiece.miny() if not self.trymove(self.curpiece, self.curx, self.cury): self.curpiece.setshape(tetrominoe.noshape) self.timer.stop() self.isstarted = false self.msg2statusbar.emit("game over") def trymove(self, newpiece, newx, newy): '''tries to move a shape''' for i in range(4): x = newx + newpiece.x(i) y = newy - newpiece.y(i) if x < 0 or x >= board.boardwidth or y < 0 or y >= board.boardheight: return false if self.shapeat(x, y) != tetrominoe.noshape: return false self.curpiece = newpiece self.curx = newx self.cury = newy self.update() return true def drawsquare(self, painter, x, y, shape): '''draws a square of a shape''' colortable = [0x000000, 0xcc6666, 0x66cc66, 0x6666cc, 0xcccc66, 0xcc66cc, 0x66cccc, 0xdaaa00] color = qcolor(colortable[shape]) painter.fillrect(x + 1, y + 1, self.squarewidth() - 2, self.squareheight() - 2, color) painter.setpen(color.lighter()) painter.drawline(x, y + self.squareheight() - 1, x, y) painter.drawline(x, y, x + self.squarewidth() - 1, y) painter.setpen(color.darker()) painter.drawline(x + 1, y + self.squareheight() - 1, x + self.squarewidth() - 1, y + self.squareheight() - 1) painter.drawline(x + self.squarewidth() - 1, y + self.squareheight() - 1, x + self.squarewidth() - 1, y + 1) class tetrominoe(object): noshape = 0 zshape = 1 sshape = 2 lineshape = 3 tshape = 4 squareshape = 5 lshape = 6 mirroredlshape = 7 class shape(object): coordstable = ( ((0, 0), (0, 0), (0, 0), (0, 0)), ((0, -1), (0, 0), (-1, 0), (-1, 1)), ((0, -1), (0, 0), (1, 0), (1, 1)), ((0, -1), (0, 0), (0, 1), (0, 2)), ((-1, 0), (0, 0), (1, 0), (0, 1)), ((0, 0), (1, 0), (0, 1), (1, 1)), ((-1, -1), (0, -1), (0, 0), (0, 1)), ((1, -1), (0, -1), (0, 0), (0, 1)) ) def __init__(self): self.coords = [[0,0] for i in range(4)] self.pieceshape = tetrominoe.noshape self.setshape(tetrominoe.noshape) def shape(self): '''returns shape''' return self.pieceshape def setshape(self, shape): '''sets a shape''' table = shape.coordstable[shape] for i in range(4): for j in range(2): self.coords[i][j] = table[i][j] self.pieceshape = shape def setrandomshape(self): '''chooses a random shape''' self.setshape(random.randint(1, 7)) def x(self, index): '''returns x coordinate''' return self.coords[index][0] def y(self, index): '''returns y coordinate''' return self.coords[index][1] def setx(self, index, x): '''sets x coordinate''' self.coords[index][0] = x def sety(self, index, y): '''sets y coordinate''' self.coords[index][1] = y def minx(self): '''returns min x value''' m = self.coords[0][0] for i in range(4): m = min(m, self.coords[i][0]) return m def maxx(self): '''returns max x value''' m = self.coords[0][0] for i in range(4): m = max(m, self.coords[i][0]) return m def miny(self): '''returns min y value''' m = self.coords[0][1] for i in range(4): m = min(m, self.coords[i][1]) return m def maxy(self): '''returns max y value''' m = self.coords[0][1] for i in range(4): m = max(m, self.coords[i][1]) return m def rotateleft(self): '''rotates shape to the left''' if self.pieceshape == tetrominoe.squareshape: return self result = shape() result.pieceshape = self.pieceshape for i in range(4): result.setx(i, self.y(i)) result.sety(i, -self.x(i)) return result def rotateright(self): '''rotates shape to the right''' if self.pieceshape == tetrominoe.squareshape: return self result = shape() result.pieceshape = self.pieceshape for i in range(4): result.setx(i, -self.y(i)) result.sety(i, self.x(i)) return result if __name__ == '__main__': app = qapplication([]) tetris = tetris() sys.exit(app.exec_())
游戏很简单,所以也就很好理解。程序加载之后游戏也就直接开始了,可以用p键暂停游戏,空格键让方块直接落到最下面。游戏的速度是固定的,并没有实现加速的功能。分数就是游戏中消除的行数。
self.tboard = board(self) self.setcentralwidget(self.tboard)
创建了一个board类的实例,并设置为应用的中心组件。
self.statusbar = self.statusbar() self.tboard.msg2statusbar[str].connect(self.statusbar.showmessage)
创建一个statusbar
来显示三种信息:消除的行数,游戏暂停状态或者游戏结束状态。msg2statusbar
是一个自定义的信号,用在(和)board类(交互),showmessage()
方法是一个内建的,用来在statusbar上显示信息的方法。
self.tboard.start()
初始化游戏:
class board(qframe): msg2statusbar = pyqtsignal(str) ...
创建了一个自定义信号msg2statusbar
,当我们想往statusbar
里显示信息的时候,发出这个信号就行了。
boardwidth = 10 boardheight = 22 speed = 300
这些是board
类的变量。boardwidth
和boardheight
分别是board的宽度和高度。speed
是游戏的速度,每300ms出现一个新的方块。
... self.curx = 0 self.cury = 0 self.numlinesremoved = 0 self.board = [] ...
在initboard()
里初始化了一些重要的变量。self.board
定义了方块的形状和位置,取值范围是0-7。
def shapeat(self, x, y): return self.board[(y * board.boardwidth) + x]
shapeat()
决定了board里方块的的种类。
def squarewidth(self): return self.contentsrect().width() // board.boardwidth
board的大小可以动态的改变。所以方格的大小也应该随之变化。squarewidth()
计算并返回每个块应该占用多少像素--也即board.boardwidth
。
def pause(self): '''pauses game''' if not self.isstarted: return self.ispaused = not self.ispaused if self.ispaused: self.timer.stop() self.msg2statusbar.emit("paused") else: self.timer.start(board.speed, self) self.msg2statusbar.emit(str(self.numlinesremoved)) self.update()
pause()
方法用来暂停游戏,停止计时并在statusbar
上显示一条信息。
def paintevent(self, event): '''paints all shapes of the game''' painter = qpainter(self) rect = self.contentsrect() ...
渲染是在paintevent()方法里发生的qpainter
负责pyqt5里所有低级绘画操作。
for i in range(board.boardheight): for j in range(board.boardwidth): shape = self.shapeat(j, board.boardheight - i - 1) if shape != tetrominoe.noshape: self.drawsquare(painter, rect.left() + j * self.squarewidth(), boardtop + i * self.squareheight(), shape)
渲染游戏分为两步。第一步是先画出所有已经落在最下面的的图,这些保存在self.board
里。可以使用shapeat()
查看这个这个变量。
if self.curpiece.shape() != tetrominoe.noshape: for i in range(4): x = self.curx + self.curpiece.x(i) y = self.cury - self.curpiece.y(i) self.drawsquare(painter, rect.left() + x * self.squarewidth(), boardtop + (board.boardheight - y - 1) * self.squareheight(), self.curpiece.shape())
第二步是画出更在下落的方块。
elif key == qt.key_right: self.trymove(self.curpiece, self.curx + 1, self.cury)
在keypressevent()
方法获得用户按下的按键。如果按下的是右方向键,就尝试把方块向右移动,说尝试是因为有可能到边界不能移动了。
elif key == qt.key_up: self.trymove(self.curpiece.rotateleft(), self.curx, self.cury)
上方向键是把方块向左旋转一下
elif key == qt.key_space: self.dropdown()
空格键会直接把方块放到底部
elif key == qt.key_d: self.onelinedown()
d键是加速一次下落速度。
def trymove(self, newpiece, newx, newy): for i in range(4): x = newx + newpiece.x(i) y = newy - newpiece.y(i) if x < 0 or x >= board.boardwidth or y < 0 or y >= board.boardheight: return false if self.shapeat(x, y) != tetrominoe.noshape: return false self.curpiece = newpiece self.curx = newx self.cury = newy self.update() return true
trymove()
是尝试移动方块的方法。如果方块已经到达board的边缘或者遇到了其他方块,就返回false。否则就把方块下落到想要
def timerevent(self, event): if event.timerid() == self.timer.timerid(): if self.iswaitingafterline: self.iswaitingafterline = false self.newpiece() else: self.onelinedown() else: super(board, self).timerevent(event)
在计时器事件里,要么是等一个方块下落完之后创建一个新的方块,要么是让一个方块直接落到底(move a falling piece one line down)。
def clearboard(self): for i in range(board.boardheight * board.boardwidth): self.board.append(tetrominoe.noshape)
clearboard(
)方法通过tetrominoe.noshape
清空broad
。
def removefulllines(self): numfulllines = 0 rowstoremove = [] for i in range(board.boardheight): n = 0 for j in range(board.boardwidth): if not self.shapeat(j, i) == tetrominoe.noshape: n = n + 1 if n == 10: rowstoremove.append(i) rowstoremove.reverse() for m in rowstoremove: for k in range(m, board.boardheight): for l in range(board.boardwidth): self.setshapeat(l, k, self.shapeat(l, k + 1)) numfulllines = numfulllines + len(rowstoremove) ...
如果方块碰到了底部,就调用removefulllines()
方法,找到所有能消除的行消除它们。消除的具体动作就是把符合条件的行消除掉之后,再把它上面的行下降一行。注意移除满行的动作是倒着来的,因为我们是按照重力来表现游戏的,如果不这样就有可能出现有些方块浮在空中的现象。
def newpiece(self): self.curpiece = shape() self.curpiece.setrandomshape() self.curx = board.boardwidth // 2 + 1 self.cury = board.boardheight - 1 + self.curpiece.miny() if not self.trymove(self.curpiece, self.curx, self.cury): self.curpiece.setshape(tetrominoe.noshape) self.timer.stop() self.isstarted = false self.msg2statusbar.emit("game over")
newpiece()
方法是用来创建形状随机的方块。如果随机的方块不能正确的出现在预设的位置,游戏结束。
class tetrominoe(object): noshape = 0 zshape = 1 sshape = 2 lineshape = 3 tshape = 4 squareshape = 5 lshape = 6 mirroredlshape = 7
tetrominoe
类保存了所有方块的形状。我们还定义了一个noshape
的空形状。
shape类保存类方块内部的信息。
class shape(object): coordstable = ( ((0, 0), (0, 0), (0, 0), (0, 0)), ((0, -1), (0, 0), (-1, 0), (-1, 1)), ... ) ...
coordstable元组保存了所有的方块形状的组成。是一个构成方块的坐标模版。
self.coords = [[0,0] for i in range(4)]
上面创建了一个新的空坐标数组,这个数组将用来保存方块的坐标。
坐标系示意图:
上面的图片可以帮助我们更好的理解坐标值的意义。比如元组(0, -1), (0, 0), (-1, 0), (-1, -1)
代表了一个z形状的方块。这个图表就描绘了这个形状。
def rotateleft(self): if self.pieceshape == tetrominoe.squareshape: return self result = shape() result.pieceshape = self.pieceshape for i in range(4): result.setx(i, self.y(i)) result.sety(i, -self.x(i)) return result
rotateleft()
方法向右旋转一个方块。正方形的方块就没必要旋转,就直接返回了。其他的是返回一个新的,能表示这个形状旋转了的坐标。
程序展示:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 浅谈:自媒体发展的趋势和历程