3.4    重构代码

现在程序代码已经达到了两百行,各种功能都混在一个文件里,不太便于编辑和展示,因此,最好进行一次重构。重构后的文件结构如3‑6所示。

36 重构后的文件结构

l  Constants.py文件存放一些常量,如颜色值和屏幕宽度高度等等。变量名称改成大写,并添加COLOR_SCREEN_等前缀,这样看起来更清晰。代码如下所示:

03\05\constants.py

# 颜色

COLOR_WHITE = (255, 255, 255)  # 白色

COLOR_BLACK = (0, 0, 0)  # 黑色

COLOR_SKY_BLUE = (135, 206, 255)  # 天空

COLOR_GROUND = (205, 179, 139)  # 地面

COLOR_BRICK = (178, 34, 34)  # 砖块

COLOR_PIPE_GREEN = (0, 238, 0)  # 管道

 

# 地图相关

MAP_BLOCK_SIZE = 20

 

# 窗口

SCREEN_WIDTH = 400

SCREEN_HEIGHT = 280

l  Utils.py存放一些全局通用的函数,如screenToMapmapToScreen等等。新增加了loadMapData()函数,用来载入地图文件,地图数据单独放到一个txt文件中便于修改。代码如下所示:

03\05\utils.py

from constants import *

from Globals import *

 

 

# 地图位置转为屏幕坐标

def mapArrayToScreen(mapArrayX, mapArrayY):

    screenX = mapArrayX * MAP_BLOCK_SIZE - Globals.game.mapViewFrom

    screenY = mapArrayY * MAP_BLOCK_SIZE

    return screenX, screenY

 

 

# 屏幕坐标转为地图坐标

def screenToMap(screenRect):

    return screenRect.move(Globals.game.mapViewFrom, 0)

 

 

#地图坐标转为屏幕坐标

def mapToScreen(mapRect):

    return mapRect.move(-1 * Globals.game.mapViewFrom, 0)

 

 

# 载入地图(地图文件使用Tab键分隔)

def loadMapData(fileName):

    mapFile = open("map/"+fileName, 'r')

    mapArray = []

    for line in mapFile:

        rowArray = [int(elt.strip()) for elt in line.strip().split('\t')]

        mapArray.append(rowArray)

    mapFile.close()

    return mapArray

l  Game.py就是原来的主文件,改动比较大,代码如下所示:

03\05\Game.py

import pygame

from constants import *

from utils import *

from Globals import *

from Mario import Mario

 

 

class Game:

    def __init__(self):

        # 初始化 Pygame

        pygame.init()

 

        # 设置窗口大小

        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

 

        # 设置窗口标题

        pygame.display.set_caption('super mario bros')

 

        # 避免输入法影响按键

        pygame.key.stop_text_input()

 

        # 创建时钟对象

        self.clock = pygame.time.Clock()

 

        # 马里奥

        self.mario = Mario(10, 200, self.screen)

 

        # 地图数据

        self.mapArray = []

        self.mapViewFrom = 0

        self.mapViewTo = SCREEN_WIDTH

        self.mapViewFromMax = 0

 

        # 存储参与碰撞的物体信息

        self.objectArray = []

 

        # 初始化地图

        self.initMap()

 

    # 初始化地图

    def initMap(self):

 

        # 地图数据

        self.mapArray = loadMapData("1-1.txt")

 

        # 滚动地图From的最大值

        self.mapViewFromMax = len(self.mapArray[0]) * MAP_BLOCK_SIZE - SCREEN_WIDTH

 

        # 存储参与碰撞的物体信息(地图坐标)

        for rowIndex in range(len(self.mapArray)):

            row = self.mapArray[rowIndex]

            for cellIndex in range(len(row)):

                cell = row[cellIndex]

                posX = cellIndex * MAP_BLOCK_SIZE

                posY = rowIndex * MAP_BLOCK_SIZE

                if cell == 1:

                    # 天空(不碰撞)

                    pass

                elif 2 <= cell <= 4:

                    # 地面,砖块,水管

                    self.objectArray.append(pygame.Rect(posX, posY, MAP_BLOCK_SIZE, MAP_BLOCK_SIZE))

 

    # 滚动地图

    def scrollMap(self, step):

        self.mapViewFrom += step

        if self.mapViewFrom < 0:

            self.mapViewFrom = 0

        if self.mapViewFrom > self.mapViewFromMax:

            self.mapViewFrom = self.mapViewFromMax

        self.mapViewTo = self.mapViewFrom + SCREEN_WIDTH

 

    def drawMap(self):

        # 画地图

        for rowIndex in range(len(self.mapArray)):

            row = self.mapArray[rowIndex]

            for cellIndex in range(len(row)):

                # 转换为屏幕坐标,在显示范围内才绘制,注意参数的顺序

                screenPos = mapArrayToScreen(cellIndex, rowIndex)

                if screenPos[0] + MAP_BLOCK_SIZE > 0 and screenPos[0] < SCREEN_WIDTH:

                    cell = row[cellIndex]

                    if cell == 1:

                        # 天空

                        pygame.draw.rect(self.screen, COLOR_SKY_BLUE, (screenPos, (MAP_BLOCK_SIZE, MAP_BLOCK_SIZE)),

                                         0)

                    elif cell == 2:

                        # 地面

                        pygame.draw.rect(self.screen, COLOR_GROUND, (screenPos, (MAP_BLOCK_SIZE, MAP_BLOCK_SIZE)),

                                         0)

                    elif cell == 3:

                        # 砖块

                        pygame.draw.rect(self.screen, COLOR_BRICK, (screenPos, (MAP_BLOCK_SIZE, MAP_BLOCK_SIZE)), 0)

                    elif cell == 4:

                        # 水管

                        pygame.draw.rect(self.screen, COLOR_PIPE_GREEN,

                                         (screenPos, (MAP_BLOCK_SIZE, MAP_BLOCK_SIZE)), 0)

 

    def run(self):

 

        # 主循环

        isRunning = True

        while isRunning:

            for event in pygame.event.get():

                if event.type == pygame.QUIT:

                    isRunning = False

 

            # 全屏擦除

            self.screen.fill((0, 0, 0))

 

            # 绘制地图

            self.drawMap()

 

            # 获取按键状态

            keys = pygame.key.get_pressed()

 

            # 更新马里奥

            self.mario.update(keys)

 

            # 刷新显示

            pygame.display.flip()

 

            # 每秒60

            self.clock.tick(60)

        # 退出 Pygame

        pygame.quit()

 

 

