4.3    使用瓦片图

之前,我们使用数字在地图中表示不同的物体,如1代表天空,2代表地面,3代表砖块,4代表水管。程序根据不同的数字绘制不同颜色的矩形。

现在,我们要用瓦片图替代简陋的矩形。那么,地图中的数字应该如何编号呢?一种方法是使用瓦片图的连续编号,第一个图是1,第二个图是2,以此类推。另一种方法是按照行号+列号来编号,比如第一行第一个是101,第一行第12个是112,第二行第一个是201,以此类推。为什么使用101而不是0101,是因为图片数量较少,行号用一位数就足够了。

考虑到目前地图文件是手工编辑的,使用行列号来编号更方便查找图片的位置。例如,地面编号是101,砖块是102,竖向水管由301302401402组成,如4‑5所示。蓝天实际上并不使用图片,只是窗口的背景色而已。

45瓦片组成画面

   上文我们已经成功拆分了瓦片图,由于每帧都要重绘画面,会频繁使用瓦片图,因此有必要将图片缓存起来。下面是一个缓存类的实现,将拆分的瓦片图按行列编号保存到字典里,代码如下所示:

04\02\TilesImageCache.py

import pygame

 

 

class TilesImageCache:

    def __init__(self):

        self.imageMap = {}

        self.bgImageList = [206, 112, 113, 114, 115, 212, 213, 214, 317, 417, 208, 314, 315, 415, 106, 504, 316]

 

        # 加载图集

        tileMap = pygame.image.load("tile_set.png").convert_alpha()

 

        # 设置瓦片大小

        tileWidth = 16

        tileHeight = 16

 

        # 获取的宽度和高度

        mapWidth, mapHeight = tileMap.get_size()

 

        # 计算每行每列的瓦片数量

        numCols = mapWidth // tileWidth

        numRows = mapHeight // tileHeight

 

        # 拆分并保存所有瓦片

        for row in range(numRows):

            for col in range(numCols):

                # 拆分瓦片(x,y,width,height)

                tileSurface = tileMap.subsurface(col * tileWidth, row * tileHeight, tileWidth, tileHeight)

 

                # 编号,row=0,col=0时是第一行第一列,key101

                key = (row + 1) * 100 + (col + 1)

                self.imageMap[key] = tileSurface

 

代码比较简单,不再赘述。地图文件的编号需要相应修改,如地面改成101,砖块改成102等等。

在程序运行时,第一次遍历地图会确定哪些物体参与碰撞检测,原先仅有1234四个数字用于判断逻辑,现在有更多图片,需要一种新的方法。由于大部分物体都参与碰撞,只有少数如白云、树木、小山等不参与碰撞,那么可以将这些不参与碰撞的编号记录在缓存类中的bgImageList变量,碰撞检测时排除这些编号即可。

程序第二次遍历地图是进行画面绘制,根据地图中的数字在图片缓存中找到对应的图片,然后绘制到屏幕上。修改后的代码如下所示:

04\03\Game.py

import pygame

from constants import *

from utils import *

from Globals import *

from Mario import Mario

from TilesImageCache import TilesImageCache

 

 

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

 

        # 初始化图片缓存

        Globals.tilesImageCache = TilesImageCache()

 

        # 马里奥

        self.mario = Mario(10, 100, 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]

                if cell == 1 or cell in Globals.tilesImageCache.bgImageList:

                    continue  # 背景图片,不碰撞

                else:

                    # 其他参与碰撞

                    posX = cellIndex * MAP_BLOCK_SIZE

                    posY = rowIndex * MAP_BLOCK_SIZE

                    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 in Globals.tilesImageCache.imageMap:

                        self.screen.blit(Globals.tilesImageCache.imageMap[cell], screenPos)

 

    def run(self):

 

        # 主循环

        isRunning = True

        while isRunning:

            for event in pygame.event.get():

                if event.type == pygame.QUIT:

                    isRunning = False

 

            # 全屏擦除(注意背景色改成了天蓝色)

            self.screen.fill(COLOR_SKY_BLUE)

 

            # 绘制地图

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

 

代码中主要修改了四个地方:

l  创建TilesImageCache对象,并放到Globals中,便于使用。

l  修改initMap()方法,在bgImageList变量中的编号不参与碰撞检测。

l  修改drawMap()方法,直接使用Globals.tilesImageCache.imageMap[cell]从缓存获取图片。

l  修改窗口背景色为天蓝色

另外,修改常量类中MAP_BLOCK_SIZE16,窗口高度改为22414个瓦片高,每个瓦片16像素)。运行程序,画面如2‑36所示,是不是更有感觉了?

236使用瓦片图的画面

         下面就是辛苦的地图制作时间了,参考马里奥的世界地图修改我们的地图文件,代码见04\04目录,最终的效果如2‑372‑38所示。

237完整的世界地图

238世界地图-关底