Пишем игру на Python
vk f t

Пишем игру на Python

Без дона­тов!

Преж­де чем мы нач­нём про­грам­ми­ро­вать что-то полез­ное на Python, давай­те зако­дим что-нибудь инте­рес­ное. Напри­мер, свою игру, где нуж­но не дать шари­ку упасть, типа Арка­но­и­да. Вы, ско­рее все­го, игра­ли в дет­стве во что-то подоб­ное, поэто­му осво­ить­ся будет про­сто.

Логика игры

Есть игро­вое поле — про­стой пря­мо­уголь­ник с твёр­ды­ми гра­ни­ца­ми. Когда шарик каса­ет­ся стен­ки или потол­ка, он отска­ки­ва­ет в дру­гую сто­ро­ну. Если он упа­дёт на пол — вы про­иг­ра­ли. Что­бы это­го не слу­чи­лось, вни­зу вдоль пола лета­ет плат­фор­ма, а вы ей управ­ля­е­те с помо­щью стре­лок. Ваша зада­ча — под­став­лять плат­фор­му под шарик как мож­но доль­ше. За каж­дое удач­ное спа­се­ние шари­ка вы полу­ча­е­те одно очко.

Алгоритм

Что­бы реа­ли­зо­вать такую логи­ку игры, нуж­но преду­смот­реть такие сце­на­рии пове­де­ния:

Хит­рость в том, что всё это про­ис­хо­дит парал­лель­но и неза­ви­си­мо друг от дру­га. То есть пока шарик лета­ет, мы вполне можем дви­гать плат­фор­му, а можем и оста­вить её на месте. И когда шарик отска­ки­ва­ет от стен, это тоже не меша­ет дру­гим объ­ек­там дви­гать­ся и вза­и­мо­дей­ство­вать меж­ду собой.

Полу­ча­ет­ся, что нам нуж­но опре­де­лить три клас­са — плат­фор­му, сам шарик и счёт, и опре­де­лить, как они реа­ги­ру­ют на дей­ствия друг дру­га. Поле нам самим опре­де­лять не нуж­но — для это­го есть уже гото­вая биб­лио­те­ка. А потом в этих клас­сах мы про­пи­шем мето­ды — они как раз и будут отве­чать за пове­де­ние наших объ­ек­тов.

Весь кайф в том, что мы всё это зада­ём один раз, а потом объ­ек­ты сами раз­би­ра­ют­ся, как им реа­ги­ро­вать друг на дру­га и что делать в раз­ных ситу­а­ци­ях. Мы не про­пи­сы­ва­ем жёст­ко весь алго­ритм, а зада­ём пра­ви­ла игры — а для это­го клас­сы под­хо­дят про­сто иде­аль­но.

По коням, пишем на Python

Для это­го про­ек­та вам потре­бу­ет­ся уста­но­вить и запу­стить сре­ду Python. Как это сде­лать — читай­те в нашей ста­тье.

Начало программы

Что­бы у нас появи­лась гра­фи­ка в игре, исполь­зу­ем биб­лио­те­ку Tkinter. Она вхо­дит в набор стан­дарт­ных биб­лио­тек Python и поз­во­ля­ет рисо­вать про­стей­шие объ­ек­ты — линии, пря­мо­уголь­ни­ки, кру­ги и кра­сить их в раз­ные цве­та. Такой про­стой Paint, толь­ко для Python.

Что­бы создать окно, где будет вид­на гра­фи­ка, исполь­зу­ют класс Tk(). Он про­сто дела­ет окно, но без содер­жи­мо­го. Что­бы появи­лось содер­жи­мое, созда­ют холст — види­мую часть окна. Имен­но на нём мы будем рисо­вать нашу игру. За холст отве­ча­ет класс Canvas(), поэто­му нам нуж­но будет создать свой объ­ект из это­го клас­са и даль­ше уже рабо­тать с этим объ­ек­том.

