2025, Sep 26 19:32
ScreenManager के साथ Kivy पोंग में गलत टक्करें: कारण और फिक्स
Kivy पोंग में ScreenManager के बाद टक्करें ऑफसेट दिखें तो वजह विजेट-कैनवास साइज मिसमैच है। Orb व Bat पर size सेट कर बग बिना गेम लूप बदले ठीक करें, आसानी से.
ScreenManager के साथ एक मुख्य मेनू जोड़ने पर साधारण Kivy पोंग क्लोन में कभी-कभी अजीब विजुअल समस्या दिखती है: पैडल और बॉल देखने में तो ठीक केंद्र में लगते हैं, लेकिन टक्करें और किनारों की जांच ऐसे बर्ताव करती हैं मानो वे कहीं और हों। गेम चलता है, बॉल उछलती है, पर यह सब अदृश्य सीमाओं के भीतर होता है। Position और size hints काम नहीं आते, और pos: 0, 0 ज़बरदस्ती देने पर सब कुछ कोने से चिपक जाता है। नीचे उसी स्थिति का न्यूनतम, पुनरुत्पाद्य सेटअप और उसका सटीक समाधान दिया गया है।
समस्या की रूपरेखा
यह कोड मेनू और गेम स्क्रीन को जोड़ता है, गेम लूप शेड्यूल करता है, और KV कैनवास से बॉल व पैडल बनाता है। लॉजिक काम करता है, लेकिन टक्करें खिसकी हुई महसूस होती हैं और दायां पैडल स्क्रीन के किनारे से पूरी तरह नहीं मिलता।
मुख्य मॉड्यूल:
from kivy.app import App
from kivy.properties import NumericProperty, ReferenceListProperty, ObjectProperty
from kivy.uix.screenmanager import Screen, ScreenManager, NoTransition
from kivy.uix.widget import Widget
from kivy.vector import Vector
from kivy.clock import Clock
class Bat(Widget):
    points = NumericProperty(0)
    def deflect_orb(self, orb):
        if self.collide_widget(orb):
            vx, vy = orb.momentum
            offset = (orb.center_y - self.center_y) / (self.height / 2)
            reflected = Vector(-1 * vx, vy)
            impulse = reflected * 1.1
            orb.momentum = impulse.x, impulse.y + offset
class Orb(Widget):
    speed_x = NumericProperty(0)
    speed_y = NumericProperty(0)
    momentum = ReferenceListProperty(speed_x, speed_y)
    def advance(self):
        self.pos = Vector(*self.momentum) + self.pos
class Arena(Widget):
    orb = ObjectProperty(None)
    left_bat = ObjectProperty(None)
    right_bat = ObjectProperty(None)
    def toss_orb(self, vel=(4, 0)):
        self.orb.center = self.center
        self.orb.momentum = vel
    def step_frame(self, dt):
        self.orb.advance()
        self.left_bat.deflect_orb(self.orb)
        self.right_bat.deflect_orb(self.orb)
        if (self.orb.y < 0) or (self.orb.top > self.height):
            self.orb.speed_y *= -1
        if self.orb.x < self.x:
            self.right_bat.points += 1
            self.toss_orb(vel=(4,0))
        if self.orb.x > self.width:
            self.left_bat.points += 1
            self.toss_orb(vel=(-4,0))
    def on_touch_move(self, touch):
        if touch.x < self.width / 3:
            self.left_bat.center_y = touch.y
        if touch.x > self.width - self.width / 3:
            self.right_bat.center_y = touch.y
class ArenaScreen(Screen):
    def on_enter(self, *args):
        match = Arena()
        self.add_widget(match)
        match.toss_orb()
        Clock.schedule_interval(match.step_frame, 1 / 60)
        return match
class MainMenu(Screen):
    pass
class PongSuite(App):
    def build(self):
        sm = ScreenManager(transition=NoTransition())
        sm.add_widget(MainMenu(name="menu"))
        sm.add_widget(ArenaScreen(name="game"))
        return sm
if __name__ == "__main__":
    PongSuite().run()
KV फ़ाइल:
#:kivy 2.3.1
<Orb>:
    canvas:
        Ellipse:
            pos: self.pos
            size: 50, 50
<Bat>:
    canvas:
        Rectangle:
            pos: self.pos
            size: 25, 200
