Pygame——创建游戏地图

本文介绍了如何使用Python的Pygame库制作一个简单的游戏地图编辑器。通过地图元素类和地图类的设计,实现了地图的编辑功能,包括背景、地图元素的选择与放置。文章还展示了地图编辑区、背景素材和元素素材的布局,并提及了后续将探讨的寻路算法。
部署运行你感兴趣的模型镜像

记得以前有几款很经典的游戏(红色警戒,命令与征服,英雄无敌),不小心暴漏了自己的年龄(这几款游戏都有年头了),因为知道并玩过这几款游戏的人可能还记得,里面有一个功能,就是自己编辑地图,在自己编辑的地图上玩游戏。
当时觉得这个功能很炫酷,因为通常游戏场景都是游戏制作者给出的,玩家没得选。最近学习Python,觉得Pygame可以很轻松就实现这个功能,于是自己实现了一下。供感兴趣的朋友们参考,批评指正。
废话不多说,说正事。
既然是编辑地图,必须要有背景和地图上展示的元素,以及元素在地图上的位置等信息。
打码开始:
地图元素类:

class Elements(Sprite):
    def __init__(self, image, position, layer, layerGroup, *groups):
        self._layer = layer
        self.groups = layerGroup, *groups
        super().__init__(self.groups)

        # image 包含了路径 "assets/images/"...
        self.imageFile = image      # 记录图像路径和文件名
        self.image = pygame.image.load(image).convert_alpha()
        self.rect = Rect(position, self.image.get_size())

    def get_layer(self):
        return self._layer

地图类如下。

class Map:
    def __init__(self):
        self.bg = None
        self.elements = []

    def re_init(self):
        if self.bg:
            self.bg.kill()

        self.bg = None

        for element in self.elements:
            element.kill()

        self.elements = []

    def change_bg(self, newBg):
        if self.bg:
            self.bg.kill()

        self.bg = newBg

    def add_element_into_map(self, element):
        self.elements.append(element)

实际上地图就是一个地图元素(包含背景地图和元素的容器)。因此非常简单。那么如何实现编辑地图呢?下面给出地图编辑类。

class MapMaker(Sprite):
    def __init__(self, image, layer, layerGroup):
        self._layer = layer
        super().__init__(layerGroup)

        self.layerGroup = layerGroup

        self.image = pygame.image.load(image).convert()
        self.rect = Rect((0, 0), SCREEN_SIZE)

        # 保存从素材库获取的背景地图素材和地图元素素材
        self.bgElements = []
        self.elements = []
        
        # 从素材库中读取素材
        self.get_elements_from_files()

        # 添加背景和地图元素后的地图 并给一个缺省的背景
        self.map = Map()
        self.map.bg = Elements(DEFAUT_MAP_BACKGROUP, (0, 0), BACKGROUND_LAYER, self.layerGroup)

        # 处理事件时使用
        self.elementSelected = None  # 将要操作的地图元素
        self.relPos = (0, 0)    # 记录在选择区鼠标位置与被选择元素rect.topleft 的相对位置

        self.selectedList = []  # 从地图区选择的地图元素
        self.relPosList = []    # 修改地图去元素的位置时记录rect.topleft 和鼠标位置的相对值

        # 是否加载过地图,保存加载地图的信息用于覆盖或者新建地图
        self.currentOpenedMap = None

        # 增加命令按钮
        self.buttonList = Group()
        self.init_button_list()

