第3章       增加地图

随着游戏中物体数量的增加,不断在代码中调整位置和大小变得越来越繁琐。因此,我们应该找到一种有效的方式来管理这些物体。

3.1    瓦片地图

瓦片地图 (Tilemap)2D游戏开发中非常流行,它用一系列小而规则的图像块(称为瓦片)构建游戏世界或关卡地图。这种技术能够提升性能和减少内存使用量,不需要包含整个关卡地图的大型图像文件,而是通过多次使用小图像或图像片段来构建地图。使用这个技术的游戏包括《超级马里奥兄弟》、《疯狂吃豆人》、《塞尔达传说:梦幻岛》、《星际争霸》和《模拟城市2000》等等。

3‑1展示了使用软件Tiled编辑地图的界面。左侧的每个小格子代表一个瓦片,整个地图由多个瓦片组成。右侧的tile_set包含可供使用的瓦片,可以选择瓦片后在地图上自由绘制。

31 使用Tiled编辑地图

Python调用Tiled的地图文件稍微复杂了些,我们先选择更简单的方式,使用列表来模拟地图。马里奥游戏的地图结构如3‑2所示。

32 马里奥的地图结构

每个格子都代表一个瓦片,地图的高度是14个瓦片,而宽度则不确定。地面的每块石头是一个瓦片,而高度为2的水管则由4个瓦片组成。砖块和问号方块各自是一个瓦片,而白云、树丛和小山由多个瓦片组成。其余部分代表蓝天,同样由多个瓦片组成。

这个地图可以使用嵌套的列表来表示。每一行可以用一个数组来表示,数组中的每个元素代表一个瓦片。然后将每一行的数组放入一个大的列表中,表示整个地图。大概的代码结构类似于这样:

mapArray=[]

mapArray.append([1,1,1,1,1,1,1,1,1,1,1,1,3,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1])

mapArray.append([1,1,1,1,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,4,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])

那这些数字是什么含义呢?我们是这样设定的:1代表天空,2代表地面,3代表砖块,4代表水管,其他元素暂时忽略。有了地图,只需按照地图顺序,从左到右、从上到下,绘制每个瓦片即可。代码如下所示:

03\01.py

import pygame

 

# 初始化pygame

pygame.init()

 

# 设置窗口大小

WIDTH = 600

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

 

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

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("按了左方向键")

 

        # 窗口边界判断

        if marioRect.x - marioWalkStep <= 0:

            marioWalkStep = marioRect.x

 

        # 下一步预判断

        nextStepRect = marioRect.move(-1 * marioWalkStep, 0)

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

        if pipeIndex != -1:

            marioWalkStep = marioRect.x - objectArray[pipeIndex].right

        marioRect = marioRect.move(-1 * marioWalkStep, 0)

    if keys[pygame.K_RIGHT]:

        print("按了右方向键")

 

        # 窗口边界判断

        if marioRect.right + marioWalkStep >= WIDTH:

            marioWalkStep = WIDTH - marioRect.right

 

        # 下一步预判断

        nextStepRect = marioRect.move(marioWalkStep, 0)

        pipeIndex = nextStepRect.collidelist(objectArray)

        if pipeIndex != -1:

            marioWalkStep = 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 = nextStepRect.collidelist(objectArray)

        if pipeIndex != -1:

            jumpStep = (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)):

            cell = row[cellIndex]

            posX = cellIndex * blockSize

            posY = rowIndex * blockSize

            if cell == 1:

                # 天空

                pygame.draw.rect(screen, skyblue, ((posX, posY), (blockSize, blockSize)), 0)

            elif cell == 2:

                # 地面

                pygame.draw.rect(screen, ground, ((posX, posY), (blockSize, blockSize)), 0)

            elif cell == 3:

                # 砖块

                pygame.draw.rect(screen, brick, ((posX, posY), (blockSize, blockSize)), 0)

            elif cell == 4:

                # 水管

                pygame.draw.rect(screen, pipeGreen, ((posX, posY), (blockSize, blockSize)), 0)

 

    # 马里奥Y轴步伐

    marioWalkStepY = 4

 

    # 下一步预判断

    nextStepRect = marioRect.move(0, marioWalkStepY)

    pipeIndex = nextStepRect.collidelist(objectArray)

    if pipeIndex != -1:

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

请注意,地图数据在两个地方进行遍历。首先,在载入地图后,我们第一次遍历地图,将参与碰撞检测的物体存储在一个数组中,以便后续与马里奥进行碰撞检测。天空不参与碰撞检测,以免马里奥被卡住。第二次遍历发生在每一帧绘制时,用来绘制每一个瓦片。这表明瓦片的绘制与碰撞检测是分离的,互相影响不大。将一个瓦片绘制成大小为100像素,但只有50像素大小的部分需要参与碰撞检测是完全可以实现的。

在程序中,地面、水管和砖块使用不同的颜色进行绘制。运行程序,画面如3‑3所示,是否感觉有些熟悉呢?

33 地图初步实现

         当马里奥走到地面的空缺处时,他会直接掉下去。这是因为我们已经去掉了作为地面的直线,现在马里奥完全依赖碰撞检测来确定是否站在地面上。如果下方没有物体支撑的话,马里奥会因为重力而掉落。