2025, Sep 26 19:21
Kivy Pong: смещённые столкновения из-за размера виджетов
После добавления ScreenManager в Kivy Pong столкновения выглядят смещёнными: геометрия виджетов не совпадает с canvas. Исправление: задайте виджетам size.
Добавление главного меню через ScreenManager к простому клону Pong на Kivy иногда приводит к странному визуальному багу: ракетки и мяч выглядят по центру, но столкновения и проверки границ ведут себя так, будто всё сдвинуто. Игра идёт, мяч отскакивает, но как будто от невидимых стен. Подсказки позиционирования и размера не помогают, а принудительное 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"
Что на самом деле не так
Игра рисует Ellipse для мяча размером 50×50 и Rectangle для каждой ракетки размером 25×200. Однако виджеты, которым принадлежат эти рисунки, сохраняют размер по умолчанию. Проверка показывает 100×100 для виджета мяча, и виджет ракетки тоже не подогнан под свой рисунок. Из‑за этого видимые формы и геометрия виджета расходятся. В результате размещение у краёв экрана и проверки столкновений работают так, словно объекты крупнее и смещены.
На первый взгляд кажется, что дело в раскладке, особенно после добавления ScreenManager, но первопричина — несоответствие размера виджета тому, что нарисовано на его canvas.
Решение
Задайте явные размеры самим виджетам так, чтобы они совпадали с примитивами на canvas. После этого правая ракетка выравнивается по границе, а столкновения совпадают с визуалом.
<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 определяют, что вы видите, но не меняют собственный размер виджета. Любая логика, завязанная на геометрию виджета — от позиционирования относительно экрана до взаимодействия элементов — использует размер виджета. Когда визуальные формы и границы виджета согласованы, исчезают непонятные смещения и «невидимые» хитбоксы.
Итог
Если вы рисуете игровые элементы на canvas вручную, всегда приводите размер виджета в соответствие с формой. В этом случае явные size: 50, 50 для мяча и size: 25, 200 для ракеток устраняют смещённые столкновения и проблемы с выравниванием по границам — без правок игрового цикла или настройки ScreenManager.