现在,窗口的高度是按14个瓦片的高度来定义的,这个没有问题。但是窗口宽度怎么办呢?难道地图有多宽,窗口就要有多宽吗?这显然不太现实。实际上,我们需要在固定宽度的窗口内实现地图的滚动效果。可以将这个过程想象成屏幕固定不动,而我们在屏幕下方手动移动地图,屏幕始终只显示地图的一部分。
屏幕坐标与地图坐标的转换关系,如图3‑4所示。我们只需要记录当前地图的显示起点,就可以将屏幕坐标快速转换为地图坐标,如图中屏幕坐标(80,60)对应的地图坐标是(680,60),只需要将X坐标加上mapViewFrom值即可。

图3‑4 屏幕坐标与地图坐标的关系
修改程序,尝试使用左右方向键操作地图滚动,代码如下所示:
03\02.py
|
import pygame ……(省略)……
# 方块大小 blockSize = 20
# 马里奥位置和大小 marioRect = pygame.Rect(10, 200, blockSize, blockSize)
# 地图数据 mapArray = [] mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) mapArray.append([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
# 滚动地图From和To mapViewFrom = 0 mapViewTo = mapViewFrom + WIDTH mapViewFromMax = len(mapArray[0]) * blockSize - WIDTH
# 地图位置转为屏幕坐标 def mapToScreen(mapArrayX, mapArrayY): screenX = mapArrayX * blockSize - mapViewFrom screenY = mapArrayY * blockSize - 0 return screenX, screenY
# 滚动地图 def scrollMap(step): global mapViewFrom, mapViewTo mapViewFrom += step if mapViewFrom < 0: mapViewFrom = 0 if mapViewFrom > mapViewFromMax: mapViewFrom = mapViewFromMax mapViewTo = mapViewFrom + WIDTH
# 主循环 isRunning = True while isRunning: for event in pygame.event.get(): if event.type == pygame.QUIT: isRunning = False
# 获取按键状态 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: print("按了左方向键") scrollMap(-5) if keys[pygame.K_RIGHT]: print("按了右方向键") scrollMap(5)
# 全屏擦除 screen.fill((0, 0, 0))
# 画地图 for rowIndex in range(len(mapArray)): row = mapArray[rowIndex] for cellIndex in range(len(row)): # 转换为屏幕坐标,在显示范围内才绘制,注意参数的顺序 screenPos = mapToScreen(cellIndex, rowIndex) if screenPos[0] + blockSize > 0 and screenPos[0] < WIDTH: cell = row[cellIndex] if cell == 1: # 天空 pygame.draw.rect(screen, skyblue, (screenPos, (blockSize, blockSize)), 0) elif cell == 2: # 地面 pygame.draw.rect(screen, ground, (screenPos, (blockSize, blockSize)), 0) elif cell == 3: # 砖块 pygame.draw.rect(screen, brick, (screenPos, (blockSize, blockSize)), 0) elif cell == 4: # 水管 pygame.draw.rect(screen, pipeGreen, (screenPos, (blockSize, blockSize)), 0)
# 显示马里奥 pygame.draw.rect(screen, black, marioRect, 1) # 边框 pygame.draw.rect(screen, white, marioRect.inflate(-2, -2), 0) # 边框内部白色填充 pygame.draw.rect(screen, black, marioRect.inflate(-10, -10), 0) # 中心黑色填充
# 刷新显示 pygame.display.flip()
# 每秒60帧 clock.tick(60) # 退出 Pygame pygame.quit() |
程序中增加了两个变量mapViewFrom和mapViewTo,分别表示地图的显示起点和终点。每帧绘制画面时,不再绘制整个地图,使用mapToScreen()方法将每个瓦片的地图坐标转换为屏幕坐标,如果在屏幕显示范围内就绘制,反之就跳过。
按下左右方向键后,对应修改mapViewFrom和mapViewTo变量的值,就可以将地图的不同部分投影到屏幕上,从而实现地图的滚动。运行程序,画面如图3‑5所示。

