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

图3‑6 重构后的文件结构
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存放一些全局通用的函数,如screenToMap和mapToScreen等等。新增加了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 |
程序的运行效果与之前是一样的,但是代码结构更加清晰,便于修改了。