if __name__ == '__main__':

    game = Game()

    Globals.game = game  # 保存起来,便于使用

    game.run()

         我们将主要逻辑写在了Game类中,全局变量如mapViewFrom等改为了内部变量。为了让外部类能引用到这些内部变量,将Game实例放到了Globals.py中,通过Globals.game.mapViewFrom这种形式来引用。地图数据放到单独的txt文件中便于修改,马里奥的操作代码放到Mario.py中,主循环的代码看起来更加清晰了。

l  Mario.py存放马里奥相关的代码,如下所示:

03\05\Mario.py

import pygame

from pygame import Rect

 

from Globals import *

from constants import *

from utils import *

 

 

class Mario(object):

 

    # 构造方法,记录初始坐标

    def __init__(self, x, y, screen):

        self.screen = screen

        self.rect = Rect(x, y, MAP_BLOCK_SIZE, MAP_BLOCK_SIZE)

        self.walkStepDefault = 2  # 走路步伐,默认值

        self.jumpStepDefault = 10  # 跳跃步伐,默认值

        self.dropStepDefault = 4  # 掉落步伐,默认值

        self.walkStep = 2  # 走路步伐,实际值

        self.jumpStep = 10  # 跳跃步伐,实际值

        self.dropStep = 4  # 掉落步伐,实际值

 

    # 更新

    def update(self, keys):

 

        # 步伐,先恢复默认值

        self.walkStep = self.walkStepDefault

        self.jumpStep = self.jumpStepDefault

        self.dropStep = self.dropStepDefault

 

        # 按键状态

        if keys[pygame.K_LEFT]:

            print("按了左方向键")

 

            # 窗口边界判断

            if self.rect.x - self.walkStep <= 0:

                self.walkStep = self.rect.x

 

            # 下一步预判断

            nextStepRect = self.rect.move(-1 * self.walkStep, 0)

            pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray)  # 是否碰撞

            if pipeIndex != -1:

                self.walkStep = self.rect.x - mapToScreen(Globals.game.objectArray[pipeIndex]).right

            self.rect = self.rect.move(-1 * self.walkStep, 0)

        if keys[pygame.K_RIGHT]:

            print("按了右方向键")

 

            # 超过屏幕中线才滚动地图,地图末尾右半屏也不滚动

            if self.rect.x >= SCREEN_WIDTH / 2 and screenToMap(

                    self.rect).x < Globals.game.mapViewFromMax + SCREEN_WIDTH / 2:

                Globals.game.scrollMap(self.walkStep)

            else:

                # 窗口边界判断

                if self.rect.right + self.walkStep >= SCREEN_WIDTH:

                    self.walkStep = SCREEN_WIDTH - self.rect.right

 

                # 下一步预判断

                nextStepRect = self.rect.move(self.walkStep, 0)

                pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray)

                if pipeIndex != -1:

                    self.walkStep = mapToScreen(Globals.game.objectArray[pipeIndex]).x - self.rect.right

                self.rect = self.rect.move(self.walkStep, 0)

        if keys[pygame.K_g]:

            print("按了跳跃键")

 

            # 窗口边界判断

            if self.rect.y - self.jumpStep <= 0:

                self.jumpStep = self.rect.y

 

            # 下一步预判断

            nextStepRect = self.rect.move(0, -1 * self.jumpStep)

            pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray)

            if pipeIndex != -1:

                self.jumpStep = (mapToScreen(Globals.game.objectArray[pipeIndex]).bottom - self.rect.y) * -1

            self.rect = self.rect.move(0, -1 * self.jumpStep)

 

        # ----重力影响----

        # 下一步预判断

        nextStepRect = self.rect.move(0, self.dropStep)

        pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray)

        if pipeIndex != -1:

            self.dropStep = mapToScreen(Globals.game.objectArray[pipeIndex]).y - self.rect.bottom

        self.rect = self.rect.move(0, self.dropStep)

 

        # 显示马里奥

        pygame.draw.rect(self.screen, COLOR_BLACK, self.rect, 1)  # 边框

        pygame.draw.rect(self.screen, COLOR_WHITE, self.rect.inflate(-2, -2), 0)  # 边框内部白色填充

        pygame.draw.rect(self.screen, COLOR_BLACK, self.rect.inflate(-10, -10), 0)  # 中心黑色填充

 

         目前就是走路、跳跃和重力相关的代码,以后可以根据需要增加。

l  Globals.py存放一些全局的类对象,便于使用,代码如下所示:

03\05\Globals.py

class Globals:

    game = None

    mario = None

         程序的运行效果与之前是一样的,但是代码结构更加清晰,便于修改了。