2025, Oct 19 15:17

Tiled и pytmx: коллизии в pygame не срабатывают из-за свойства в тайлсете

Почему get_tile_properties_by_gid в pytmx возвращает пусто и коллизии в pygame не работают. Задайте свойство в Tiled на уровне tile, а не tileset. Учитывайте gid.

Когда вы экспериментируете с пользовательскими свойствами в Tiled и подключаете их к pygame через pytmx, часто всплывает один неприятный нюанс: свойства вроде бы заданы, но в рантайме никак себя не проявляют. Симптом обманчиво простой: проверка столкновений всегда возвращает false, и персонаж спокойно проходит сквозь тайлы, которые должны быть непроходимыми. Причина здесь не в pygame и не в логике движения, а в том, где именно расположено свойство внутри TMX.

Постановка задачи

В тайлсете на карте определено логическое свойство “Colider”, а слой использует gid 2 для этих тайлов. Со стороны Python свойства тайла считываются через get_tile_properties_by_gid(gid) — ожидается, что флаг появится и движение игрока будет остановлено. На практике чтение возвращает пусто, и проверка ни разу не срабатывает.

<tileset firstgid="2" name="pixil-frame-0" tilewidth="32" tileheight="32" tilecount="1" columns="1">
  <properties>
    <property name="Colider" type="bool" value="true"/>
  </properties>
  <image source="pixil-frame-0.png" width="32" height="32"/>
</tileset>

Ниже — минимальный пример pygame + pytmx, который демонстрирует проблему со стороны кода. Имена произвольные, логика простая: рисуем карту, двигаем игрока шагами по 32 пикселя и блокируем перемещение, если у целевого тайла Colider установлен в true.

import pygame
import pytmx
pygame.init()
VIEW_W, VIEW_H = 30 * 32, 20 * 32
canvas = pygame.display.set_mode((VIEW_W, VIEW_H))
ticker = pygame.time.Clock()
# --- Load TMX Map ---
tmx_map = pytmx.load_pygame(r'C:\Users\miked\OneDrive\Documents\GitHub\RFIDCard\map-rpg-game\Test_map.tmx')
cell_w = tmx_map.tilewidth
cell_h = tmx_map.tileheight
def paint_map():
    for lyr in tmx_map.visible_layers:
        if isinstance(lyr, pytmx.TiledTileLayer):
            for gx, gy, gid in lyr:
                bmp = tmx_map.get_tile_image_by_gid(gid)
                if bmp:
                    canvas.blit(bmp, (gx * cell_w, gy * cell_h))
class Avatar:
    def __init__(self, px, py):
        self.px = px
        self.py = py
        self.sprite = pygame.Surface((32, 32))
        self.sprite.fill((255, 0, 0))
    def hits_block(self, tx, ty, tmx_obj):
        for lyr in tmx_obj.visible_layers:
            if isinstance(lyr, pytmx.TiledTileLayer):
                try:
                    gid = lyr.data[ty][tx]
                except IndexError:
                    continue
                if gid == 0:
                    continue
                props = tmx_obj.get_tile_properties_by_gid(gid)
                if props and props.get("Colider") is True:
                    return True
        return False
    def render(self, surf):
        surf.blit(self.sprite, (self.px, self.py))
    def shift(self, dx, dy):
        next_tx = (self.px + dx) // cell_w
        next_ty = (self.py + dy) // cell_h
        if not self.hits_block(next_tx, next_ty, tmx_map):
            self.px += dx
            self.py += dy
    def handle_input(self, keys_state):
        if keys_state.get('up') or keys_state.get('w'):
            self.shift(0, -32)
        if keys_state.get('down') or keys_state.get('s'):
            self.shift(0, 32)
        if keys_state.get('left') or keys_state.get('a'):
            self.shift(-32, 0)
        if keys_state.get('right') or keys_state.get('d'):
            self.shift(32, 0)
