|
PyQt4 中的俄罗斯方块
创建一个电脑游戏是很具挑战性的。迟早程序员想创建一个电脑游戏。事实上,很多人对编程感兴趣,是因为它们玩了游戏,然后想创建自己的游戏。创建一个游戏会提升你的编程技能。
俄罗斯方块
俄罗斯方块从创建开始就成为很流行的游戏之一。游戏最开始是由俄罗斯程序员 Alexey Pajitnov 设计和编写的。从那时开始几乎在所有的计算机平台运行。甚至我的手机就有一个修改版的俄罗斯方块。
这个游戏是落下方块。我们可以有七种不同的形状,称为 tetromino 。S形状,Z形状,T形状,L形状,行,镜像L形状,方块形状。而这些形状又由四个方块构成。这些形状会掉到板上。这个游戏的目标是移动和旋转这些形状,所以可以把他们填满。如果我们填满了一行,这行就被销毁然后我们得分。游戏在方块填到顶部就结束了。
PyQt4 是可以用于创建应用的。也有很多其它库可以用于游戏。尽管如此,PyQt4 仍可以创建游戏。
下面的例子是一个改版的俄罗斯方块,在 PyQt4 安装的目录下。
开发
我们不准备使用图片,而是使用 PyQt4 的 API 。在每个计算机游戏背后,都有一个数学模型。俄罗斯方块也是。
一些想法。
- 使用 QtCore.QBasicTimer 创建游戏的一轮
- 这些方块是往下掉的
- 这些形状一格格移动
- 游戏盘数学上是简单的列表
#!/usr/bin/python
# tetris.py
import sys
import random
from PyQt4 import QtCore, QtGui
class Tetris(QtGui.QMainWindow):
def __init__(self):
#QtGui.QMainWindow.__init__(self)
super(Tetris, self).__init__()
self.setGeometry(300, 300, 180, 380)
self.setWindowTitle('Tetris')
self.tetrisboard = Board(self)
self.setCentralWidget(self.tetrisboard)
self.statusbar = self.statusBar()
self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"),
self.statusbar, QtCore.SLOT("showMessage(QString)"))
self.tetrisboard.start()
self.center()
def center(self):
screen = QtGui.QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width()-size.width())/2,
(screen.height()-size.height())/2)
class Board(QtGui.QFrame):
BoardWidth = 10
BoardHeight = 22
Speed = 300
def __init__(self, parent):
#QtGui.QFrame.__init__(self, parent)
super(Board, self).__init__(parent)
self.timer = QtCore.QBasicTimer()
self.isWaitingAfterLine = False
self.curPiece = Shape()
self.nextPiece = Shape()
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.isStarted = False
self.isPaused = False
self.clearBoard()
self.nextPiece.setRandomShape()
def shapeAt(self, x, y):
return self.board[(y * Board.BoardWidth) + x]
def setShapeAt(self, x, y, shape):
self.board[(y * Board.BoardWidth) + x] = shape
def squareWidth(self):
return self.contentsRect().width() / Board.BoardWidth
def squareHeight(self):
return self.contentsRect().height() / Board.BoardHeight
def start(self):
if self.isPaused:
return
self.isStarted = True
self.isWaitingAfterLine = False
self.numLinesRemoved = 0
self.clearBoard()
self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
str(self.numLinesRemoved))
self.newPiece()
self.timer.start(Board.Speed, self)
def pause(self):
if not self.isStarted:
return
self.isPaused = not self.isPaused
if self.isPaused:
self.timer.stop()
self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "paused")
else:
self.timer.start(Board.Speed, self)
self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
str(self.numLinesRemoved))
self.update()
def paintEvent(self, event):
painter = QtGui.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 != Tetrominoes.NoShape:
self.drawSquare(painter,
rect.left() + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
if self.curPiece.shape() != Tetrominoes.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):
if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
QtGui.QWidget.keyPressEvent(self, event)
return
key = event.key()
if key == QtCore.Qt.Key_P:
self.pause()
return
if self.isPaused:
return
elif key == QtCore.Qt.Key_Left:
self.tryMove(self.curPiece, self.curX - 1, self.curY)
elif key == QtCore.Qt.Key_Right:
self.tryMove(self.curPiece, self.curX + 1, self.curY)
elif key == QtCore.Qt.Key_Down:
self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
elif key == QtCore.Qt.Key_Up:
self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
elif key == QtCore.Qt.Key_Space:
self.dropDown()
elif key == QtCore.Qt.Key_D:
self.oneLineDown()
else:
QtGui.QWidget.keyPressEvent(self, event)
def timerEvent(self, event):
if event.timerId() == self.timer.timerId():
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
QtGui.QFrame.timerEvent(self, event)
def clearBoard(self):
for i in range(Board.BoardHeight * Board.BoardWidth):
self.board.append(Tetrominoes.NoShape)
def dropDown(self):
newY = self.curY
while newY > 0:
if not self.tryMove(self.curPiece, self.curX, newY - 1):
break
newY -= 1
self.pieceDropped()
def oneLineDown(self):
if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
self.pieceDropped()
def pieceDropped(self):
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):
numFullLines = 0
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shapeAt(j, i) == Tetrominoes.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.emit(QtCore.SIGNAL("messageToStatusbar(QString)"),
str(self.numLinesRemoved))
self.isWaitingAfterLine = True
self.curPiece.setShape(Tetrominoes.NoShape)
self.update()
def newPiece(self):
self.curPiece = self.nextPiece
self.nextPiece.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(Tetrominoes.NoShape)
self.timer.stop()
self.isStarted = False
self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game over")
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) != Tetrominoes.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.update()
return True
def drawSquare(self, painter, x, y, shape):
colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
color = QtGui.QColor(colorTable[shape])
painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,
self.squareHeight() - 2, color)
painter.setPen(color.light())
painter.drawLine(x, y + self.squareHeight() - 1, x, y)
painter.drawLine(x, y, x + self.squareWidth() - 1, y)
painter.setPen(color.dark())
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 Tetrominoes(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 = Tetrominoes.NoShape
self.setShape(Tetrominoes.NoShape)
def shape(self):
return self.pieceShape
def setShape(self, 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):
self.setShape(random.randint(1, 7))
def x(self, index):
return self.coords[index][0]
def y(self, index):
return self.coords[index][1]
def setX(self, index, x):
self.coords[index][0] = x
def setY(self, index, y):
self.coords[index][1] = y
def minX(self):
m = self.coords[0][0]
for i in range(4):
m = min(m, self.coords[i][0])
return m
def maxX(self):
m = self.coords[0][0]
for i in range(4):
m = max(m, self.coords[i][0])
return m
def minY(self):
m = self.coords[0][1]
for i in range(4):
m = min(m, self.coords[i][1])
return m
def maxY(self):
m = self.coords[0][1]
for i in range(4):
m = max(m, self.coords[i][1])
return m
def rotatedLeft(self):
if self.pieceShape == Tetrominoes.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 rotatedRight(self):
if self.pieceShape == Tetrominoes.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
app = QtGui.QApplication(sys.argv)
tetris = Tetris()
tetris.show()
sys.exit(app.exec_())
我稍微简化了此游戏,所以理解起来比较容易。游戏在载入后就立即开始了。我们可以按 'p' 来暂停游戏。而空格可以让形状立即掉到底部。游戏以恒速进行,并没有实现加速。分数是我们移掉的行数。
self.statusbar = self.statusBar()
self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"),
self.statusbar, QtCore.SLOT("showMessage(QString)"))
我们创建了状态栏,用于显示消息。我们将显示三种可能的消息。移除的行数,暂停消息,结束消息。
...
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
...
在游戏开始前,我们先初始化一些重要的变量。变量 self.board 是一组 0 到 7 的列表。它代表这些形状的位置,以及剩余的形状。
for j in range(Board.BoardWidth):
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
if shape != Tetrominoes.NoShape:
self.drawSquare(painter,
rect.left() + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
游戏的绘制被分为两部分。第一步,我们绘制所有的形状,或剩余的形状,这些是已经掉落的。所有的都记在 self.board 中。我们使用 shapeAt() 方法访问它。
if self.curPiece.shape() != Tetrominoes.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 == QtCore.Qt.Key_Left:
self.tryMove(self.curPiece, self.curX - 1, self.curY)
elif key == QtCore.Qt.Key_Right:
self.tryMove(self.curPiece, self.curX + 1, self.curY)
在 keyPressEvent 中,我们检查按键。如果我们按向右的键,那么那块就会往右移。我们说 try,是因为可能不能移。
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) != Tetrominoes.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.update()
return True
在这个方法中,我们尝试移动形状。如果到了画板边缘或是碰到其它,我们就返回 False 。否则,我们就放置到新的位置。
def timerEvent(self, event):
if event.timerId() == self.timer.timerId():
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
QtGui.QFrame.timerEvent(self, event)
在计时器事件中,我们或者是创建一个新的方块,或是让方块下落。
def removeFullLines(self):
numFullLines = 0
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shageAt(j, i) == Tetrominoes.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))
...
如果那块碰到底部,我们就调用 removeFullLines() 方法。首先,我们找到所有的行数。然后我们移除他们。我们要把满了的行数移除。注意,我们把要移除的列表进行反转。否则,它可能不能正确移除。我们没有使用重力效应。这就意味着某些方块会悬浮。
def newPiece(self):
self.curPiece = self.nextPiece
self.nextPiece.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(Tetrominoes.NoShape)
self.timer.stop()
self.isStarted = False
self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game Over")
newPiece() 方法随机创建一个方块。如果这个方块不能动了,游戏就结束了。
Shape 类保存了每个方块的形状。
self.coords = [[0,0] for i in range(4)]
在创建时,我们创建了一个空的坐标列表。这个列表保存每个方块的坐标。比如,这些元组 (0,-1),(0,0),(-1,0),(-1,-1) 代表旋转的 S 形状。下面的图表指示了这个形状。
当我们绘制方块时,我们就把它画在 self.curX 和 self.curY 上。然后我们把四个方块画起来。
这是一个用 PyQt4 写的俄罗斯方块。
|