随着游戏中物体数量的增加,不断在代码中调整位置和大小变得越来越繁琐。因此,我们应该找到一种有效的方式来管理这些物体。
瓦片地图 (Tilemap)在2D游戏开发中非常流行,它用一系列小而规则的图像块(称为瓦片)构建游戏世界或关卡地图。这种技术能够提升性能和减少内存使用量,不需要包含整个关卡地图的大型图像文件,而是通过多次使用小图像或图像片段来构建地图。使用这个技术的游戏包括《超级马里奥兄弟》、《疯狂吃豆人》、《塞尔达传说:梦幻岛》、《星际争霸》和《模拟城市2000》等等。
图3‑1展示了使用软件Tiled编辑地图的界面。左侧的每个小格子代表一个瓦片,整个地图由多个瓦片组成。右侧的tile_set包含可供使用的瓦片,可以选择瓦片后在地图上自由绘制。

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

图3‑2 马里奥的地图结构
每个格子都代表一个瓦片,地图的高度是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所示,是否感觉有些熟悉呢?

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