Если мы при­ну­ди­тель­но не огра­ни­чим ско­рость плат­фор­мы, то она будет пере­ме­щать­ся мгно­вен­но, ведь ком­пью­тер счи­та­ет очень быст­ро и момен­таль­но пере­дви­нет её к дру­го­му краю. Поэто­му мы будем искус­ствен­но огра­ни­чи­вать вре­мя дви­же­ния, а для это­го нам пона­до­бит­ся модуль Time — он тоже стан­дарт­ный.

Послед­нее, что нам гло­баль­но нуж­но, — зада­вать слу­чай­ным обра­зом началь­ное поло­же­ние шари­ка и плат­фор­мы, что­бы было инте­рес­нее играть. За это отве­ча­ет модуль Random — он помо­га­ет гене­ри­ро­вать слу­чай­ные чис­ла и пере­ме­ши­вать дан­ные.

Запи­шем всё это в виде кода на Python:

    
language: Python 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()


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Мы под­клю­чи­ли все нуж­ные биб­лио­те­ки, сде­ла­ли и настро­и­ли игро­вое поле. Теперь зай­мём­ся клас­са­ми.

Шарик

Сна­ча­ла про­го­во­рим сло­ва­ми, что нам нуж­но от шари­ка. Он дол­жен уметь:

Это­го доста­точ­но, что­бы шарик жил сво­ей жиз­нью и умел вза­и­мо­дей­ство­вать с окру­жа­ю­щей сре­дой. При этом нуж­но не забыть о том, что каж­дый класс дол­жен содер­жать кон­струк­тор — код, кото­рый отве­ча­ет за созда­ние ново­го объ­ек­та. Без это­го сде­лать шарик не полу­чит­ся. Запи­шем это на Python:

    
language: Python 3
# Описываем класс 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


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Платформа

Сде­ла­ем то же самое для плат­фор­мы — сна­ча­ла опи­шем её пове­де­ние сло­ва­ми, а потом пере­ве­дём в код. Итак, вот что долж­на уметь плат­фор­ма:

А вот как это будет в виде кода:

    
language: Python 3
#  Описываем класс 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


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Счёт

Мож­но было не выде­лять счёт в отдель­ный класс и каж­дый раз обра­ба­ты­вать вруч­ную. Но здесь реаль­но про­ще сде­лать класс, задать нуж­ные мето­ды, что­бы они сами потом разо­бра­лись, что и когда делать.

От счё­та нам нуж­но толь­ко одно (кро­ме кон­струк­то­ра) — что­бы он пра­виль­но реа­ги­ро­вал на каса­ние плат­фор­мы, уве­ли­чи­вал чис­ло очков и выво­дил их на экран:

    
language: Python 3
#  Описываем класс 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)


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Игра

У нас всё гото­во для того, что­бы напи­сать саму игру. Мы уже про­ве­ли необ­хо­ди­мую под­го­тов­ку всех эле­мен­тов, и нам оста­ёт­ся толь­ко создать кон­крет­ные объ­ек­ты шари­ка, плат­фор­мы и счё­та и ска­зать им, в каком поряд­ке мы будем что делать.

Смысл игры в том, что­бы не уро­нить шарик. Пока это­го не про­изо­шло — всё дви­жет­ся, но как толь­ко шарик упал — нуж­но пока­зать сооб­ще­ние о кон­це игры и оста­но­вить про­грам­му.

Посмот­ри­те, как лако­нич­но выгля­дит код непо­сред­ствен­но самой игры:

    
language: Python 3
# создаём объект — зелёный счёт 

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)


Ско­пи­ро­вать код
Код ско­пи­ро­ван
ПОЛНЫЙ КОД ПРОГРАММЫ

    
language: Python 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)


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Что дальше

На осно­ве это­го кода вы може­те сде­лать свою моди­фи­ка­цию игры:

Ещё по теме