2025, Oct 19 15:32
Tiled TMX में कस्टम प्रॉपर्टीज़ सही रखें: pytmx/pygame में कोलिजन समस्या का असली कारण
pytmx/pygame में Tiled TMX की कस्टम प्रॉपर्टीज़ क्यों नहीं पढ़तीं और कोलिजन फेल होता है, समझें; tileset बनाम tile पर सही सेटिंग से इसे ठीक करें — व्यावहारिक उदाहरण सहित
जब आप Tiled में कस्टम प्रॉपर्टीज़ के साथ प्रयोग करते हैं और उन्हें pytmx के ज़रिए pygame में जोड़ते हैं, तो एक आम उलझन यह होती है कि प्रॉपर्टीज़ मौजूद तो होती हैं, लेकिन रनटाइम पर दिखती नहीं। लक्षण भ्रामक है: कोलिजन जाँच हमेशा 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 उदाहरण है जो कोड स्तर पर समस्या दिखाता है। नाम मनमाने हैं, तर्क सरल है: मैप रेंडर करें, खिलाड़ी को 32px के कदमों में चलाएँ, और अगर लक्ष्य टाइल में 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 के उत्तर पर आधारित है।