2025, Sep 26 19:00
Fixing Invisible Collisions in a Kivy Pong Clone: canvas–widget size mismatch with ScreenManager
Troubleshooting Kivy Pong with ScreenManager: collisions feel offset due to canvas–widget size mismatch. Set explicit widget sizes to align visuals and logic.
Adding a main menu with ScreenManager to a simple Kivy Pong clone sometimes results in a weird visual bug: paddles and ball look visually centered, but collisions and edge checks act as if they’re offset. The game runs, the ball bounces, but it all happens against invisible boundaries. Position and size hints don’t seem to help, and forcing pos: 0, 0 just pins everything to the corner. Here is a minimal, reproducible setup of that situation and a precise fix.
Problem setup
The following code wires a menu and a game screen, schedules the game loop, and draws the ball and paddles from the KV canvas. The logic is functional, but collisions feel shifted and the right paddle doesn’t perfectly meet the screen edge.
Main module:
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 file:
#: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"
What actually goes wrong
The game draws an Ellipse for the ball with size 50×50 and a Rectangle for each paddle with size 25×200. However, the widgets that own these drawings keep their default size. Checking those values shows 100×100 for the ball widget, and the paddle widget also isn’t sized to its drawing. This mismatch means the visible shapes and the underlying widget geometry diverge. As a result, placement at the screen edges and collision checks behave as if the objects were larger and shifted.
It looks like a layout issue at first glance, especially after introducing ScreenManager, but the root cause is the widget size not matching what’s drawn on its canvas.
The fix
Set explicit sizes on the widgets themselves so they match the canvas primitives. With that change, the right paddle aligns with the border and collisions line up with the visuals.
<Orb>:
    size: 50, 50
    canvas:
        Ellipse:
            pos: self.pos
            size: 50, 50
<Bat>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: 25, 200
After this update, the collision looks correct and the right paddle is positioned properly (at least on Linux). If the window is resized, collisions continue to look correct and object sizes remain the same, while distances appear larger. Whether that effect is desired depends on the target behavior.
Why you want this on your radar
In Kivy, canvas instructions define what you see, but they don’t change the widget’s own size. Any logic tied to widget geometry, including positioning relative to the screen and how elements interact, will use the widget’s size. Ensuring visual shapes and widget bounds are consistent eliminates confusing offsets and “invisible” hitboxes.
Wrap-up
When custom-drawing game elements on the canvas, always align the widget’s size with the drawn shape. In this case, explicitly setting size: 50, 50 for the ball and size: 25, 200 for the paddles resolves the offset collisions and border alignment issues without touching the game loop or the ScreenManager setup.
The article is based on a question from StackOverflow by blindcat97 and an answer by furas.