本帖最后由 lightninng 于 2015-5-16 18:03 编辑
11 用PyQt5做俄罗斯方块
11.1 俄罗斯方块简介编写计算机游戏非常有挑战性,早晚有一天,一名程序员会希望编写一个计算机游戏。事实上,很多人因为玩游戏并希望创造自己的游戏而对编程产生兴趣的。编写计算机游戏可以大大的提高你的编程水平。 俄罗斯方块游戏是现有的最受欢迎的计算机游戏之一。游戏最初由一名俄罗斯程序员Alexey Pajitnov于1985年设计并编写。从那时开始,俄罗斯方块游戏的众多变种出现在几乎每种计算机平台上。甚至我的移动电话也有一个改版的俄罗斯方块游戏。 俄罗斯方块也叫“掉落方块解迷游戏”。在这个游戏中,我们有七种不同的形状叫做tetrominoes。S形、Z形、T形、L形、线形、反L形和方块。每个形状由四个小方块组成。形状从顶板上落下来。俄罗斯方块游戏的目标是移动并旋转形状以便将他们尽可能的组合起来。如果我们控制填充满了一行,这一行就会消失,并且我们的得分。直到方块顶到顶部游戏结束。 PyQt5被设计成用来编写程序的工具包。有其他的库是专门设计用来编写计算机游戏的。尽管如此,PyQt5和其他应用程序工具包也可以被用来编写游戏。 下面的例子是俄罗斯方块游戏的改版,随PyQt5的安装文件而存在。 我们没有为我们的俄罗斯方块使用图片。我们通过PyQt5编程工具包中的绘图API来绘制方块。每个计算机游戏中,都有数学模型,俄罗斯方块游戏也是。 11.2 游戏内部的设计我们用QtCore.QBasicTimer()来创建一个游戏循环 绘制俄罗斯方块 图形一块一块的移动而不是一个像素一个像素的移动。 PS:以下第一版程序,这个版本是教程中的版本,我只针对信号槽(原版用的是pyqt4.5之前的信号槽)、还有一些不能运行通过的部分进行了修改 - # -*- coding: utf-8 -*-
- """俄罗斯方块第一版"""
- import sys
- import random
- from PyQt5 import QtWidgets, QtCore, QtGui
- class Tetris(QtWidgets.QMainWindow):
- def __init__(self):
- super(Tetris, self).__init__()
- self.setWindowTitle("俄罗斯方块")
- self.setGeometry(300, 300, 300, 682)
- self.tetris_board = Board(self)
- self.setCentralWidget(self.tetris_board)
- self.status_bar = self.statusBar()
- self.tetris_board.messages_to_satusbar.connect(self.status_bar.showMessage)
- self.tetris_board.start()
- self.center()
- def center(self):
- screen = QtWidgets.QDesktopWidget().screenGeometry()
- size = self.geometry()
- self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)
- class Board(QtWidgets.QFrame):
- board_width = 10
- board_height = 22
- speed = 300
- messages_to_satusbar = QtCore.pyqtSignal(str)
- def __init__(self, parent):
- super(Board, self).__init__(parent)
- self.timer = QtCore.QBasicTimer()
- self.is_waiting_after_line = False
- self.cur_piece = Shape()
- self.next_piece = Shape()
- self.cur_x = 0
- self.cur_y = 0
- self.num_lines_moved = 0
- self.board = []
- self.setFocusPolicy(QtCore.Qt.StrongFocus)
- self.is_started = False
- self.is_paused = False
- self.clear_board()
- self.next_piece.set_random_shape()
- def shape_at(self, x, y):
- return self.board[int((y*Board.board_width) + x)]
- def set_shape_at(self, x, y, shape):
- self.board[int((y * Board.board_width) + x)] = shape
- def square_width(self):
- # print("width", self.contentsRect().width() / Board.board_width)
- return self.contentsRect().width() / Board.board_width
- def square_height(self):
- # print("square_height", self.contentsRect().height() / Board.board_height)
- return self.contentsRect().height() / Board.board_height
- def start(self):
- if self.is_paused:
- return
- self.is_started = True
- self.num_lines_moved = 0
- self.clear_board()
- self.messages_to_satusbar.emit(str(self.num_lines_moved))
- self.new_piece()
- self.timer.start(Board.speed, self)
- def pause(self):
- if not self.is_started:
- return
- self.is_paused = not self.is_paused
- if self.is_paused:
- self.timer.stop()
- self.messages_to_satusbar.emit("paused")
- else:
- self.timer.start(Board.speed, self)
- self.messages_to_satusbar.emit(str(self.num_lines_moved))
- self.update()
- def paintEvent(self, event):
- paint = QtGui.QPainter(self)
- rect = self.contentsRect()
- board_top = rect.bottom() - Board.board_height * self.square_height()
- for i in range(Board.board_height):
- for j in range(Board.board_width):
- shape = self.shape_at(j, Board.board_height - i - 1)
- if shape != Tetrominoes.NoShape:
- self.draw_square(paint, rect.left() + j*self.square_width(),
- board_top + i*self.square_height(), shape)
- if self.cur_piece.shape() != Tetrominoes.NoShape:
- for i in range(4):
- x = self.cur_x + self.cur_piece.x(i)
- y = self.cur_y - self.cur_piece.y(i)
- self.draw_square(paint, rect.left() + x*self.square_width(),
- board_top + (Board.board_height-y-1)*self.square_height(),
- self.cur_piece.shape())
- def keyPressEvent(self, event):
- if not self.is_started or self.cur_piece.shape() == Tetrominoes.NoShape:
- QtWidgets.QWidget.keyPressEvent(self, event)
- return
- key = event.key()
- if key == QtCore.Qt.Key_P:
- self.pause()
- return
- if self.is_paused:
- return
- elif key == QtCore.Qt.Key_Left:
- self.try_move(self.cur_piece, self.cur_x - 1, self.cur_y)
- elif key == QtCore.Qt.Key_Right:
- self.try_move(self.cur_piece, self.cur_x + 1, self.cur_y)
- elif key == QtCore.Qt.Key_Down:
- self.try_move(self.cur_piece.rotated_right(), self.cur_x, self.cur_y)
- elif key == QtCore.Qt.Key_Up:
- self.try_move(self.cur_piece.rotated_left(), self.cur_x, self.cur_y)
- elif key == QtCore.Qt.Key_Space:
- self.drop_down()
- elif key == QtCore.Qt.Key_D:
- self.one_line_down()
- else:
- QtWidgets.QWidget.keyPressEvent(self, event)
- def timerEvent(self, event):
- if event.timerId() == self.timer.timerId():
- if self.is_waiting_after_line:
- self.is_waiting_after_line = False
- self.new_piece()
- else:
- self.one_line_down()
- else:
- QtWidgets.QFrame.timerEvent(self, event)
- def clear_board(self):
- for i in range(Board.board_height * Board.board_width):
- self.board.append(Tetrominoes.NoShape)
- def drop_down(self):
- new_y = self.cur_y
- while new_y > 0:
- if not self.try_move(self.cur_piece, self.cur_x, new_y - 1):
- break
- new_y -= 1
- self.piece_dropped()
- def one_line_down(self):
- if not self.try_move(self.cur_piece, self.cur_x, self.cur_y - 1):
- self.piece_dropped()
- def piece_dropped(self):
- for i in range(4):
- x = self.cur_x + self.cur_piece.x(i)
- y = self.cur_y - self.cur_piece.y(i)
- self.set_shape_at(x, y, self.cur_piece.shape())
- self.remove_full_lines()
- if not self.is_waiting_after_line:
- self.new_piece()
- def remove_full_lines(self):
- num_full_lines = 0
- rows_to_remove = []
- for i in range(Board.board_height):
- n = 0
- for j in range(Board.board_width):
- if not self.shape_at(j, i) == Tetrominoes.NoShape:
- n += 1
- if n == 10:
- rows_to_remove.append(i)
- rows_to_remove.reverse()
- for m in rows_to_remove:
- for k in range(m, Board.board_height):
- for l in range(Board.board_width):
- self.set_shape_at(l, k, self.shape_at(l, k + 1))
- num_full_lines += len(rows_to_remove)
- if num_full_lines > 0:
- self.num_lines_moved += num_full_lines
- self.messages_to_satusbar.emit(str(self.num_lines_moved))
- self.is_waiting_after_line = True
- self.cur_piece.set_shape(Tetrominoes.NoShape)
- self.update()
- def new_piece(self):
- self.cur_piece = self.next_piece
- self.next_piece.set_random_shape()
- self.cur_x = Board.board_width / 2 + 1
- self.cur_y = Board.board_height - 1 + self.cur_piece.min_y()
- if not self.try_move(self.cur_piece, self.cur_x, self.cur_y):
- self.cur_piece.set_shape(Tetrominoes.NoShape)
- self.timer.stop()
- self.is_started = False
- self.messages_to_satusbar.emit("游戏结束")
- def try_move(self, new_piece, new_x, new_y):
- for i in range(4):
- x = new_x + new_piece.x(i)
- y = new_y - new_piece.y(i)
- if x < 0 or x >= Board.board_width or y < 0 or y >= Board.board_height:
- return False
- if self.shape_at(x, y) != Tetrominoes.NoShape:
- return False
- self.cur_piece = new_piece
- self.cur_x = new_x
- self.cur_y = new_y
- self.update()
- return True
- def draw_square(self, painter, x, y, shape):
- color_table = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC, 0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
- color = QtGui.QColor(color_table[shape])
- painter.fillRect(x + 1, y + 1, self.square_width() - 2, self.square_height() - 2, color)
- painter.setPen(color.lighter())
- painter.drawLine(x, y + self.square_height() - 1, x, y)
- painter.drawLine(x, y, x + self.square_width() - 1, y)
- painter.setPen(color.darker())
- painter.drawLine(x + 1, y + self.square_height() - 1,
- x + self.square_width() - 1, y + self.square_height() - 1)
- painter.drawLine(x + self.square_width() - 1, y + self.square_height() - 1,
- x + self.square_width() - 1, y + 1)
- class Tetrominoes(object):
- NoShape = 0
- ZShape = 1
- SShape = 2
- LineShape = 3
- TShape = 4
- SquareShape = 5
- LShape = 6
- MirroredLShape = 7
- class Shape(object):
- coords_table = (((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.piece_shape = Tetrominoes.NoShape
- self.set_shape(Tetrominoes.NoShape)
- def shape(self):
- return self.piece_shape
- def set_shape(self, shape):
- table = Shape.coords_table[shape]
- for i in range(4):
- for j in range(2):
- self.coords[i][j] = table[i][j]
- self.piece_shape = shape
- def set_random_shape(self):
- self.set_shape(random.randint(1, 7))
- def x(self, index):
- return self.coords[index][0]
- def y(self, index):
- return self.coords[index][1]
- def set_x(self, index, x):
- self.coords[index][0] = x
- def set_y(self, index, y):
- self.coords[index][1] = y
- def min_x(self):
- m = self.coords[0][0]
- for i in range(4):
- m = min(m, self.coords[i][0])
- return m
- def max_x(self):
- m = self.coords[0][0]
- for i in range(4):
- m = max(m, self.coords[i][0])
- return m
- def min_y(self):
- m = self.coords[0][1]
- for i in range(4):
- m = min(m, self.coords[i][1])
- return m
- def max_y(self):
- m = self.coords[0][1]
- for i in range(4):
- m = max(m, self.coords[i][1])
- return m
- def rotated_left(self):
- if self.piece_shape == Tetrominoes.SquareShape:
- return self
- result = Shape()
- result.piece_shape = self.piece_shape
- for i in range(4):
- result.set_x(i, self.y(i))
- result.set_y(i, -self.x(i))
- return result
- def rotated_right(self):
- if self.piece_shape == Tetrominoes.SquareShape:
- return self
- result = Shape()
- result.piece_shape = self.piece_shape
- for i in range(4):
- result.set_x(i, -self.y(i))
- result.set_y(i, self.x(i))
- return result
- app = QtWidgets.QApplication(sys.argv)
- tetris = Tetris()
- tetris.show()
- sys.exit(app.exec_())
复制代码
首先,说说修改一个小bug和关于信号槽的部分之前没有涉及到的内容: 1、Board类中的shape_at和set_shape_at方法中,对self.board进行了索引取值操作,但是给出的索引是用一个表达式,这个表达式计算出来的值是float型导致报错,观察一下,式中的变量均为int型,于是用int()函数强行把索引值转化为int型,原代码和改后代码如下 - def shape_at(self, x, y):
- return self.board[(y*Board.board_width) + x]
- def shape_at(self, x, y):
- return self.board[int((y*Board.board_width) + x)]
复制代码2、关于PyQt5中的信号槽问题在前面的章节中我已经作过介绍,现在说一下信号槽的另外一个问题:怎么给槽函数传递参数? 其实主要就是3点: (1)在创建信号变量时,按槽函数的传入参数顺序,将对应参数的类型传入QtCore.pyqtSignal(),渣语文见谅,不懂的请看程序中的做法: - class Board(QtWidgets.QFrame):
- board_width = 10
- board_height = 22
- speed = 300
- messages_to_satusbar = QtCore.pyqtSignal(str)
复制代码 可以看到最后一句中我们指定要传入的参数为一个str类型的变量(2)在用emit()方法发射信号时,将要传入槽函数的参数作为emit方法的参数传入,如这个程序中 - if not self.try_move(self.cur_piece, self.cur_x, self.cur_y):
- self.cur_piece.set_shape(Tetrominoes.NoShape)
- self.timer.stop()
- self.is_started = False
- self.messages_to_satusbar.emit("游戏结束")
复制代码 当符合某种条件时,要在状态栏显示"游戏结束 ",将"游戏结束"这个字符串传入信号message_to_statusbar所对应的槽函数(3)定义信号时定义的、信号发谢时传入的、以及槽函数定义中的变量类型,数量和顺序,必须一致,如这个程序中 - self.tetris_board.messages_to_satusbar.connect(self.status_bar.showMessage)
复制代码 这里的showMessage是QStatusBar自带的方法,我们看看它的这个方法的定义- showMessage(...)
- QStatusBar.showMessage(str, int msecs=0)
复制代码 可以看到,它可以传入两个参数,第一个参数就是str类型的参数,第二个默认参数可以不传,正好与上面信号的定义和发射相对应。3、关于函数和变量的命名问题,python本身是有自己的规范的(请搜索PEP 8),在前面教程的代码中,我都将原作者所使用的小驼峰写法(即首单词字母小写,后续单词首字母大写,单词之间不用_隔开,如myPrincess)改成PEP 8的写法(如my_princess),所以在最后的章节中,我依然使用这样的写法(说老实话我也纠结,但为了一致我还是决定这样做),鱼油们在写自己的程序的时候请自行取舍 然后我们看原教程中对于这整个游戏代码的解释
|