Kivy — Pong
О практикуме
Pong в вертикальной ориентации — удобной для телефона: ваша ракетка внизу, соперник (ИИ) сверху, мяч отскакивает от боковых стен и ракеток. Первый до 7 очков побеждает. Управление — перетаскивание пальцем по полю (на ПК — зажатая мышь).
Чему учит этот практикум
- Непрерывная физика — позиция мяча обновляется каждый кадр через
Clock(обзор Kivy); - canvas — ракетки и мяч без PNG-спрайтов;
- AABB-столкновения — прямоугольники пересекаются или нет;
- NumericProperty — счёт обновляет Label автоматически;
- адаптивные размеры —
on_sizeпересчитывает ракетки от долей поля.
Аналог на Pygame с клавиатурой и FSM — Python — Ping Pong. Там классический горизонтальный корт; здесь — мобильный вертикальный и тач вместо WASD.
Оценка времени — 2–4 часа.
Карта этапов
| Этап | Фокус | Результат |
|---|---|---|
| 0 | Каркас App | Тёмное окно |
| 1 | Paddle | Цветная ракетка на canvas |
| 2 | Ball | Мяч с serve() |
| 3 | GameField | Фон, пунктир, дочерние виджеты |
| 4 | Движение мяча | Clock.schedule_interval |
| 5 | Стены | Отскок от левого и правого края |
| 6 | Ракетки | AABB и угол отскока |
| 7 | Тач | Ракетка следует за пальцем |
| 8 | Счёт и ИИ | Гол, подача, победа |
| 9 | Ревизия | Overlay "Tap to play again" |
На каждом этапе — блок Разбор после кода. Застряли — сверьтесь с суммой листингов этапов 0–8 или перейдите к Snake после этапа 9.
Как проходить практикум
- Создайте папку
kivypong/и установите Kivy — см. зависимости. - Весь код — в одном
main.py; после каждого этапа запускайтеpython main.py. - Читайте Разбор — там связь
Clock,canvasи AABB с мобильным UX. - Финал — этап 9.
Архитектура
В отличие от 2048, здесь непрерывная физика — мяч обновляется каждый кадр через Clock.schedule_interval:
PingPongApp
└── BoxLayout (vertical)
├── Label — счёт You / CPU
└── GameField (Widget)
├── canvas.before — фон, пунктир
├── opponent: Paddle
├── ball: Ball
└── player: Paddle ← on_touch_* drag
Определение. AABB (axis-aligned bounding box) — проверка пересечения двух pygame.Rect-подобных прямоугольников без поворота. Достаточно для Pong; подробнее о коллизиях — в Pygame Pong, этап 9.
| Класс | Роль |
|---|---|
Paddle | Ракетка на canvas, NumericProperty score |
Ball | Скорость, serve(), отскок |
GameField | Поле, цикл, тач, ИИ, голы |
PingPongApp | Компоновка, подписка на score, overlay |
Зависимости
mkdir kivypong && cd kivypong
python -m venv .venv && .venv\Scripts\activate # Windows
pip install "kivy>=2.3.0"
Структура: один файл main.py. Сравните с пакетом game/ в Pygame Pong — там к этапу 14 логику выносят в модули.
Этап 0 — каркас
Цель. App, тёмный фон окна, вертикальный layout.
Теория
Window.clearcolor задаёт цвет за виджетами — полезно для letterbox на широких мониторах. BoxLayout(orientation="vertical") — типичный каркас мобильного экрана: HUD сверху, игровое поле растягивается через size_hint=(1, 1) на этапе 3.
mkdir kivypong && cd kivypong
pip install "kivy>=2.3.0"
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
class PingPongApp(App):
def build(self):
Window.clearcolor = (0.05, 0.06, 0.08, 1)
root = BoxLayout(orientation="vertical", padding=12, spacing=8)
root.add_widget(Label(text="Pong", font_size="24sp"))
return root
if __name__ == "__main__":
PingPongApp().run()
Разбор.
Window.clearcolor— цвет вне виджетов (поля за layout).orientation="vertical"— заголовок сверху, поле займёт оставшееся место на этапе 8.
Самопроверка
- Окно с тёмным фоном открывается.
Этап 1 — Paddle
Цель. виджет-ракетка, нарисованный Rectangle на canvas.
from kivy.graphics import Color, Rectangle
from kivy.properties import NumericProperty
from kivy.uix.widget import Widget
class Paddle(Widget):
score = NumericProperty(0)
def __init__(self, color, **kwargs):
super().__init__(**kwargs)
with self.canvas:
Color(*color)
self.rect = Rectangle(pos=self.pos, size=self.size)
self.bind(pos=self._update_rect, size=self._update_rect)
def _update_rect(self, *_):
self.rect.pos = self.pos
self.rect.size = self.size
Разбор.
color— кортеж RGBA(r, g, b, a)от 0 до 1.score = NumericProperty(0)— при изменении можноbind(score=...)(Properties).- Ракетка — обычный
Widget, неButton; клики проходят к родителюGameField.
Самопроверка
- Две ракетки разного цвета видны на поле.
Этап 2 — Ball
Цель. мяч с начальной скоростью при подаче.
from random import uniform
from kivy.graphics import Ellipse
from kivy.properties import NumericProperty
class Ball(Widget):
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
def __init__(self, **kwargs):
super().__init__(**kwargs)
with self.canvas:
Color(1, 1, 1, 1)
self.ellipse = Ellipse(pos=self.pos, size=self.size)
self.bind(pos=self._update_ellipse, size=self._update_ellipse)
def serve(self, direction_y=-1):
speed = self.parent.ball_speed if self.parent else 420
self.velocity_x = uniform(-0.35, 0.35) * speed
self.velocity_y = direction_y * speed * 0.85
def stop(self):
self.velocity_x = 0
self.velocity_y = 0
Разбор.
Ellipseв квадратном виджете выглядит как круг.uniform(-0.35, 0.35)— случайный небольшой горизонтальный компонент при подаче.direction_y— вниз к игроку (-1) или вверх к ИИ (+1) после гола.
Самопроверка
- Мяч отображается белым кругом.
Этап 3 — GameField
Цель. контейнер поля с фоном, центральной линией и тремя игровыми виджетами.
GameField(Widget) добавляет player, opponent, ball как дочерние элементы.
В canvas.before:
- тёмный
Rectangle— фон поля; - пунктирная
Lineпо центру по вертикали (разделение кортов).
В on_size пересчитывайте размеры от долей поля — адаптивность без жёстких пикселей:
paddle_w = min(self.width * 0.28, 220)
paddle_h = max(self.height * 0.025, 14)
ball_size = max(min(self.width, self.height) * 0.035, 16)
Позиции:
- игрок внизу —
y + height * 0.04; - соперник сверху —
top - paddle.height - height * 0.04; - мяч в центре при
reset_positions().
Разбор.
canvas.beforeрисуется под ракетками и мячом.center_line.pointsобновляйте в_update_backgroundпри ресайзе.- См. адаптивный дизайн — проценты вместо фиксированных px.
Самопроверка
- При изменении размера окна ракетки и мяч перестраиваются.
- Пунктир проходит по центру поля.
Этап 4 — игровой цикл
Цель. мяч движется плавно, ~60 кадров в секунду.
from kivy.clock import Clock
# в __init__ GameField:
Clock.schedule_interval(self.update, 1.0 / 60.0)
def update(self, dt):
if not self.running:
return
self._move_ball(dt)
def _move_ball(self, dt):
ball = self.ball
ball.x += ball.velocity_x * dt
ball.y += ball.velocity_y * dt
Разбор.
- В Pygame тот же приём —
ball.x += vx * dtпослеclock.tick()(практикум Pygame Pong). running = Falseостанавливает физику между подачами.start_round()вызываетball.serve()и ставитrunning = True.
Самопроверка
- После
start_round()мяч движется плавно.
Этап 5 — боковые стены
Цель. отскок от левого и правого края поля.
if ball.right >= self.right:
ball.x = self.right - ball.width
ball.velocity_x *= -1
elif ball.x <= self.x:
ball.x = self.x
ball.velocity_x *= -1
Разбор.
- Сначала выталкиваем мяч из стены (
ball.x = ...), иначе он "залипнет". - Инверсия
velocity_x— упругий отскок без потери скорости (пока без затухания).
Самопроверка
- Мяч отскакивает от левого и правого края.
Этап 6 — столкновение с ракеткой
Цель. мяч отскакивает от ракеток; угол зависит от точки удара.
AABB (axis-aligned bounding box)
def _collides_with_paddle(self, ball, paddle):
return (
ball.y < paddle.top
and ball.top > paddle.y
and ball.right > paddle.x
and ball.x < paddle.right
)
Прямоугольники пересекаются, если по обеим осям есть наложение.
Угол отскока
offset = (ball.center_x - paddle.center_x) / (paddle.width / 2)
offset = max(-1, min(1, offset))
speed = Vector(ball.velocity_x, ball.velocity_y).length()
speed = min(speed * 1.04, self.ball_speed * 1.6)
ball.velocity_x = speed * offset * 0.85
ball.velocity_y = speed * (1 if upward else -1)
ball.y = paddle.top if upward else paddle.y - ball.height
Разбор.
- Удар по краю ракетки (
offsetблизок к ±1) — сильный горизонтальный компонент. - Удар по центру — мяч летит почти вертикально.
- Коэффициент
1.04слегка ускоряет мяч;capне даёт бесконечно разогнаться. - После отскока выставляем
ball.y— мяч не остаётся внутри ракетки (иначе множественные столкновения).
Та же идея угла — в Pygame Pong, этап 9.
Самопроверка
- Мяч отскакивает от обеих ракеток.
- Удар по краю меняет траекторию сильнее, чем по центру.
Этап 7 — сенсорное управление
Цель. нижняя ракетка следует за пальцем.
def on_touch_down(self, touch):
if not self.collide_point(*touch.pos):
return super().on_touch_down(touch)
self.player_touch_active = True
self.player.x = self._clamp_paddle_x(
touch.x - self.player.width / 2, self.player
)
return True
def on_touch_move(self, touch):
if self.player_touch_active:
self.player.x = self._clamp_paddle_x(
touch.x - self.player.width / 2, self.player
)
return True
return super().on_touch_move(touch)
def on_touch_up(self, touch):
if self.player_touch_active:
self.player_touch_active = False
return True
_clamp_paddle_x ограничивает X ракетки границами поля.
Разбор.
return True— событие не уходит к дочерним виджетам; поле "владеет" жестом.- Центр ракетки под пальцем —
touch.x - width/2; так привычнее на телефоне. - На ПК работает зажатая мышь — удобно для отладки без устройства.
Свайп (короткий жест) здесь не нужен — важен drag (типы ввода в мобильных играх).
Самопроверка
- Ракетка следует за мышью при зажатой кнопке.
- Ракетка не выходит за края поля.
Этап 8 — счёт, ИИ, победа
Цель. голы, подача после паузы, победа до 7, overlay перезапуска.
ИИ соперника
Плавное движение к проекции мяча на X ракетки:
def _move_ai(self, dt):
target_x = self.ball.center_x - self.opponent.width / 2
diff = target_x - self.opponent.x
step = self.ai_speed * dt
# двигаться не больше step за кадр — не телепорт
ai_speed чуть меньше реакции идеального игрока — матч остаётся выигрываемым.
Гол
- мяч ниже нижней ракетки → очко сопернику;
- выше верхней → очко игроку;
running = False,Clock.schedule_once(..., 0.8)→start_round(direction_y=...).
UI счёта
В PingPongApp.build():
- заголовок
You: N/CPU: N; self.field.player.bind(score=self._update_score_labels);- при 7 очках — overlay
Label"You win!" / "CPU wins!" и "Tap to play again"; - тап по overlay сбрасывает счёт и вызывает
start_round().
Самопроверка
- Счёт увеличивается при пропуске мяча.
- Партия заканчивается при 7 очках.
- После тапа по overlay начинается новая партия.
Этап 9 — ревизия
Цель. финальная проверка и переход к Snake.
Сверка с суммой листингов этапов 0–8.
Управление
| Ввод | Действие |
|---|---|
| Перетаскивание по полю | Движение нижней ракетки |
| Тап на overlay | Новая партия после победы |
Итоговая самопроверка
- ИИ отбивает большинство мячей, но не непобедим.
- Скорость мяча слегка растёт после отскока (1.04, с ограничением).
- Весь код в одном
main.py— для учебного проекта нормально; в Pygame Pong к этапу 14 выносят пакетgame/.
Дальше — Kivy — Snake.