Прежде чем мы начнём программировать что-то полезное на Python, давайте закодим что-нибудь интересное. Например, свою игру, где нужно не дать шарику упасть, типа Арканоида. Вы, скорее всего, играли в детстве во что-то подобное, поэтому освоиться будет просто.
Логика игры
Есть игровое поле — простой прямоугольник с твёрдыми границами. Когда шарик касается стенки или потолка, он отскакивает в другую сторону. Если он упадёт на пол — вы проиграли. Чтобы этого не случилось, внизу вдоль пола летает платформа, а вы ей управляете с помощью стрелок. Ваша задача — подставлять платформу под шарик как можно дольше. За каждое удачное спасение шарика вы получаете одно очко.
Алгоритм
Чтобы реализовать такую логику игры, нужно предусмотреть такие сценарии поведения:
- игра начинается;
- шарик начинает двигаться;
- если нажаты стрелки влево или вправо — двигаем платформу;
- если шарик коснулся стенок, потолка или платформы — делаем отскок;
- если шарик коснулся платформы — увеличиваем счёт на единицу;
- если шарик упал на пол — выводим сообщение и заканчиваем игру.
Хитрость в том, что всё это происходит параллельно и независимо друг от друга. То есть пока шарик летает, мы вполне можем двигать платформу, а можем и оставить её на месте. И когда шарик отскакивает от стен, это тоже не мешает другим объектам двигаться и взаимодействовать между собой.
Получается, что нам нужно определить три класса — платформу, сам шарик и счёт, и определить, как они реагируют на действия друг друга. Поле нам самим определять не нужно — для этого есть уже готовая библиотека. А потом в этих классах мы пропишем методы — они как раз и будут отвечать за поведение наших объектов.
Весь кайф в том, что мы всё это задаём один раз, а потом объекты сами разбираются, как им реагировать друг на друга и что делать в разных ситуациях. Мы не прописываем жёстко весь алгоритм, а задаём правила игры — а для этого классы подходят просто идеально.
По коням, пишем на Python
Для этого проекта вам потребуется установить и запустить среду Python. Как это сделать — читайте в нашей статье.
Начало программы
Чтобы у нас появилась графика в игре, используем библиотеку Tkinter. Она входит в набор стандартных библиотек Python и позволяет рисовать простейшие объекты — линии, прямоугольники, круги и красить их в разные цвета. Такой простой Paint, только для Python.
Чтобы создать окно, где будет видна графика, используют класс Tk(). Он просто делает окно, но без содержимого. Чтобы появилось содержимое, создают холст — видимую часть окна. Именно на нём мы будем рисовать нашу игру. За холст отвечает класс Canvas(), поэтому нам нужно будет создать свой объект из этого класса и дальше уже работать с этим объектом.
Если мы принудительно не ограничим скорость платформы, то она будет перемещаться мгновенно, ведь компьютер считает очень быстро и моментально передвинет её к другому краю. Поэтому мы будем искусственно ограничивать время движения, а для этого нам понадобится модуль Time — он тоже стандартный.
Последнее, что нам глобально нужно, — задавать случайным образом начальное положение шарика и платформы, чтобы было интереснее играть. За это отвечает модуль Random — он помогает генерировать случайные числа и перемешивать данные.
Запишем всё это в виде кода на Python:
# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
import random
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Game')
# запрещаем менять размеры окна, для этого используем свойство resizable
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты
canvas.pack()
# обновляем окно с холстом
tk.update()
Мы подключили все нужные библиотеки, сделали и настроили игровое поле. Теперь займёмся классами.
Шарик
Сначала проговорим словами, что нам нужно от шарика. Он должен уметь:
- задавать своё начальное положение и направление движение;
- понимать, когда он коснулся платформы;
- рисовать сам себя и понимать, когда нужно отрисовать себя в новом положении (например, после отскока от стены).
Этого достаточно, чтобы шарик жил своей жизнью и умел взаимодействовать с окружающей средой. При этом нужно не забыть о том, что каждый класс должен содержать конструктор — код, который отвечает за создание нового объекта. Без этого сделать шарик не получится. Запишем это на Python:
# Описываем класс Ball, который будет отвечать за шарик
class Ball:
# конструктор — он вызывается в момент создания нового объекта на основе этого класса
def __init__(self, canvas, paddle, score, color):
# задаём параметры объекта, которые нам передают в скобках в момент создания
self.canvas = canvas
self.paddle = paddle
self.score = score
# цвет нужен был для того, чтобы мы им закрасили весь шарик
# здесь появляется новое свойство id, в котором хранится внутреннее название шарика
# а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
self.id = canvas.create_oval(10,10, 25, 25, fill=color)
# помещаем шарик в точку с координатами 245,100
self.canvas.move(self.id, 245, 100)
# задаём список возможных направлений для старта
starts = [-2, -1, 1, 2]
# перемешиваем его
random.shuffle(starts)
# выбираем первый из перемешанного — это будет вектор движения шарика
self.x = starts[0]
# в самом начале он всегда падает вниз, поэтому уменьшаем значение по оси y
self.y = -2
# шарик узнаёт свою высоту и ширину
self.canvas_height = self.canvas.winfo_height()
self.canvas_width = self.canvas.winfo_width()
# свойство, которое отвечает за то, достиг шарик дна или нет. Пока не достиг, значение будет False
self.hit_bottom = False
# обрабатываем касание платформы, для этого получаем 4 координаты шарика в переменной pos (левая верхняя и правая нижняя точки)
def hit_paddle(self, pos):
# получаем кординаты платформы через объект paddle (платформа)
paddle_pos = self.canvas.coords(self.paddle.id)
# если координаты касания совпадают с координатами платформы
if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
# увеличиваем счёт (обработчик этого события будет описан ниже)
self.score.hit()
# возвращаем метку о том, что мы успешно коснулись
return True
# возвращаем False — касания не было
return False
# обрабатываем отрисовку шарика
def draw(self):
# передвигаем шарик на заданные координаты x и y
self.canvas.move(self.id, self.x, self.y)
# запоминаем новые координаты шарика
pos = self.canvas.coords(self.id)
# если шарик падает сверху
if pos[1] <= 0:
# задаём падение на следующем шаге = 2
self.y = 2
# если шарик правым нижним углом коснулся дна
if pos[3] >= self.canvas_height:
# помечаем это в отдельной переменной
self.hit_bottom = True
# выводим сообщение и количество очков
canvas.create_text(250, 120, text='Вы проиграли', font=('Courier', 30), fill='red')
# если было касание платформы
if self.hit_paddle(pos) == True:
# отправляем шарик наверх
self.y = -2
# если коснулись левой стенки
if pos[0] <= 0:
# движемся вправо
self.x = 2
# если коснулись правой стенки
if pos[2] >= self.canvas_width:
# движемся влево
self.x = -2
Платформа
Сделаем то же самое для платформы — сначала опишем её поведение словами, а потом переведём в код. Итак, вот что должна уметь платформа:
- двигаться влево или вправо в зависимости от нажатой стрелки;
- понимать, когда игра началась и можно двигаться.
А вот как это будет в виде кода:
# Описываем класс Paddle, который отвечает за платформы
class Paddle:
# конструктор
def __init__(self, canvas, color):
# canvas означает, что платформа будет нарисована на нашем изначальном холсте
self.canvas = canvas
# создаём прямоугольную платформу 10 на 100 пикселей, закрашиваем выбранным цветом и получаем её внутреннее имя
self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
# задаём список возможных стартовых положений платформы
start_1 = [40, 60, 90, 120, 150, 180, 200]
# перемешиваем их
random.shuffle(start_1)
# выбираем первое из перемешанных
self.starting_point_x = start_1[0]
# перемещаем платформу в стартовое положение
self.canvas.move(self.id, self.starting_point_x, 300)
# пока платформа никуда не движется, поэтому изменений по оси х нет
self.x = 0
# платформа узнаёт свою ширину
self.canvas_width = self.canvas.winfo_width()
# задаём обработчик нажатий
# если нажата стрелка вправо — выполняется метод turn_right()
self.canvas.bind_all('<KeyPress-Right>', self.turn_right)
# если стрелка влево — turn_left()
self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
# пока игра не началась, поэтому ждём
self.started = False
# как только игрок нажмёт Enter — всё стартует
self.canvas.bind_all('<KeyPress-Return>', self.start_game)
# движемся вправо
def turn_right(self, event):
# будем смещаться правее на 2 пикселя по оси х
self.x = 2
# движемся влево
def turn_left(self, event):
# будем смещаться левее на 2 пикселя по оси х
self.x = -2
# игра начинается
def start_game(self, event):
# меняем значение переменной, которая отвечает за старт
self.started = True
# метод, который отвечает за движение платформы
def draw(self):
# сдвигаем нашу платформу на заданное количество пикселей
self.canvas.move(self.id, self.x, 0)
# получаем координаты холста
pos = self.canvas.coords(self.id)
# если мы упёрлись в левую границу
if pos[0] <= 0:
# останавливаемся
self.x = 0
# если упёрлись в правую границу
elif pos[2] >= self.canvas_width:
# останавливаемся
self.x = 0
Счёт
Можно было не выделять счёт в отдельный класс и каждый раз обрабатывать вручную. Но здесь реально проще сделать класс, задать нужные методы, чтобы они сами потом разобрались, что и когда делать.
От счёта нам нужно только одно (кроме конструктора) — чтобы он правильно реагировал на касание платформы, увеличивал число очков и выводил их на экран:
# Описываем класс Score, который отвечает за отображение счетов
class Score:
# конструктор
def __init__(self, canvas, color):
# в самом начале счёт равен нулю
self.score = 0
# будем использовать наш холст
self.canvas = canvas
# создаём надпись, которая показывает текущий счёт, делаем его нужно цвета и запоминаем внутреннее имя этой надписи
self.id = canvas.create_text(450, 10, text=self.score, font=('Courier', 15), fill=color)
# обрабатываем касание платформы
def hit(self):
# увеличиваем счёт на единицу
self.score += 1
# пишем новое значение счёта
self.canvas.itemconfig(self.id, text=self.score)
Игра
У нас всё готово для того, чтобы написать саму игру. Мы уже провели необходимую подготовку всех элементов, и нам остаётся только создать конкретные объекты шарика, платформы и счёта и сказать им, в каком порядке мы будем что делать.
Смысл игры в том, чтобы не уронить шарик. Пока этого не произошло — всё движется, но как только шарик упал — нужно показать сообщение о конце игры и остановить программу.
Посмотрите, как лаконично выглядит код непосредственно самой игры:
# создаём объект — зелёный счёт
score = Score(canvas, 'green')
# создаём объект — белую платформу
paddle = Paddle(canvas, 'White')
# создаём объект — красный шарик
ball = Ball(canvas, paddle, score, 'red')
# пока шарик не коснулся дна
while not ball.hit_bottom:
# если игра началась и платформа может двигаться
if paddle.started == True:
# двигаем шарик
ball.draw()
# двигаем платформу
paddle.draw()
# обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
tk.update_idletasks()
# обновляем игровое поле, и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
tk.update()
# замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
time.sleep(0.01)
# если программа дошла досюда, значит, шарик коснулся дна. Ждём 3 секунды, пока игрок прочитает финальную надпись, и завершаем игру
time.sleep(3)
# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
import random
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Game')
# запрещаем менять размеры окна, для этого используем свойство resizable
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить. Попробуйте :)
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты
canvas.pack()
# обновляем окно с холстом
tk.update()
# Описываем класс Ball, который будет отвечать за шарик
class Ball:
# конструктор — он вызывается в момент создания нового объекта на основе этого класса
def __init__(self, canvas, paddle, score, color):
# задаём параметры объекта, которые нам передают в скобках в момент создания
self.canvas = canvas
self.paddle = paddle
self.score = score
# цвет нужен был для того, чтобы мы им закрасили весь шарик
# здесь появляется новое свойство id, в котором хранится внутреннее название шарика
# а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
self.id = canvas.create_oval(10,10, 25, 25, fill=color)
# помещаем шарик в точку с координатами 245,100
self.canvas.move(self.id, 245, 100)
# задаём список возможных направлений для старта
starts = [-2, -1, 1, 2]
# перемешиваем его
random.shuffle(starts)
# выбираем первый из перемешанного — это будет вектор движения шарика
self.x = starts[0]
# в самом начале он всегда падает вниз, поэтому уменьшаем значение по оси y
self.y = -2
# шарик узнаёт свою высоту и ширину
self.canvas_height = self.canvas.winfo_height()
self.canvas_width = self.canvas.winfo_width()
# свойство, которое отвечает за то, достиг шарик дна или нет. Пока не достиг, значение будет False
self.hit_bottom = False
# обрабатываем касание платформы, для этого получаем 4 координаты шарика в переменной pos (левая верхняя и правая нижняя точки)
def hit_paddle(self, pos):
# получаем кординаты платформы через объект paddle (платформа)
paddle_pos = self.canvas.coords(self.paddle.id)
# если координаты касания совпадают с координатами платформы
if pos[2] >= paddle_pos[0] and pos[0] <= paddle_pos[2]:
if pos[3] >= paddle_pos[1] and pos[3] <= paddle_pos[3]:
# увеличиваем счёт (обработчик этого события будет описан ниже)
self.score.hit()
# возвращаем метку о том, что мы успешно коснулись
return True
# возвращаем False — касания не было
return False
# метод, который отвечает за движение шарика
def draw(self):
# передвигаем шарик на заданный вектор x и y
self.canvas.move(self.id, self.x, self.y)
# запоминаем новые координаты шарика
pos = self.canvas.coords(self.id)
# если шарик падает сверху
if pos[1] <= 0:
# задаём падение на следующем шаге = 2
self.y = 2
# если шарик правым нижним углом коснулся дна
if pos[3] >= self.canvas_height:
# помечаем это в отдельной переменной
self.hit_bottom = True
# выводим сообщение и количество очков
canvas.create_text(250, 120, text='Вы проиграли', font=('Courier', 30), fill='red')
# если было касание платформы
if self.hit_paddle(pos) == True:
# отправляем шарик наверх
self.y = -2
# если коснулись левой стенки
if pos[0] <= 0:
# движемся вправо
self.x = 2
# если коснулись правой стенки
if pos[2] >= self.canvas_width:
# движемся влево
self.x = -2
# Описываем класс Paddle, который отвечает за платформы
class Paddle:
# конструктор
def __init__(self, canvas, color):
# canvas означает, что платформа будет нарисована на нашем изначальном холсте
self.canvas = canvas
# создаём прямоугольную платформу 10 на 100 пикселей, закрашиваем выбранным цветом и получаем её внутреннее имя
self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
# задаём список возможных стартовых положений платформы
start_1 = [40, 60, 90, 120, 150, 180, 200]
# перемешиваем их
random.shuffle(start_1)
# выбираем первое из перемешанных
self.starting_point_x = start_1[0]
# перемещаем платформу в стартовое положение
self.canvas.move(self.id, self.starting_point_x, 300)
# пока платформа никуда не движется, поэтому изменений по оси х нет
self.x = 0
# платформа узнаёт свою ширину
self.canvas_width = self.canvas.winfo_width()
# задаём обработчик нажатий
# если нажата стрелка вправо — выполняется метод turn_right()
self.canvas.bind_all('<KeyPress-Right>', self.turn_right)
# если стрелка влево — turn_left()
self.canvas.bind_all('<KeyPress-Left>', self.turn_left)
# пока платформа не двигается, поэтому ждём
self.started = False
# как только игрок нажмёт Enter — всё стартует
self.canvas.bind_all('<KeyPress-Return>', self.start_game)
# движемся вправо
def turn_right(self, event):
# будем смещаться правее на 2 пикселя по оси х
self.x = 2
# движемся влево
def turn_left(self, event):
# будем смещаться левее на 2 пикселя по оси х
self.x = -2
# игра начинается
def start_game(self, event):
# меняем значение переменной, которая отвечает за старт движения платформы
self.started = True
# метод, который отвечает за движение платформы
def draw(self):
# сдвигаем нашу платформу на заданное количество пикселей
self.canvas.move(self.id, self.x, 0)
# получаем координаты холста
pos = self.canvas.coords(self.id)
# если мы упёрлись в левую границу
if pos[0] <= 0:
# останавливаемся
self.x = 0
# если упёрлись в правую границу
elif pos[2] >= self.canvas_width:
# останавливаемся
self.x = 0
# Описываем класс Score, который отвечает за отображение счетов
class Score:
# конструктор
def __init__(self, canvas, color):
# в самом начале счёт равен нулю
self.score = 0
# будем использовать наш холст
self.canvas = canvas
# создаём надпись, которая показывает текущий счёт, делаем его нужно цвета и запоминаем внутреннее имя этой надписи
self.id = canvas.create_text(450, 10, text=self.score, font=('Courier', 15), fill=color)
# обрабатываем касание платформы
def hit(self):
# увеличиваем счёт на единицу
self.score += 1
# пишем новое значение счёта
self.canvas.itemconfig(self.id, text=self.score)
# создаём объект — зелёный счёт
score = Score(canvas, 'green')
# создаём объект — белую платформу
paddle = Paddle(canvas, 'White')
# создаём объект — красный шарик
ball = Ball(canvas, paddle, score, 'red')
# пока шарик не коснулся дна
while not ball.hit_bottom:
# если игра началась и платформа может двигаться
if paddle.started == True:
# двигаем шарик
ball.draw()
# двигаем платформу
paddle.draw()
# обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
tk.update_idletasks()
# обновляем игровое поле и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
tk.update()
# замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
time.sleep(0.01)
# если программа дошла досюда, значит, шарик коснулся дна. Ждём 3 секунды, пока игрок прочитает финальную надпись, и завершаем игру
time.sleep(3)
Выводы
Это одна из самых простых игр, где уже можно ощутить всю мощь объектно-ориентированного программирования. Смотрите сами:
- мы сделали класс «шарик», который уже при создании получает нужные размеры и цвет;
- внутри этого класса мы прописываем логику поведения;
- эта логика умеет взаимодействовать с другими сущностями в коде, используя значения их каких-то свойств, например, координат и размеров площадки;
- для площадки мы также сделали новый класс со своей логикой — в ней уже есть всё, что нам нужно от площадки;
- когда есть классы, то на их основе мы можем создавать любое количество объектов, которые будут уметь делать то же самое, что и основной класс;
- благодаря ООП мы можем разделить внутренние значения с внешними параметрами, которые доступны извне.
А вот что ещё можно почитать по ООП:
Объектно-ориентированное программирование: на пальцах
ООП для новичков: классы и объекты
ООП для новичков: публичное и приватное
ООП: атрибуты и методы
Что дальше
На основе этого кода вы можете сделать свою модификацию игры:
- добавить второй шарик;
- раскрасить элементы в другой цвет;
- поменять размеры шарика; поменять скорость платформы;
- сделать всё это сразу;
- поменять логику программы на свою.