Python ·

基于Pygame开发贪吃蛇和俄罗斯方块(三)

Pygame 实现俄罗斯方块

一、实验介绍

1.1 实验内容

本节实验我们将使用Pygame实现经典的俄罗斯方块,实验中将讲解俄罗斯方块中的基本模块、术语以及原理,代码量较大,希望同学们认真理解。最终效果图如下:

tetris2

1.2 实验知识点

  • 实现俄罗斯方块

1.3 实验环境

  • Python 2.7.6
  • Xfce终端

1.4 适合人群

本课程难度为一般,属于初级级别课程,适合具有Python基础的用户,熟悉Python基础知识加深巩固。

1.5 代码获取

本节实验所用到的代码和相关资源文件可以通过下面命令下载到实验楼环境中,作为参照对比进行学习。

$ wget http://labfile.oss.aliyuncs.com/courses/940/tetris.zip

解压缩至/home/shiyanlou/tetris

$ unzip tetris.zip

二、开发准备

本次课程主要利用Pygame模块来进行开发,首先我们需要打开Xfce终端,并使用pip命令来安装Pygame

$ sudo pip install pygame

安装完成之后进入Python的交互界面,输入以下命令查看是否成功安装。

import pygame

若无异常,则说明安装成功。

三、实验步骤

3.1 基本术语

Board:由10乘20个Box组成,Piece就落在这里面。

Box:组成Piece的其中小方块,是组成Piece的基本单元。

Piece:从Board顶掉下的东西,游戏者可以翻转和改变位置。每个Piece由4个Box组成。

Shape:不同类型的Piece,这里Shape的名字被叫做T, S, Z, J, L, I, O。如下图所示:

shape

Template:用一个列表存放Shape被翻转后的所有可能样式。变量名字如S_SHAPE_TEMPLATE。

3.2 初始化

BOXSIZE,BOARDWIDTH,BOARDHEIGHT与前面贪吃蛇相关初始化类似,使其与屏幕像素点联系起来。

MOVESIDEWAYSFREQ和MOVEDOWNFREQ两个变量设置相关频率。

XMARGIN和TOPMARGIN分别指Board距窗口两边的距离和上边的距离。

COLORS指组成Piece的小Box的颜色,而LIGHTCOLORS是围绕在小Box周围的颜色,是为了强调出轮廓而设计的。

游戏必须知道每个类型的Piece有多少种Shape,在这里我们用在列表中嵌入含有字符串的列表来构成这个Template,一个Piece类型的Template含有了这个Piece可能变换的所有Shape。

TEMPLATEWIDTH和TEMPLATEHEIGHT则表示组成Template的行和列。

字典PIECES里面储存了所有不同的Template。

# -*- coding: UTF-8 -*-
# tetris.py

import random, time, pygame, sys
from pygame.locals import *


#设置屏幕刷新率
FPS = 25
# 设置窗口宽度
WINDOWWIDTH = 640
# 设置窗口高度
WINDOWHEIGHT = 480
# 方格大小
BOXSIZE = 20
# 放置俄罗斯方块窗口的大小
BOARDWIDTH = 10 
BOARDHEIGHT = 20
# 代表空的形状
BLANK = '.'
# 若一直按下方向左键或右键那么每0.15秒方块才会继续移动
MOVESIDEWAYSFREQ = 0.15
# 向下的频率
MOVEDOWNFREQ = 0.1
# x方向的边距
XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2)
# 距离窗口顶部的边距
TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5

# 定义颜色
WHITE       = (255, 255, 255)
GRAY        = (185, 185, 185)
BLACK       = (  0,   0,   0)
RED         = (155,   0,   0)
LIGHTRED    = (175,  20,  20)
GREEN       = (  0, 155,   0)
LIGHTGREEN  = ( 20, 175,  20)
BLUE        = (  0,   0, 155)
LIGHTBLUE   = ( 20,  20, 175)
YELLOW      = (155, 155,   0)
LIGHTYELLOW = (175, 175,  20)
BORDERCOLOR = BLUE
BGCOLOR = BLACK
TEXTCOLOR = WHITE
TEXTSHADOWCOLOR = GRAY

COLORS      = (     BLUE,      GREEN,      RED,      YELLOW)
LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW)

# 断言 每一个颜色都应该对应有亮色
assert len(COLORS) == len(LIGHTCOLORS)