<Arena>:
    orb: pong_orb
    left_bat: bat_left
    right_bat: bat_right
    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height
    Label:
        font_size: 70
        center_x: root.width / 4
        top: root.top - 50
        text: str(root.left_bat.points)
    Label:
        font_size: 70
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: str(root.right_bat.points)
    Orb:
        id: pong_orb
        center: self.parent.center
    Bat:
        id: bat_left
        x: self.parent.x
        center_y: self.parent.center_y
    Bat:
        id: bat_right
        x: self.parent.width - self.width
        center_y: self.parent.center_y
<MainMenu>:
    start_btn: action_start
    BoxLayout:
        orientation: 'vertical'
        Button:
            id: action_start
            text: "[b]Start Game[/b]"
            markup: True
            width: 200
            height: 200
            on_press: root.manager.current = "game"
असल में गड़बड़ी क्या है
गेम में बॉल के लिए 50×50 की Ellipse और हर पैडल के लिए 25×200 का Rectangle ड्रॉ होता है। लेकिन जिन विजेट्स पर ये ड्रॉइंग्स हैं, वे अपनी डिफ़ॉल्ट साइज ही रखते हैं। मान जांचने पर बॉल विजेट 100×100 दिखता है, और पैडल विजेट भी अपनी ड्रॉइंग के आकार के मुताबिक नहीं है। इस असंगति से दिखाई देने वाले आकार और वास्तविक विजेट ज्योमेट्री अलग हो जाते हैं। नतीजतन, स्क्रीन किनारों पर प्लेसमेंट और टक्कर जांच ऐसे बर्ताव करते हैं मानो ऑब्जेक्ट बड़े और खिसके हुए हों।
पहली नज़र में यह लेआउट की समस्या लगती है, खासकर ScreenManager जोड़ने के बाद, लेकिन असल वजह यह है कि विजेट का आकार उसके कैनवास पर बने शेप से मेल नहीं खा रहा।
समाधान
विजेट्स पर ही स्पष्ट साइज सेट करें ताकि वे कैनवास प्रिमिटिव्स से मेल खाएं। ऐसा करते ही दायां पैडल बॉर्डर के साथ ठीक बैठता है और टक्करें विजुअल्स के अनुरूप हो जाती हैं।
<Orb>:
    size: 50, 50
    canvas:
        Ellipse:
            pos: self.pos
            size: 50, 50
<Bat>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: 25, 200
इस अपडेट के बाद टक्करें सही दिखती हैं और दायां पैडल ठीक जगह पर आता है (कम से कम Linux पर)। विंडो का आकार बदलने पर भी टक्करें सही लगती रहती हैं और ऑब्जेक्ट का साइज वही रहता है, जबकि दूरियां बड़ी नजर आती हैं। यह प्रभाव चाहिए या नहीं, यह लक्षित व्यवहार पर निर्भर करेगा।
यह बात ध्यान में क्यों रखनी चाहिए
Kivy में canvas निर्देश तय करते हैं कि आप क्या देखते हैं, लेकिन वे विजेट का अपना आकार नहीं बदलते। विजेट ज्योमेट्री पर आधारित कोई भी लॉजिक—स्क्रीन के सापेक्ष पोजिशनिंग से लेकर एलिमेंट्स की परस्पर क्रिया तक—विजेट के आकार का ही उपयोग करता है। इसलिए विजुअल शेप और विजेट बाउंड्स को एक जैसा रखने से उलझाने वाले ऑफ़सेट्स और “अदृश्य” हिटबॉक्स से बचा जा सकता है।
निष्कर्ष
जब आप कैनवास पर गेम एलिमेंट्स को कस्टम-ड्रॉ करते हैं, तो विजेट की साइज को ड्रॉ किए गए शेप के साथ संरेखित रखें। इसी मामले में, बॉल के लिए size: 50, 50 और पैडल के लिए size: 25, 200 स्पष्ट रूप से सेट करने से ऑफ़सेट टक्करें और बॉर्डर अलाइनमेंट की समस्याएं बिना गेम लूप या ScreenManager सेटअप को छेड़े दूर हो जाती हैं।
यह लेख StackOverflow पर प्रश्न (लेखक: blindcat97) और furas द्वारा दिए गए उत्तर पर आधारित है।