从 mapMaker类的初始化函数中可以看到定义了一些地图操作时需要的变量,另外定义了几个button。Button类在我的以前的文章中已经给出过,基本上没有什么修改,小小调整一下就可以直接拿过来用。这里就不重复了。
下面是button初始化和button的回调函数:

    def init_button_list(self):
        self.buttonList.append(Button(NEW_MAP_BUTTON, "New Map", FONT_NAME, FONT_SIZE, FONT_COLOR, MSG_LAYER,
                                    (BOTTOM_COLOR, ON_HOVER_COLOR, ON_CLICK_COLOR),
                                    self.layerGroup, None, None, None, self.new_map_callback))
        self.buttonList.append(Button(LOAD_MAP_BUTTON, "Load Map", FONT_NAME, FONT_SIZE, FONT_COLOR, MSG_LAYER,
                                    (BOTTOM_COLOR, ON_HOVER_COLOR, ON_CLICK_COLOR),
                                    self.layerGroup, None, None, None, self.load_map))
        self.buttonList.append(Button(SAVE_MAP_BUTTON, "Save Map", FONT_NAME, FONT_SIZE, FONT_COLOR, MSG_LAYER,
                                    (BOTTOM_COLOR, ON_HOVER_COLOR, ON_CLICK_COLOR),
                                    self.layerGroup, None, None, None, self.save_map))
        # 暂时不实现在此地图上玩游戏的功能,后续的文章中会继续实现。
        self.buttonList.append(Button(START_GAME_BUTTON, "Start Game", FONT_NAME, FONT_SIZE, FONT_COLOR, MSG_LAYER,
                                    (BOTTOM_COLOR, ON_HOVER_COLOR, ON_CLICK_COLOR),
                                    self.layerGroup, None, None, None, FONT_COLOR, None))

    def new_map_callback(self):
        self.map.re_init()
        self.map.bg = Elements(DEFAUT_MAP_BACKGROUP, (0, 0), BACKGROUND_LAYER, self.layerGroup)

        self.currentOpenedMap = None

        pygame.display.update()

    def save_map(self):
        dialog = win32ui.CreateFileDialog(0)  # 0 为保存文件对话框
        dialog.SetOFNInitialDir(IMAGE_PATH + "maps/")
        flag = dialog.DoModal()

        if flag != 1:
            return

        mapFileName = dialog.GetPathName()
        with open(mapFileName, 'w+') as mapFile:
            # 第一行保存背景地图。如果选择地图,那么保存给定的地图,如果没有选定地图,给一个缺省地图
            bgInfo = self.map.bg.imageFile + ":" + "(0, 0)" + ":" + str(self.map.bg.get_layer())
            mapFile.write(bgInfo)
            mapFile.write("\r")
            # 后续保存地图上的山、树、等其他元素
            for element in self.map.elements:
                info = element.imageFile + ":" + str(element.rect.topleft) + ":" + str(element.get_layer())
                mapFile.write(info)
                mapFile.write("\r")

    # 从保存的地图文件中加载信息到 self.map中
    def load_map(self):
        dialog = win32ui.CreateFileDialog(1)        # 1 为选择文件对话框
        dialog.SetOFNInitialDir(IMAGE_PATH + "maps/")
        flag = dialog.DoModal()

        if flag != 1:
            return

        mapFile = dialog.GetPathName()
        if mapFile:
            self.map.re_init()
            with open(mapFile, 'r') as elements:
                elementList = elements.readlines()

                # 第一行保存的是地图背景
                imageFile, position, layer = elementList[0].split(":")
                self.map.bg = Elements(imageFile, eval(position), int(layer), self.layerGroup)
                elementList.pop(0)

                # 后面是地图中的元素
                for element in elementList:
                    imageFile, position, layer = element.split(':')
                    self.map.elements.append(Elements(imageFile, eval(position), int(layer), self.layerGroup))

从素材库中读取地图元素方法:

    # 从素材库(文件目录下)读取素材信息到 self.bgElements 和 self.elements[]
    def get_elements_from_files(self):
        element = None

        # 地图元素显示位置
        bgPosition = BG_POSITION
        elementPos = ELEMENTS_POSITION
        for root, dirs, files in os.walk(IMAGE_PATH + 'mapelements/'):
            for file in files:
                if root[-2:] == 'bg':
                    element = Elements(IMAGE_PATH + 'mapelements/bg/' + file, bgPosition, BACKGROUND_LAYER,
                                       self.layerGroup)
                    self.bgElements.append(element)
                    bgPosition = (bgPosition[0] + element.rect.width, bgPosition[1])
                else:
                    if root[-1] == '1':
                        element = Elements(IMAGE_PATH + 'mapelements/layer1/' + file, elementPos, LAYER_1,
                                           self.layerGroup)

                    if root[-1] == '2':
                        element = Elements(IMAGE_PATH + 'mapelements/layer2/' + file, elementPos, LAYER_2,
                                           self.layerGroup)

                    self.elements.append(element)
                    elementPos = (elementPos[0], elementPos[1] + element.rect.height)