# 模板的宽高
TEMPLATEWIDTH = 5
TEMPLATEHEIGHT = 5

# 形状_S(S旋转有2种)
S_SHAPE_TEMPLATE = [['.....',
                     '.....',
                     '..OO.',
                     '.OO..',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..OO.',
                     '...O.',
                     '.....']]

# 形状_Z(Z旋转有2种)
Z_SHAPE_TEMPLATE = [['.....',
                     '.....',
                     '.OO..',
                     '..OO.',
                     '.....'],
                    ['.....',
                     '..O..',
                     '.OO..',
                     '.O...',
                     '.....']]

# 形状_I(I旋转有2种)
I_SHAPE_TEMPLATE = [['..O..',
                     '..O..',
                     '..O..',
                     '..O..',
                     '.....'],
                    ['.....',
                     '.....',
                     'OOOO.',
                     '.....',
                     '.....']]

# 形状_O(O旋转只有一个)
O_SHAPE_TEMPLATE = [['.....',
                     '.....',
                     '.OO..',
                     '.OO..',
                     '.....']]

# 形状_J(J旋转有4种)
J_SHAPE_TEMPLATE = [['.....',
                     '.O...',
                     '.OOO.',
                     '.....',
                     '.....'],
                    ['.....',
                     '..OO.',
                     '..O..',
                     '..O..',
                     '.....'],
                    ['.....',
                     '.....',
                     '.OOO.',
                     '...O.',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..O..',
                     '.OO..',
                     '.....']]

# 形状_L(L旋转有4种)
L_SHAPE_TEMPLATE = [['.....',
                     '...O.',
                     '.OOO.',
                     '.....',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..O..',
                     '..OO.',
                     '.....'],
                    ['.....',
                     '.....',
                     '.OOO.',
                     '.O...',
                     '.....'],
                    ['.....',
                     '.OO..',
                     '..O..',
                     '..O..',
                     '.....']]

# 形状_T(T旋转有4种)
T_SHAPE_TEMPLATE = [['.....',
                     '..O..',
                     '.OOO.',
                     '.....',
                     '.....'],
                    ['.....',
                     '..O..',
                     '..OO.',
                     '..O..',
                     '.....'],
                    ['.....',
                     '.....',
                     '.OOO.',
                     '..O..',
                     '.....'],
                    ['.....',
                     '..O..',
                     '.OO..',
                     '..O..',
                     '.....']]

# 定义一个数据结构存储对应的形状
PIECES = {'S': S_SHAPE_TEMPLATE,
          'Z': Z_SHAPE_TEMPLATE,
          'J': J_SHAPE_TEMPLATE,
          'L': L_SHAPE_TEMPLATE,
          'I': I_SHAPE_TEMPLATE,
          'O': O_SHAPE_TEMPLATE,
          'T': T_SHAPE_TEMPLATE}

3.3 main()方法

主函数的前部分主要创建一些全局变量和在游戏开始之前显示一个开始画面。之后是游戏主循环,循环中首先简单的随机决定采用哪个背景音乐,然后调用runGame()运行游戏,当游戏失败,runGame()就会返回到main()函数,这时会停止背景音乐并且显示游戏失败的画面。当游戏者按下任一个键,显示游戏失败的showTextScreen()函数就会返回到main()函数,游戏循环会再次开始然后继续下一次游戏。

def main():
    # 定义全局变量
    global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT
    # 初始化pygame
    pygame.init()
    # 获得pygame时钟
    FPSCLOCK = pygame.time.Clock()
    # 设置窗口
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
    # 设置基础的字体
    BASICFONT = pygame.font.Font('resources/ARBERKLEY.ttf', 18)
    # 设置大字体
    BIGFONT = pygame.font.Font('resources/ARBERKLEY.ttf', 100)
    # 窗口标题
    pygame.display.set_caption('Tetris')
    # 显示开始画面
    showTextScreen('Tetris')

    # 游戏主循环
    while True:

#因实验楼中暂时无法播放音频,故将此段代码注释,同学们可以在自己的电脑上尝试播放
        # 二选一随机播放背景音乐