hero = Avatar(5 * 32, 5 * 32)
# --- Main Loop ---
running = True
while running:
    for evt in pygame.event.get():
        if evt.type == pygame.QUIT:
            running = False
    keys = pygame.key.get_pressed()
    hero.handle_input({
        'up': keys[pygame.K_UP],
        'down': keys[pygame.K_DOWN],
        'left': keys[pygame.K_LEFT],
        'right': keys[pygame.K_RIGHT],
        'w': keys[pygame.K_w],
        'a': keys[pygame.K_a],
        's': keys[pygame.K_s],
        'd': keys[pygame.K_d],
    })
    canvas.fill((0, 0, 0))
    paint_map()
    hero.render(canvas)
    pygame.display.flip()
    ticker.tick(10)

Что на самом деле идет не так

Поиск свойств возвращает None, потому что свойство прикреплено к самому тайлсету, а не к конкретному тайлу внутри него. Вызов get_tile_properties_by_gid(gid) возвращает свойства для конкретного элемента tile, поэтому данные должны лежать внутри узла tile. Иными словами, props всегда равен None, проверка на Colider не срабатывает, и движение нигде не блокируется.

Есть и нюанс отображения, объясняющий, почему здесь фигурирует gid 2. Gid тайла вычисляется как gid = firstgid + id. При firstgid, равном 2, и единственном тайле с id 0 в данных слоя и получается gid 2.

Исправляем данные, а не код

Надежное решение — перенести свойство внутрь элемента tile внутри тайлсета. После этого get_tile_properties_by_gid(2) вернет словарь, в котором действительно есть Colider.

<tileset firstgid="2" name="pixil-frame-0" tilewidth="32" tileheight="32" tilecount="1" columns="1">
  <tile id="0">
    <properties>
      <property name="Colider" type="bool" value="true"/>
    </properties>
  </tile>
  <image source="pixil-frame-0.png" width="32" height="32"/>
</tileset>

После правки быстрая проверка подтверждает, что поиск теперь отдает ожидаемую структуру.

props = tmx_map.get_tile_properties_by_gid(2)
print(props)
{'id': 0, 'Colider': True, 'width': 32, 'height': 32, 'frames': []}

Игровой код менять не нужно: процедура перемещения корректно блокирует шаг, когда props.get("Colider") равно True.

Если свойство действительно относится к тайлсету

Если задумка — хранить значение на уровне всего тайлсета, а не отдельных тайлов, его можно прочитать из коллекции tilesets. Поскольку tilesets — это список, доступ идет не по gid, а по индексу тайлсета.

val = tmx_map.tilesets[1].properties.get("Colider")

Это отличимо от свойств отдельных тайлов и никак не влияет на get_tile_properties_by_gid(gid). Пользуйтесь им только тогда, когда свойство действительно описывает весь тайлсет целиком.

Почему это важно

Смешивание свойств уровня тайлсета и уровня тайла меняет способ, которым загрузчик достает данные. Жестко «прошивать» номера gid, например считать 2 блокирующим, кажется быстрым обходным путем, но привязывает поведение к хрупкому идентификатору. Корректное размещение свойств внутри элемента tile делает игровую логику управляемой данными и сохраняет устойчивость кода по мере развития карты. К тому же отладка становится прямой, потому что print(props) сразу показывает, оттуда ли вы читаете.

Итоги

Если проверка свойства постоянно проваливается, сначала проверьте, где живут данные. Для логики на уровне отдельных тайлов — например, коллизий — задавайте свойства внутри элемента tile и помните соответствие gid = firstgid + id при разборе данных слоя. Используйте быстрые print-проверки, чтобы посмотреть считываемые объекты, и обращайтесь к свойствам тайлсета лишь тогда, когда это действительно нужно. При корректном размещении свойств тот же код коллизий работает как задумано и масштабируется без жестких проверок gid.

Материал основан на вопросе на StackOverflow от F-22 Destroyer и ответе furas.