什么的代码有点摸不着头脑? 看一下文件组织结构:
在这里插入图片描述
明白了吗?mapelements目录下分类存储了bg,layer1,layer2等信息,你可以根据自己的需要存放地图元素。总之,显示在最上面的layer 要设置的大,在地下可以被部分或全部覆盖的元素layer 设置要小。
好了,mapMaker类的基本功能就差不多了。这里展示一下我做的一个演示。
在这里插入图片描述
格栅区是地图编辑区,下面是背景地图素材,右边是地图元素素材,右下角是四个功能按钮。
为什么这里展示的是格栅背景图呢?因为游戏钟有会有敌人和英雄,通常英雄是由玩家操纵的,而敌人是电脑自己操作的。不管是敌人还是英雄,都会运动(从一个地方移动到另一个地方)。那么英雄或者敌人怎么能从当前的地方到达目的地呢?想一想,地图中的山川、河流、房子、树木,这些对于运动物体来说都是障碍物,那么运动体在寻路的过程中必须要避开障碍寻找最佳的路径到达目的地。后续的文章中将会讨论并实现这个过程,本文只讨论地图编辑相关的实现。
上面的代码虽然提供了一些操作,那么元素怎么从元素区放到地图上的呢?怎么更新背景呢?地图上的元素需要移动或删除怎么弄呢?这些都涉及到对事件的响应和处理。

    # 检测相关事件,包括命令按钮, 地图元素操作等
    def check_event(self, event):
        for button in self.buttonList:
            button.check_event(event)

        if event.type == MOUSEBUTTONDOWN and pygame.mouse.get_pressed()[0]:
            downPos = pygame.mouse.get_pos()
            # 从地图背景元素区选择元素
            for element in self.bgElements:
                if element.rect.collidepoint(downPos):
                    self.elementSelected = Elements(element.imageFile, element.rect.topleft, element.get_layer(),
                                                    self.layerGroup)
                    self.relPos = (element.rect.left - downPos[0], element.rect.top - downPos[1])

            # 从地图元素区选择元素
            for element in self.elements:
                if element.rect.collidepoint(downPos):
                    self.elementSelected = Elements(element.imageFile, element.rect.topleft, element.get_layer(),
                                                    self.layerGroup)
                    self.relPos = (element.rect.left - downPos[0], element.rect.top - downPos[1])

            # 在地图区选择已经放在地图山的元素,有些元素可能重叠,所以点击一个位置可能会选择到多个元素
            for element in self.map.elements:
                if element.rect.collidepoint(downPos):
                    self.selectedList.append(element)
                    self.relPosList.append((element.rect.left - downPos[0], element.rect.top - downPos[1]))

        # 拖动元素时随鼠标位置显示
        if event.type == MOUSEMOTION:
            relativeMove = pygame.mouse.get_rel()
            if self.elementSelected:
                self.elementSelected.rect.left += relativeMove[0]
                self.elementSelected.rect.top += relativeMove[1]

            for element in self.selectedList:
                element.rect.left += relativeMove[0]
                element.rect.top += relativeMove[1]

        if event.type == MOUSEBUTTONUP:
            upPos = pygame.mouse.get_pos()

            # 将从元素区选择的地图元素添加到地图上
            if self.elementSelected and self.map.bg.rect.collidepoint(upPos):
                # 如果是背景
                if self.elementSelected.imageFile.find("/mapelements/bg/") != -1:
                    self.map.change_bg(Elements(self.elementSelected.imageFile.replace("mapelements/bg", "bg"),
                                                (0, 0), BACKGROUND_LAYER, self.layerGroup))

                    self.elementSelected.kill()
                # 如果是地图元素
                else:
                    self.elementSelected.rect.left = (upPos[0] + self.relPos[0]) // GRID_WIDTH * GRID_WIDTH
                    self.elementSelected.rect.top = (upPos[1] + self.relPos[1]) // GRID_HEIGHT * GRID_HEIGHT

                    self.map.add_element_into_map(self.elementSelected)
                    self.relPos = (0, 0)

                self.elementSelected = None
                # self.map.output_map_sprites()
            # 如果将选择的元素区的背景或者地图元素放在非地图区,相当于放弃刚才的选择
            else:
                if self.elementSelected:
                    self.elementSelected.kill()
                    self.elementSelected = None

            # 修改地图区的元素位置或者删除选中的元素
            if self.selectedList:
                # 如果是修改选择元素的位置
                if self.map.bg.rect.collidepoint(upPos):
                    for i in range(0, self.selectedList.__len__() - 1):
                        self.selectedList[i].rect.left = (upPos[0] + self.relPosList[i][0]) // GRID_WIDTH * GRID_WIDTH
                        self.selectedList[i].rect.top = (upPos[1] + self.relPosList[i][1]) // GRID_HEIGHT * GRID_HEIGHT
                # 如果删除选中的地图元素
                else:
                    for sprite in self.selectedList:
                        sprite.kill()

                # 清空列表
                self.selectedList.clear()
                self.relPosList.clear()