#        if random.randint(0, 1) == 0:
#            pygame.mixer.music.load('resources/tetrisb.mid')
#        else:
#            pygame.mixer.music.load('resources/tetrisc.mid')
#        pygame.mixer.music.play(-1, 0.0)

        # 运行游戏
        runGame()
        # 退出游戏后,结束播放音乐
        pygame.mixer.music.stop()
        # 显示结束画面
        showTextScreen('Game Over')

3.4 基本模块

在开始编写runGame()方法之前,我们不妨先来定义一些基本模块,这些模块在之后都将被用到。

我们可以定义一个退出方法,以便需要的时候退出游戏:

# 退出
def terminate():
    pygame.quit()
    sys.exit()

此外,玩家可能会按键退出游戏,当玩家按下Esc键时,我们即调用刚刚定义的退出方法来退出游戏,相关监测方法如下:

# 检查是否有退出事件
def checkForQuit():

    # 获得所有QUIT事件
    for event in pygame.event.get(QUIT):
        # 若存在任何QUIT事件,则终止
        terminate()
    # 获得所有KEYUP事件
    for event in pygame.event.get(KEYUP):
        if event.key == K_ESCAPE:
            # 若KEYUP事件是Esc键,则终止
            terminate()
        # 把其他的KEYUP事件对象放回来
        pygame.event.post(event)

与此同时,我们还需要定义一个用于监测是否有按键被按下的方法,当处于开始、结束、暂停画面时,可以调用该方法,以便程序退出当前画面。

# 检查是否有按键被按下
def checkForKeyPress():
    # 通过事件队列寻找KEYUP事件
    # 从事件队列删除KEYDOWN事件
    checkForQuit()

    for event in pygame.event.get([KEYDOWN, KEYUP]):
        if event.type == KEYDOWN:
            continue
        return event.key
    return None

考虑到我们需要在屏幕上显示文字,不妨直接创建相关方法,以便程序复用,如下,此程序接收三个参数:要显示的文字、要显示文字的字体、要显示文字的颜色,它会返回相应的文本对象,以方便使用。

# 创建文本绘制对象
def makeTextObjs(text, font, color):
    surf = font.render(text, True, color)
    return surf, surf.get_rect()

游戏开始画面、游戏结束画面以及游戏暂停画面可用一个方法来实现,我们向该方法中传递相关的文本参数以显示相关的画面,同时我们在循环中调用之前的checkForKeyPress()方法,以监测是否有按键被按下,从而实现是否从当前画面中退出:

# 显示开始、暂停、结束画面
def showTextScreen(text):
    # 这个函数用于在屏幕中央显示大文本,直到按下任意键
    # 绘制文字阴影
    titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTSHADOWCOLOR)
    titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
    DISPLAYSURF.blit(titleSurf, titleRect)

    # 绘制文字
    titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR)
    titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)
    DISPLAYSURF.blit(titleSurf, titleRect)

    # 绘制额外的"Press a key to play."文字
    pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.', BASICFONT, TEXTCOLOR)
    pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)
    DISPLAYSURF.blit(pressKeySurf, pressKeyRect)

    while checkForKeyPress() == None:
        pygame.display.update()
        FPSCLOCK.tick()

在游戏开始之前,我们需要一个空的Board框,我们用一个二维列表表示Board框,列表中的每一项都为BLANK:

# 清空Board
def getBlankBoard():
    board = []
    for i in range(BOARDWIDTH):
        board.append([BLANK] * BOARDHEIGHT)
    return board

游戏运行时,每次都会有一个新的Piece从顶部落下,我们用一个字典来表示Piece,字典中的键包括Shape、相应Shape的方向、起始位置、以及颜色,获得新Piece的方法如下:

# 随机获得一个新的Piece(形状,方向,颜色)
def getNewPiece():
    shape = random.choice(list(PIECES.keys()))
    newPiece = {'shape': shape,
                'rotation': random.randint(0, len(PIECES[shape]) - 1),
                'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2), # x居中
                'y': -2, # y在屏幕的上方,小于0
                'color': random.randint(0, len(COLORS)-1)}
    return newPiece

获取新Piece之后,就要将它添加到Board框中,添加的原理就是将Board框中对应坐标的Box绘制成相应Piece的颜色:

# 将一个Piece添加到Board中
def addToBoard(board, piece):
    for x in range(TEMPLATEWIDTH):
        for y in range(TEMPLATEHEIGHT):
            if PIECES[piece['shape']][piece['rotation']][y][x] != BLANK:
                board[x + piece['x']][y + piece['y']] = piece['color']

