2025, Oct 31 06:47

Как отличить боковое и верхнее попадание мяча о ракетку в Python turtle

Как корректно определить, куда попал мяч в Python turtle: бок или верх ракетки. Сравним смещения с учетом пропорций и инверсия скорости надежные отражения.

Определить, попал ли мяч в боковую грань ракетки или в её верх, кажется простым — пока не начнёшь кодить. Наивная проверка равенства прямо на границе редко ведёт себя так, как ожидается, при быстром непрерывном движении. Ниже — краткий разбор этой ситуации на примере мини‑игры на Python turtle и аккуратный способ настроить отклик на столкновения так, как вам нужно.

Постановка задачи

Цель — отражать движущийся мяч от ракетки по‑разному в зависимости от места контакта. Попадание в бок должно менять знак скорости по x, а удар в верх или низ — менять знак скорости по y.

from turtle import Turtle, Screen
import time
import random
# --------------- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ --------------------
vx = 20
vy = 20
tiles = []
def detect_hit():
    global vx, vy, tiles
    if orb.xcor() > (canvas.window_width() / 2) - 20 or orb.xcor() < -(canvas.window_width() / 2) + 20:
        vx *= -1
    elif orb.ycor() > (canvas.window_height() / 2) - 20 or orb.ycor() < -(canvas.window_height() / 2) + 20:
        vy *= -1
    elif abs(bar.xcor() - orb.xcor()) < 20/2 + 150/2 and abs(bar.ycor() - orb.ycor()) < 20/2 + 60/2:
        if abs(bar.xcor() - orb.xcor()) == 20/2 + 150/2:
            vx *= -1
        else:
            vy *= -1
def tick():
    orb.goto(orb.xcor() + vx, orb.ycor() + vy)
    detect_hit()
    canvas.update()
    canvas.ontimer(tick, 1)
def nudge_right():
    bar.setx(bar.xcor() + 25)
def nudge_left():
    bar.setx(bar.xcor() - 25)
canvas = Screen()
time.sleep(2)
orb = Turtle(shape='circle')
orb.pu()
orb.speed(0)
orb.goto(0, -300)
orb.speed(0)
orb.left(random.randint(45, 135))
bar = Turtle(shape='square')
bar.speed(0)
bar.goto(0, -400)
bar.shapesize(1, 7.5, 3)
bar.up()
canvas.listen()
canvas.onkeypress(nudge_right, 'Right')
canvas.onkeypress(nudge_left, 'Left')
canvas.onkeypress(nudge_right, 'd')
canvas.onkeypress(nudge_left, 'a')
tick()
canvas.mainloop()

Почему исходный подход ошибочно классифицирует попадания

Решение «бок против верха» выше опирается на проверку, равняется ли горизонтальное смещение заданной граничной сумме. В движении такая проверка равенства ненадёжна, особенно когда ракетка значительно вытянута по одной из осей. В этом примере ракетка растянута с соотношением 1 к 7.5, поэтому простое сравнение немасштабированных расстояний не отражает её форму.

Реально помогает сравнивать, насколько мяч смещён по каждой оси с учётом пропорций ракетки. При столкновении сверху/снизу доминирует вертикальное смещение; при боковом — горизонтальное. Ключ — включить соотношение сторон ракетки в это сравнение.

Надёжная проверка «бок vs верх» с весами по осям

Практическое правило простое. Сравните абсолютные смещения по x и y после масштабирования их растяжением ракетки. Если взвешенное расстояние по x больше — считаем, что удар пришёлся в бок и инвертируем скорость по x; иначе это верх/низ и инвертируем y. Здесь напрямую используется соотношение 1:7.5.

if abs(bar.xcor() - orb.xcor()) * 1 > abs(bar.ycor() - orb.ycor()) * 7.5:
    vx *= -1
else:
    vy *= -1

Иными словами, чтобы засчитать верх или низ, вертикальное смещение должно быть пропорционально больше. Поскольку ракетка значительно длиннее по горизонтали, компоненту y нужно взвесить коэффициентом 7.5, чтобы сопоставить её с x.

Исправленная ветка обработки столкновения в контексте

Подставьте пропорциональное сравнение в блок коллизий, остальное оставьте как есть. Проверка перекрытия остаётся прежней; меняется только способ выбора оси, которую инвертировать после обнаружения перекрытия.

def detect_hit():
    global vx, vy, tiles
    if orb.xcor() > (canvas.window_width() / 2) - 20 or orb.xcor() < -(canvas.window_width() / 2) + 20:
        vx *= -1
    elif orb.ycor() > (canvas.window_height() / 2) - 20 or orb.ycor() < -(canvas.window_height() / 2) + 20:
        vy *= -1
    elif abs(bar.xcor() - orb.xcor()) < 20/2 + 150/2 and abs(bar.ycor() - orb.ycor()) < 20/2 + 60/2:
        if abs(bar.xcor()) or abs(orb.xcor()):  # заглушка — подчёркиваем, что изменилось только решение
            pass
        # Выбираем ось отражения, сравнивая взвешенные расстояния
        if abs(bar.xcor() - orb.xcor()) * 1 > abs(bar.ycor() - orb.ycor()) * 7.5:
            vx *= -1
        else:
            vy *= -1

Суть — именно в пропорциональном сравнении. Конкретные веса 1 и 7.5 соответствуют текущему растяжению ракетки и могут быть скорректированы, если её размеры изменятся.

Почему это важно

Игроки сразу замечают, когда мяч отражается в неправильном направлении. Последовательный отклик на столкновения улучшает контроль и делает игру предсказуемой в хорошем смысле. Учитывая пропорции ракетки напрямую при выборе между боковым и верхним/нижним попаданием, вы избегаете ложных классификаций без усложнения остального цикла.

Итоги и практические рекомендации

Фиксируйте контакт через проверку перекрытия, а затем выбирайте ось отражения сравнением абсолютных смещений по x и y, масштабированных к соотношению сторон ракетки. Такая логика остаётся компактной и соответствует реальной форме на экране. Если позже измените растяжение ракетки, обновите веса, чтобы сохранить прежнее поведение. Также следите за обработкой ввода и частотой таймера: прямые триггеры по клавишам и слишком агрессивный таймер могут дать рваную анимацию, а подбор частоты, которую процессор стабильно выдерживает, помогает сгладить геймплей.

Материал основан на вопросе с StackOverflow от Aadvik и ответе от Aadvik.