什么有一段代码:

self.map.change_bg(Elements(self.elementSelected.imageFile.replace("mapelements/bg", "bg"),
                                                (0, 0), BACKGROUND_LAYER, self.layerGroup))

替换图片路径说明一下,因为背景地图元素只是存储的一个缩略图,真是的大背景地图存储在这里:
在这里插入图片描述
应该明白了吧。当然你也可以在展示地图元素时使用pygame的transform功能,这样只需要保存真实背景图片即可。
好了全部的类都已经完成,进入测试代码了。

def main():
    pygame.init()

    screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)

    layerGroup = LayeredUpdates()

    mapMaker = MapMaker(DEFAUT_MAP_BACKGROUP, BACKGROUND_LAYER, layerGroup)

    screen.fill((0, 0, 0))
    layerGroup.draw(screen)
    pygame.display.update()

    fpsClock = pygame.time.Clock()

    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()

            mapMaker.check_event(event)

        screen.fill((0, 0, 0))
        layerGroup.draw(screen)

        fpsClock.tick(30)

        pygame.display.update()


if __name__ == '__main__':
    main()

所有的代码都在上面了,部分常量的定义如下:

IMAGE_PATH = "assets/images/"

SCREEN_SIZE = (1300, 900)

GRID_X_QTY = 100
GRID_Y_QTY = 80
GRID_WIDTH = 10
GRID_HEIGHT = 10
OBSTRUCT = 0

BUTTON_SIZE = (90, 40)
NEW_MAP_BUTTON = (1100, 805)
LOAD_MAP_BUTTON = (1200, 805)
SAVE_MAP_BUTTON = (1100, 855)
START_GAME_BUTTON = (1200, 855)
BG_POSITION = (0, 805)
ELEMENTS_POSITION = (1200, 0)

FONT_SIZE = 18

BACKGROUND_LAYER = 1
LAYER_1 = 10
LAYER_2 = 20
MSG_LAYER = 50

DEFAUT_MAP_BACKGROUP = "assets/images/bg/bg5.png"

What?就这么简单?是的,所有测试代码都在这里,需要实现的功能和操作都封装在类里,因此测试代码只需要在while循环中加入事件检测就可以了。
感兴趣的朋友可以试一试。
另外,保存地图后文件演示如下:

assets/images/bg/bg5.png:(0, 0):0
assets/images/mapelements/layer1/mountain3.png:(140, 300):10
assets/images/mapelements/layer1/mountain4.png:(360, 140):10
assets/images/mapelements/layer1/mountain3.png:(880, 160):10
assets/images/mapelements/layer1/mountain3.png:(940, 160):10
assets/images/mapelements/layer1/mountain1.png:(440, 140):10
assets/images/mapelements/layer1/tower.png:(380, 280):10
assets/images/mapelements/layer1/tower.png:(1000, 300):10
assets/images/mapelements/layer1/mountain4.png:(300, 640):10
assets/images/mapelements/layer1/mountain4.png:(880, 680):10
assets/images/mapelements/layer1/mountain4.png:(820, 680):10
assets/images/mapelements/layer1/mountain2.png:(360, 660):10

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

### 实现 Pygame 地图编辑器的思路与示例 在 Pygame 中实现游戏地图编辑功能,可以通过创建一个交互式界面,允许用户通过鼠标点击或拖拽来放置、删除或修改地图元素(如地形、建筑等)。为了提高效率和灵活性,可以结合一些辅助模块,例如 `pyscroll` 来处理地图滚动和渲染。 #### 基本结构设计 首先需要定义地图的基本单位——图块(Tile),并设置一个二维数组来记录每个图块的状态。例如,0 表示空地,1 表示墙壁,2 表示草地等。编辑器应提供对这些值的可视化操作。 ```python import pygame # 初始化 Pygame pygame.init() # 屏幕尺寸和图块大小 SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600 TILE_SIZE = 40 # 创建屏幕对象 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Map Editor") # 加载图块图像 tile_grass = pygame.image.load("grass.png").convert() tile_wall = pygame.image.load("wall.png").convert() # 地图数据(假设为15行20列) map_data = [[0 for _ in range(20)] for _ in range(15)] # 主循环标志 running = True # 游戏主循环 while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.MOUSEBUTTONDOWN: # 获取鼠标点击位置 mouse_x, mouse_y = pygame.mouse.get_pos() tile_x = mouse_x // TILE_SIZE tile_y = mouse_y // TILE_SIZE # 切换图块状态(0 -> 1 -> 0) map_data[tile_y][tile_x] = (map_data[tile_y][tile_x] + 1) % 2 # 绘制地图 for y, row in enumerate(map_data): for x, tile in enumerate(row): screen.blit(tile_grass if tile == 0 else tile_wall, (x * TILE_SIZE, y * TILE_SIZE)) # 更新显示 pygame.display.flip() # 退出 Pygame pygame.quit() ``` #### 高级功能扩展 如果希望支持更多复杂功能,比如加载 Tiled 编辑器制作的地图,可以使用 `pytmx` 和 `pyscroll` 模块。`pyscroll` 是一个轻量级但功能强大的模块,用于在 Pygame 游戏中实现高效的动态滚动地图,并且与 `pytmx` 的集成意味着可以直接利用 Tiled Map Editor 创建地图 [^2]。通过这些工具,可以在地图编辑器中实现平滑的缩放和移动体验。 此外,还可以添加保存和加载地图的功能。一种简单的方式是使用 Python 的 `json` 模块来序列化地图数据: ```python import json def save_map(map_data): with open('map.json', 'w') as f: json.dump(map_data, f) def load_map(): with open('map.json', 'r') as f: return json.load(f) ``` 这种方法确保了地图数据能够持久化存储,并可在不同会话之间复用 [^4]。 #### 用户交互优化 为了让地图编辑器更加友好,可以增加以下特性: - **选择不同的图块类型**:通过按键切换当前绘制的图块类型。 - **撤销/重做功能**:维护一个历史栈来记录每次操作前的地图状态。 - **网格线显示**:帮助用户更精确地定位图块。 - **多层地图支持**:例如背景层、物体层、前景层等,适用于复杂的 RPG 或模拟经营类游戏 [^1]。 #### 性能考虑 当处理大规模地图时,需要注意性能问题。可以通过仅更新屏幕上可见区域的方式来减少不必要的绘图操作。此外,对于大量图块的渲染,可采用精灵组(Sprite Groups)和碰撞检测技术来优化 [^5]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值