当Piece着陆之后在Board外时,游戏结束,因此我们需要一个判断Board边界的方法:

# Board边界
def isOnBoard(x, y):
    return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT

刚刚那种判断游戏结束的方法,我们可以将其抽象出来,看成是检查Piece的当前位置是否合法,因为不光是在判断游戏结束时调用它,在调整Piece形状时也需要调用,判断调整形状后的Piece在当前位置中是否容得下。

# Piece在当前的Board里是否是一个合法可用的位置
def isValidPosition(board, piece, adjX=0, adjY=0):
    # 若Piece在Board内并且无碰撞,则返回True
    for x in range(TEMPLATEWIDTH):
        for y in range(TEMPLATEHEIGHT):
            isAboveBoard = y + piece['y'] + adjY < 0
            if isAboveBoard or PIECES[piece['shape']][piece['rotation']][y][x] == BLANK:
                continue
            if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY):
                return False
            if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK:
                return False
    return True

此外,当游戏中某一行被填满时,我们将移除这一行,因此我们需要一个判断某行是否填满的方法,判断的原理就是看相应的这一行列表中的项是否为BLANK:

# 判断当前的这行是否被全部填满
def isCompleteLine(board, y):
    for x in range(BOARDWIDTH):
        if board[x][y] == BLANK:
            return False
    return True

还需要一个移除某行的方法,也就是将这一行上面的每一行都下降一行,同时还应该返回完成填满的总行数,这个值将作为玩家的分值,也就是说每成功移除一行,玩家分数加1:

# 检查每一行,移除完成填满的一行,将这一行上面的所有的都下降一行,返回完成填满的总行数
def removeCompleteLines(board):
    numLinesRemoved = 0
    # 从-1开始从下往上检查每一行
    y = BOARDHEIGHT - 1 
    while y >= 0:
        if isCompleteLine(board, y):
            for pullDownY in range(y, 0, -1):
                for x in range(BOARDWIDTH):
                    board[x][pullDownY] = board[x][pullDownY-1]
            for x in range(BOARDWIDTH):
                board[x][0] = BLANK
            numLinesRemoved += 1
        else:
            y -= 1
    return numLinesRemoved

随着游戏分数的越来越大,相应的等级也应越来越大,等级是当前消除行数除以10,我们假设刚开始时等级为1,随着等级的不断增大,下落频率应相应的减小,如下所示:

# 根据分数来计算等级和下落的频率
def calculateLevelAndFallFreq(score):

    level = int(score / 10) + 1
    fallFreq = 0.27 - (level * 0.02)
    return level, fallFreq

3.5 runGame()方法

在游戏开始和Piece掉落之前,我们需要初始化一些跟游戏开始相关的变量。fallingPiece变量被赋值成当前掉落的变量,nextPiece变量被赋值成游戏者可以在屏幕NEXT区域看见的下一个Piece。

游戏主循环中,fallingPiece变量在Piece着陆后被设置成None。这意味着nextPiece变量中的下一个Piece应该被赋值给fallingPiece变量,然后一个随机的Piece又会被赋值给nextPiece变量。lastFallTime变量也被赋值成当前时间,这样我们就可以通过fallFreq变量控制Piece下落的频率。

事件循环主要处理当翻转方块、移动方块时或者暂停游戏时的一些事情。若游戏暂停,我们应该隐藏掉游戏界面以防止游戏者作弊(否则游戏者会看着画面思考怎么处理方块),用DISPLAYSURF.fill(BGCOLOR)就可以实现这个效果。停止按下方向键或ASD键会把movingLeft,movingRight,movingDown变量设置为False,表明游戏者不再想要在此方向上移动方块。当左方向键按下(而且往左移动是有效的,通过调用isVaildPosition()函数知道的),那么我们应该改变一个方块的位置使其向左移动一格。

如果方向键上或W键被按下,那么就会翻转方块,就是将储存在fallingPiece字典中的‘rotation’键的键值加1,然而,当增加的'rotation'键值大于所有当前类型方块的形状的数目的话(此变量储存在len(SHAPES[fallingPiece['shape']])变量中),那么它翻转到最初的形状。如果翻转后的形状因为其中的一些小方块已经超过边框的范围而无效,那么我们就要把它变回原来的形状通过将fallingPiece['rotation'])减去1,同理,按Q键执行反向翻转时则是加1。

