Начало
В этой статье я хочу поделиться своим опытом с создании игры ping-pong на пайтон с использованием модуля turtle. Мне 14 лет и я только начинаю программировать, так что не судите строго)

Инициализируем библиотеки turtle и random (чтобы выбирать случайное направление мяча)
from turtle import *
from random import choice
Часть кода для закрытия окна без ошибки
Если честно эту часть кода я подсмотрел в интернете
scr = Screen()
canvas = scr.getcanvas()
root = canvas.winfo_toplevel()
scr.setup(600, 400)
scr.title('Ping-Pong')
scr.cv._rootwindow.resizable(False, False)
def on_close():
global running
running = False
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_close)
running = True

Эта часть программы нужна для правильного закрытия окна (далее будет бесконечный цикл поэтому это нужно)
Также здесь я настроил размеры окна и убрал увеличение/уменьшение окна
Настройка счёта
В этой части я обычной черепашкой (предварительно подняв её перо) перемещаюсь в верхнюю часть экрана и пишу там счёт 0-0
penup()
hideturtle()
setposition(0, 150)
write(f'Красный: 0 Синий: 0', font=("Arial", 20, "bold"), align="center")

Также я добавил список с направлениями в которых будет двигаться мяч
lst = [30, 60, 120, 150, 210, 240, 300, 330]
Создание класса-наследника Sprint
Не знаю почему именно Sprint но пускай будет
Этот класс наследует всё от класса Turtle(), а также я добавил туда свои свойства и методы
class Sprint(Turtle):
score = 0
def __init__(self, x, width, height, color_, shape="square"):
super().__init__(shape)
self.color(color_)
self.x = x
self.shapesize(width, height)
def change_settings(self):
self.penup()
self.speed(0)
self.setposition(self.x, 0)
def move_up(self):
x, y = self.pos()
if y <= 150:
self.setposition(x, y+20)
def move_down(self):
x, y = self.pos()
if y >= -140:
self.setposition(x, y-20)
В этот класс первым делом я добавляю свойство score для счётчика очков
Потом инициализирую следующие свойства - x, width, height, color_, shape
shape я сразу передаю супер-классу
Методы:
-
Настройки всего класса
-
Двигаться вверх
-
Двигаться вниз
В методе change_settings я просто даю настройки черепашки - поднимаю перо, устанавливаю скорость 0 (чтобы не было анимации) и устанавливаю позицию черепашки
Методы move_up и move_down говорят сами за себя. В них я распаковкой получай значения x и y черепашки и проверяю: если она не выходит за границы, то двигаю её на 20 пикселей вверх или вниз
Создание класса-наследника Ball
Также класс-наследник от turtle со своими свойствами и методами
class Ball(Turtle):
def __init__(self, shape="circle"):
super().__init__(shape)
def change_settings(self):
self.penup()
self.speed(0)
self.shapesize(0.7)
self.setheading(choice(lst))
Также супер классу передаю форму и такой-же метод change_settings
В методе change_settings я: поднимаю перо, устанавливаю скорость 0 (чтобы убрать анимацию), устанавливаю размер мяча 0.7 (я посчитал этот размер оптимальным), и выбираю случайное направление мяча
Основная часть программы
Вот мы и подошли к методу класса Ball в котором и будет твориться вся дичь
def go(self, enemy1, enemy2):
while running:
if not running:
break
self.forward(2.5)
if self.ycor() >= 190:
self.setheading(360 - self.heading())
elif self.ycor() <= -180:
self.setheading(360 - self.heading())
if self.xcor() < -300:
self.setposition(0, 0)
self.setheading(choice(lst))
enemy2.score += 1
clear()
write(f'Красный: {enemy1.score} Синий: {enemy2.score}', font=("Arial", 20, "bold"), align="center")
elif self.xcor() > 300:
self.setposition(0, 0)
self.setheading(choice(lst))
enemy1.score += 1
clear()
write(f'Красный: {enemy1.score} Синий: {enemy2.score}', font=("Arial", 20, "bold"), align="center")
if -179 > self.xcor() > -181 and abs(self.ycor() - enemy1.ycor()) <= 50:
self.setheading(180 - self.heading())
self.forward(2)
if 179 < self.xcor() < 181 and abs(self.ycor() - enemy2.ycor()) <= 50:
self.setheading(180 - self.heading())
self.forward(2)
Давайте я буду рассказывать всё по частям
1-4 строка:
Объявление метода go с двумя параметрами (это наши черепашки)
Запуск бесконечного цикла и вместо while True я пишу while running, помните первую часть кода? Так вот. В ней проверяется не закрылось ли окно и если так то выполняется функция on_close, где переменная running объявляется глобальной и ей присваивается значение False, тем самым завершая цикл (также в той функции происходит закрытие окна).
Пятая строка нужна чтобы наш мяч двигался
6-9 строка:
Проверяется не вышил ли мяч за границы по y и если это так то зеркально отражает шарик (меняет его направление)
11-22 строка:
В этой части кода происходит проверка на выход мяча за границы по x. Если это так то мяч перемещается на координаты 0, 0 и заново выбирается направление
Также здесь происходит подсчёт очков. Если мяч вышел за границы красной платформы то добавляется очко синей и наоборот. Потом обычная черепашка стирает старую информацию про счёт и печатает обновлённую
24-30 строка:
Тут происходит проверка соприкосновения мяча с платформой
Как это примерно работает:
Сначала проверяется не дошёл ли мяч до платформы (а именно до координаты -180 или 180). Затем я проверяю что координаты y совпадают у обоих элементов, выглядит это примерно вот так:
abs(self.ycor() - enemy1.ycor()) <= 50
Тут из координаты y мяча вычитается координата y платформы и если эти значения по модулю не превышают 50, то значит платформа и мяч находятся в соприкосновении
Если условие выполняется и меняю направление мяча на противоположное и сразу иду вперёд на 2 пикселя, чтобы не было повторного выполнения условия
Создание экземпляров класса Sprint
t1 = Sprint(-200, 4, 1, 'red')
t1.change_settings()
t2 = Sprint(200, 4, 1, 'blue')
t2.change_settings()
Создаю 2 экземпляра передавая каждому свои аргументы и сразу выполняю метод change_settings()
Создание экземпляра класса Ball
ball = Ball()
ball.change_settings()
В этот класс мне не нужно передавать аргументов. Также сразу запускаю метод change_settings()
Настройка обработки нажатий
Начинаю считывать нажатия с помощью scr.listen()
scr.listen()
onkey(t1.move_up, 'w')
onkey(t1.move_down, 's')
onkey(t2.move_up, 'Up')
onkey(t2.move_down, 'Down')
На клавиши w, s у нас реагирует красная платформа, а на стрелочки вверх и вниз реагирует синяя платформа
Запуск метода go
Запускаем метод класса Ball передав два аргумента в виде черепашек и наслаждаемся игрой)
ball.go(t1, t2)
Полный код:
from turtle import *
from random import choice
from time import sleep
scr = Screen()
canvas = scr.getcanvas()
root = canvas.winfo_toplevel()
scr.setup(600, 400)
scr.title('Ping-Pong')
scr.cv._rootwindow.resizable(False, False)
def on_close():
global running
running = False
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_close)
running = True
penup()
hideturtle()
setposition(0, 150)
write(f'Красный: 0 Синий: 0', font=("Arial", 20, "bold"), align="center")
lst = [30, 60, 120, 150, 210, 240, 300, 330]
class Sprint(Turtle):
score = 0
def __init__(self, x, width, height, color_, shape="square"):
super().__init__(shape)
self.color(color_)
self.x = x
self.shapesize(width, height)
def change_settings(self):
self.penup()
self.speed(0)
self.setposition(self.x, 0)
def move_up(self):
x, y = self.pos()
if y <= 150:
self.setposition(x, y+20)
def move_down(self):
x, y = self.pos()
if y >= -140:
self.setposition(x, y-20)
class Ball(Turtle):
def __init__(self, shape="circle"):
super().__init__(shape)
def change_settings(self):
self.penup()
self.speed(0)
self.shapesize(0.7)
self.setheading(choice(lst))
def go(self, enemy1, enemy2):
while running:
self.forward(2.5)
if self.ycor() >= 190:
self.setheading(360 - self.heading())
elif self.ycor() <= -180:
self.setheading(360 - self.heading())
if self.xcor() < -300:
self.setposition(0, 0)
self.setheading(choice(lst))
enemy2.score += 1
clear()
write(f'Красный: {enemy1.score} Синий: {enemy2.score}', font=("Arial", 20, "bold"), align="center")
elif self.xcor() > 300:
self.setposition(0, 0)
self.setheading(choice(lst))
enemy1.score += 1
clear()
write(f'Красный: {enemy1.score} Синий: {enemy2.score}', font=("Arial", 20, "bold"), align="center")
if -179 > self.xcor() > -181 and abs(self.ycor() - enemy1.ycor()) <= 50:
self.setheading(180 - self.heading())
self.forward(2)
if 179 < self.xcor() < 181 and abs(self.ycor() - enemy2.ycor()) <= 50:
self.setheading(180 - self.heading())
self.forward(2)
t1 = Sprint(-200, 4, 1, 'red')
t1.change_settings()
t2 = Sprint(200, 4, 1, 'blue')
t2.change_settings()
ball = Ball()
ball.change_settings()
scr.listen()
onkey(t1.move_up, 'w')
onkey(t1.move_down, 's')
onkey(t2.move_up, 'Up')
onkey(t2.move_down, 'Down')
ball.go(t1, t2)
Заранее извиняюсь за возможные некорректные формулировки
Автор: Eralv1lle