Перейти к основному содержимому

Kivy — Pong

Разработчику Начальный уровень

О практикуме

Pong в вертикальной ориентации — удобной для телефона: ваша ракетка внизу, соперник (ИИ) сверху, мяч отскакивает от боковых стен и ракеток. Первый до 7 очков побеждает. Управление — перетаскивание пальцем по полю (на ПК — зажатая мышь).

Чему учит этот практикум

  • Непрерывная физика — позиция мяча обновляется каждый кадр через Clock (обзор Kivy);
  • canvas — ракетки и мяч без PNG-спрайтов;
  • AABB-столкновения — прямоугольники пересекаются или нет;
  • NumericProperty — счёт обновляет Label автоматически;
  • адаптивные размерыon_size пересчитывает ракетки от долей поля.

Аналог на Pygame с клавиатурой и FSM — Python — Ping Pong. Там классический горизонтальный корт; здесь — мобильный вертикальный и тач вместо WASD.

Оценка времени — 2–4 часа.

Карта этапов

ЭтапФокусРезультат
0Каркас AppТёмное окно
1PaddleЦветная ракетка на canvas
2BallМяч с serve()
3GameFieldФон, пунктир, дочерние виджеты
4Движение мячаClock.schedule_interval
5СтеныОтскок от левого и правого края
6РакеткиAABB и угол отскока
7ТачРакетка следует за пальцем
8Счёт и ИИГол, подача, победа
9РевизияOverlay "Tap to play again"
Как читать

На каждом этапе — блок Разбор после кода. Застряли — сверьтесь с суммой листингов этапов 0–8 или перейдите к Snake после этапа 9.


Как проходить практикум

  1. Создайте папку kivypong/ и установите Kivy — см. зависимости.
  2. Весь код — в одном main.py; после каждого этапа запускайте python main.py.
  3. Читайте Разбор — там связь Clock, canvas и AABB с мобильным UX.
  4. Финал — этап 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.