当游戏者按下空格键,方块将会迅速的下落至着陆。程序首先需要找出到它着陆需要下降个多少个格子,其中有关moving的三个变量都要被设置为False(保证程序后面部分的代码知道游戏者已经停止了按下所有的方向键)。

方块自然下落的速率由lastFallTime变量决定。如果自从上个Piece掉落了一个格子后过去了足够的时间,那么我们就会再让Piece移动一个格子。

# 运行游戏
def runGame():

    # 在游戏开始前初始化变量
    # 获得一个空的board
    board = getBlankBoard()

    # 最后向下移动的时刻
    lastMoveDownTime = time.time()

    # 最后侧向移动的时刻
    lastMoveSidewaysTime = time.time()

    # 最后的下降时间
    lastFallTime = time.time()

    # 是否可以  向下,向左,向右
    # 注意:这里没有向上可用
    movingDown = False 
    movingLeft = False
    movingRight = False

    # 分数
    score = 0

    # 根据分数计算等级和下降的频率
    level, fallFreq = calculateLevelAndFallFreq(score)

    # 获得新的形状(当前的形状)
    fallingPiece = getNewPiece()

    # 获得下一个形状
    nextPiece = getNewPiece()

    # 游戏循环体
    while True:

        # 当前没有下降的形状
        if fallingPiece == None:
            # 重新获得新的形状和下一个形状
            fallingPiece = nextPiece
            nextPiece = getNewPiece()

            # 重置最后下降的时间
            lastFallTime = time.time()

            # 判断界面上是否还有空位(方块是否到顶),没有则结束游戏
            if not isValidPosition(board, fallingPiece):
                return

        # 检查是否有退出事件
        checkForQuit()

        # 事件处理循环
        for event in pygame.event.get():
            # KEYUP事件处理
            if event.type == KEYUP:
                # 用户按P键暂停
                if (event.key == K_p):
                    DISPLAYSURF.fill(BGCOLOR)
                    #停止音乐
                    pygame.mixer.music.stop()

                    # 显示暂停界面,直到按任意键继续
                    showTextScreen('Paused')

#因实验楼中暂时无法播放音频,故将此段代码注释,同学们可以在自己的电脑上尝试播放
                    # 继续循环音乐
#                    pygame.mixer.music.play(-1, 0.0)

                    # 重置各种时间
                    lastFallTime = time.time()
                    lastMoveDownTime = time.time()
                    lastMoveSidewaysTime = time.time()

                elif (event.key == K_LEFT or event.key == K_a):
                    movingLeft = False
                elif (event.key == K_RIGHT or event.key == K_d):
                    movingRight = False
                elif (event.key == K_DOWN or event.key == K_s):
                    movingDown = False

            # KEYDOWN事件处理
            elif event.type == KEYDOWN:

                # 左右移动piece
                if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1):
                    fallingPiece['x'] -= 1
                    movingLeft = True
                    movingRight = False
                    lastMoveSidewaysTime = time.time()

                elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1):
                    fallingPiece['x'] += 1
                    movingRight = True
                    movingLeft = False
                    lastMoveSidewaysTime = time.time()

                # UP或W键 旋转piece (在有空间旋转的前提下)
                # 正向旋转
                elif (event.key == K_UP or event.key == K_w):
                    fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])
                    if not isValidPosition(board, fallingPiece):
                        fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
                # Q键,反向旋转
                elif (event.key == K_q):
                    fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])
                    if not isValidPosition(board, fallingPiece):
                        fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])

                # DOWN或S键 使piece下降得更快
                elif (event.key == K_DOWN or event.key == K_s):
                    movingDown = True
                    if isValidPosition(board, fallingPiece, adjY=1):
                        fallingPiece['y'] += 1
                    lastMoveDownTime = time.time()

                # 空格键,直接下降到最下面且可用的地方
                elif event.key == K_SPACE:
                    movingDown = False
                    movingLeft = False
                    movingRight = False
                    for i in range(1, BOARDHEIGHT):
                        if not isValidPosition(board, fallingPiece, adjY=i):
                            break
                    fallingPiece['y'] += i - 1

        # 根据记录的用户输入方向的变量来移动piece
        if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ:
            if movingLeft and isValidPosition(board, fallingPiece, adjX=-1):
                fallingPiece['x'] -= 1
            elif movingRight and isValidPosition(board, fallingPiece, adjX=1):
                fallingPiece['x'] += 1
            lastMoveSidewaysTime = time.time()

        if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1):
            fallingPiece['y'] += 1
            lastMoveDownTime = time.time()

        # 自动下降piece
        if time.time() - lastFallTime > fallFreq:
            if not isValidPosition(board, fallingPiece, adjY=1):
                addToBoard(board, fallingPiece)
                score += removeCompleteLines(board)
                level, fallFreq = calculateLevelAndFallFreq(score)
                fallingPiece = None
            else:
                fallingPiece['y'] += 1
                lastFallTime = time.time()

        # 绘制屏幕上的所有东西
        DISPLAYSURF.fill(BGCOLOR)
        drawBoard(board)
        drawStatus(score, level)
        drawNextPiece(nextPiece)
        if fallingPiece != None:
            drawPiece(fallingPiece)

        pygame.display.update()
        FPSCLOCK.tick(FPS)