图3‑5 滚动地图初步实现
按下方向键后,只有背景地图在移动,马里奥始终在屏幕左下角,实际是没有移动的,但视觉上感觉马里奥在行走。为了简化演示代码,程序中没有进行碰撞检测,所以马里奥和水管重叠了。
载入地图保存碰撞物体信息时,Rect对象保存的是地图坐标。进行碰撞检测时,需要将地图坐标转换为屏幕坐标,如果该坐标在可视范围内才参与碰撞检测。考虑到碰撞检测使用的是以下语句:
nextStepRect.collidelist(objectArray)
其中objectArray是一个数组,如果转换坐标那么数组中的所有Rect对象都要转换一次,比较麻烦。简单起见,我们可以将马里奥的坐标转换为地图坐标再进行碰撞检测,同时恢复马里奥的移动,修改后的完整代码如下所示:
03\03.py
|
import pygame
# 初始化pygame pygame.init()
# 设置窗口大小 WIDTH = 400 HEIGHT = 280 screen = pygame.display.set_mode((WIDTH, HEIGHT))
# 设置窗口标题 pygame.display.set_caption('super mario bros')
# 避免输入法影响按键 pygame.key.stop_text_input()
# 时钟 clock = pygame.time.Clock()
# 颜色 white = 255, 255, 255 # 白色 black = 0, 0, 0 # 黑色 skyblue = 135, 206, 255 # 天空 ground = 205, 179, 139 # 地面 brick = 178, 34, 34 # 砖块 pipeGreen = 0, 238, 0 # 管道
# 方块大小 blockSize = 20
# 马里奥位置和大小 marioRect = pygame.Rect(10, 200, blockSize, blockSize)
# 地图数据 mapArray = [] mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) mapArray.append([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) mapArray.append([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
# 滚动地图From和To mapViewFrom = 0 mapViewTo = mapViewFrom + WIDTH mapViewFromMax = len(mapArray[0]) * blockSize - WIDTH
# 地图位置转为屏幕坐标 def mapArrayToScreen(mapArrayX, mapArrayY): screenX = mapArrayX * blockSize - mapViewFrom screenY = mapArrayY * blockSize - 0 return screenX, screenY
# 屏幕坐标转为地图坐标 def screenToMap(screenRect): return screenRect.move(mapViewFrom, 0)
# 屏幕坐标转为地图坐标 def mapToScreen(mapRect): return mapRect.move(-1 * mapViewFrom, 0)
# 滚动地图 def scrollMap(step): global mapViewFrom, mapViewTo mapViewFrom += step if mapViewFrom < 0: mapViewFrom = 0 if mapViewFrom > mapViewFromMax: mapViewFrom = mapViewFromMax mapViewTo = mapViewFrom + WIDTH
# 存储参与碰撞的物体信息 objectArray = [] for rowIndex in range(len(mapArray)): row = mapArray[rowIndex] for cellIndex in range(len(row)): cell = row[cellIndex] posX = cellIndex * blockSize posY = rowIndex * blockSize if cell == 1: # 天空(不碰撞) pass elif 2 <= cell <= 4: # 地面,砖块,水管 objectArray.append(pygame.Rect(posX, posY, blockSize, blockSize))
# 主循环 isRunning = True while isRunning: for event in pygame.event.get(): if event.type == pygame.QUIT: isRunning = False
# 马里奥的步伐 marioWalkStep = 18
# 获取按键状态 keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: print("按了左方向键") scrollMap(-5)
# 窗口边界判断 if marioRect.x - marioWalkStep <= 0: marioWalkStep = marioRect.x
# 下一步预判断 nextStepRect = marioRect.move(-1 * marioWalkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(objectArray) # 是否碰撞 if pipeIndex != -1: marioWalkStep = marioRect.x - mapToScreen(objectArray[pipeIndex]).right marioRect = marioRect.move(-1 * marioWalkStep, 0) if keys[pygame.K_RIGHT]: print("按了右方向键") scrollMap(5)
# 窗口边界判断 if marioRect.right + marioWalkStep >= WIDTH: marioWalkStep = WIDTH - marioRect.right
# 下一步预判断 nextStepRect = marioRect.move(marioWalkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(objectArray) if pipeIndex != -1: marioWalkStep = mapToScreen(objectArray[pipeIndex]).x - marioRect.right marioRect = marioRect.move(marioWalkStep, 0) if keys[pygame.K_g]: print("按了跳跃键")
# 跳跃步伐 jumpStep = 10
# 窗口边界判断 if marioRect.y - jumpStep <= 0: jumpStep = marioRect.y
# 下一步预判断 nextStepRect = marioRect.move(0, -1 * jumpStep) pipeIndex = screenToMap(nextStepRect).collidelist(objectArray) if pipeIndex != -1: jumpStep = (mapToScreen(objectArray[pipeIndex]).bottom - marioRect.y) * -1 marioRect = marioRect.move(0, -1 * jumpStep)
# 全屏擦除 screen.fill((0, 0, 0))
# 画地图 for rowIndex in range(len(mapArray)): row = mapArray[rowIndex] for cellIndex in range(len(row)): # 转换为屏幕坐标,在显示范围内才绘制,注意参数的顺序 screenPos = mapArrayToScreen(cellIndex, rowIndex) if screenPos[0] + blockSize > 0 and screenPos[0] < WIDTH: cell = row[cellIndex] if cell == 1: # 天空 pygame.draw.rect(screen, skyblue, (screenPos, (blockSize, blockSize)), 0) elif cell == 2: # 地面 pygame.draw.rect(screen, ground, (screenPos, (blockSize, blockSize)), 0) elif cell == 3: # 砖块 pygame.draw.rect(screen, brick, (screenPos, (blockSize, blockSize)), 0) elif cell == 4: # 水管 pygame.draw.rect(screen, pipeGreen, (screenPos, (blockSize, blockSize)), 0)
# 马里奥Y轴步伐 marioWalkStepY = 4
# 下一步预判断 nextStepRect = marioRect.move(0, marioWalkStepY) pipeIndex = screenToMap(nextStepRect).collidelist(objectArray) if pipeIndex != -1: marioWalkStepY = mapToScreen(objectArray[pipeIndex]).y - marioRect.bottom marioRect = marioRect.move(0, marioWalkStepY)
# 显示马里奥 pygame.draw.rect(screen, black, marioRect, 1) # 边框 pygame.draw.rect(screen, white, marioRect.inflate(-2, -2), 0) # 边框内部白色填充 pygame.draw.rect(screen, black, marioRect.inflate(-10, -10), 0) # 中心黑色填充
# 刷新显示 pygame.display.flip()
# 每秒60帧 clock.tick(60) # 退出 Pygame pygame.quit() |
运行程序,马里奥移动的同时,地图也是滚动的,马里奥也不会进入水管了。