3.2    滚动地图

现在,窗口的高度是按14个瓦片的高度来定义的,这个没有问题。但是窗口宽度怎么办呢?难道地图有多宽,窗口就要有多宽吗?这显然不太现实。实际上,我们需要在固定宽度的窗口内实现地图的滚动效果。可以将这个过程想象成屏幕固定不动,而我们在屏幕下方手动移动地图,屏幕始终只显示地图的一部分。

屏幕坐标与地图坐标的转换关系,如3‑4所示。我们只需要记录当前地图的显示起点,就可以将屏幕坐标快速转换为地图坐标,如图中屏幕坐标(80,60)对应的地图坐标是(680,60),只需要将X坐标加上mapViewFrom值即可。

34 屏幕坐标与地图坐标的关系

修改程序,尝试使用左右方向键操作地图滚动,代码如下所示:

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])

 

# 滚动地图FromTo

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()

程序中增加了两个变量mapViewFrommapViewTo,分别表示地图的显示起点和终点。每帧绘制画面时,不再绘制整个地图,使用mapToScreen()方法将每个瓦片的地图坐标转换为屏幕坐标,如果在屏幕显示范围内就绘制,反之就跳过。

按下左右方向键后,对应修改mapViewFrommapViewTo变量的值,就可以将地图的不同部分投影到屏幕上,从而实现地图的滚动。运行程序,画面3‑5所示。

35 滚动地图初步实现

按下方向键后,只有背景地图在移动,马里奥始终在屏幕左下角,实际是没有移动的,但视觉上感觉马里奥在行走。为了简化演示代码,程序中没有进行碰撞检测,所以马里奥和水管重叠了。

载入地图保存碰撞物体信息时,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])

 

# 滚动地图FromTo

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()

运行程序,马里奥移动的同时,地图也是滚动的,马里奥也不会进入水管了。