3.6 绘制屏幕

之前的编程,为了简化操作,我们使用的是Board的坐标,在绘制图形之前,我们需要把Box的坐标转换为相应的像素坐标:

# 根据Board的坐标转化成像素坐标
def convertToPixelCoords(boxx, boxy):
    return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE))

绘制Box,:

# 绘制Box
def drawBox(boxx, boxy, color, pixelx=None, pixely=None):
    # 使用Board的坐标绘制单个Box(一个Piece含有4个Box),若像素坐标pixelx、pixely被指定,则直接使用像素坐标(用于NextPiece区域)
    if color == BLANK:
        return
    if pixelx == None and pixely == None:
        pixelx, pixely = convertToPixelCoords(boxx, boxy)
    pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1))
    pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4))

绘制Board主要包括绘制Board边框、绘制Board背景以及绘制Board中的Box:

# 绘制Board
def drawBoard(board):
    # 绘制Board边框
    pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5)
    # 绘制Board背景
    pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT))
    # 绘制Board中的Box
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            drawBox(x, y, board[x][y])

绘制分数、等级等状态信息:

# 绘制游戏分数、等级信息
def drawStatus(score, level):
    # 绘制分数文本
    scoreSurf = BASICFONT.render('Score: %s' % score, True, TEXTCOLOR)
    scoreRect = scoreSurf.get_rect()
    scoreRect.topleft = (WINDOWWIDTH - 150, 20)
    DISPLAYSURF.blit(scoreSurf, scoreRect)

    # 绘制等级文本
    levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR)
    levelRect = levelSurf.get_rect()
    levelRect.topleft = (WINDOWWIDTH - 150, 50)
    DISPLAYSURF.blit(levelSurf, levelRect)

绘制Piece:

# 绘制各种形状Piece(S,Z,I,O,J,L,T)
def drawPiece(piece, pixelx=None, pixely=None):
    shapeToDraw = PIECES[piece['shape']][piece['rotation']]
    if pixelx == None and pixely == None:
        # 若pixelx、pixely没有被指定,则使用piece数据结构中存储的位置
        pixelx, pixely = convertToPixelCoords(piece['x'], piece['y'])

    # 绘制组成Piece的每个Box
    for x in range(TEMPLATEWIDTH):
        for y in range(TEMPLATEHEIGHT):
            if shapeToDraw[y][x] != BLANK:
                drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE))

绘制NextPiece区域显示的内容:

# 绘制提示信息,下一个Piece
def drawNextPiece(piece):
    # 绘制"Next"文本
    nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR)
    nextRect = nextSurf.get_rect()
    nextRect.topleft = (WINDOWWIDTH - 120, 80)
    DISPLAYSURF.blit(nextSurf, nextRect)
    # 绘制NextPiece
    drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100)

3.7 尝试运行代码

if __name__ == "__main__":
    main()

执行 python tetris.py

四、实验总结

本节课程我们讲解了俄罗斯方块的实现原理,同学们可以基于已完成的基础版本增添更多功能,例如下方的搞笑俄罗斯链接,另外,如果你想更深入的了解Pygame的细节或相关游戏制作,可以参考下方的书籍。

五、参考链接

搞笑俄罗斯

《Making Games with Python & Pygame》

参与评论