Программируем игру про астероиды

Пузыри

Давай вернемся к примеру с одним шаром, который летает отталкиваясь от стен.

from tkinter import *

tk = Tk()
c = Canvas(tk, width=640, height=640, bg='white')
c.pack()

# выделяем скорость шарика в переменные
vx = 10
vy = 15

# сохраняем номер соданной фигуры в переменную
my_favorite_oval = c.create_oval(10, 10, 50, 50, fill='blue')

def moveBall():
    # используем переменные vx и vy, как глобальные
    global vx, vy

    # получаем текущие координаты овала
    x1, y1, x2, y2 = c.coords(my_favorite_oval)

    # если шарик на границе по x - развернуть скорость по x
    if x1 <= 0 or x2 >= 640:
        vx *= -1
    # аналогично по y
    if y1 <= 0 or y2 >= 640:
        vy *= -1

    # передвигаем наш овал на скорость
    c.move(my_favorite_oval, vx, vy)
    # повторяем через полсекунды
    c.after(50, moveBall)

# спустя полсекунды (50 мс) после запуска выполнить moveBall
c.after(50, moveBall)

mainloop()

Давай применим списки, чтобы сделать сразу 10 шаров. Что потребуется сделать?

  • Завести по список с шарами и списки для двух скоростей.
  • Заполнить списки 10 случайными числами.
  • Написать цикл для перемещения шаров.

Реагируем на нажатия клавиш

Для начала наш главный герой - это прямоугольник, который будет управляться с клавиатуры. Обработка нажатий клавиш работает по тому же принципу, что и кнопки - через дополнительную функцию. Мы можем связать нажатие клавиши на клавиатуре с вызовом нашей функции.

from tkinter import *

tk = Tk()
c = Canvas(tk, width=640, height=640, bg='white')
c.pack()

# создаем прямоульник
player = c.create_rectangle(300, 300, 340, 340, fill='green')

def move_player(key):
    # если нажали a
    if key.char == 'a':
        # сдвигаем прямоугольник влево
        c.move(player, -20, 0)
    # если нажали d
    if key.char == 'd':
        # сдвигаем прямоугольник вправо
        c.move(player, 20, 0)
    # если нажали w
    if key.char == 'w':
        # сдвигаем прямоугольник вверх
        c.move(player, 0, -20)
    # если нажали s
    if key.char == 's':
        # сдвигаем прямоугольник вниз
        c.move(player, 0, 20)

# при нажатии любой клавишы вызываем keyDown
tk.bind("<KeyPress>", move_player)

mainloop()

Добавляем графику

Вместо овалов и прямоугольников можно использовать картинки в формате gif (это ограничение библиотеки tkinter). Просто вместо create_oval можно использовать функцию create_image. Но есть два нюанса: во-первых картинку нужно сначала загрузить в память с помощью функции PhotoImage(путь к картинке), а во-вторых - у картинки не четыре координаты, две.

Перед использованием выбранные картинки нужно конвертировать в gif и подогнать под нужный нам размер (у нас - 40 на 40 пикселей). Вот что получилось у меня:

from tkinter import *
import random

tk = Tk()
c = Canvas(tk, width=640, height=640, bg='white')
c.pack()

# загружаем картинки - астероид, небо и игрок
asteroid_image = PhotoImage(file='asteroid.gif')
player_image = PhotoImage(file='player.gif')
sky_image = PhotoImage(file='sky.gif')

# рисуем небо
c.create_image(0, 0, image=sky_image, anchor=NW)

# количество шаров
N = 10

# списки со скоростями
x_speeds = []
y_speeds = []

# список с фигурами
asteroids = []

# создаем N астероидов
for i in range(N):
    # придумываем координаты
    x = random.randint(20, 610)
    y = random.randint(20, 610)

    # создаем астероид - теперь это картинка на базе asteroid_image
    asteroid = c.create_image(x, y, image=asteroid_image, anchor=NW)

    # придумываем скорость
    vx = random.randint(-15, 15)
    vy = random.randint(-15, 15)

    # добавляем астероид в список
    asteroids.append(asteroid)

    # добавляем скорости в списки
    x_speeds.append(vx)
    y_speeds.append(vy)

# создаем игрока на основе картинки
player = c.create_image(300, 300, image=player_image, anchor=NW)

def move_ball():
    # для каждого астероида из N
    for i in range(N):
        # берем номер астероида c номером i из списка
        asteroid = asteroids[i]

        # и его скорости
        vx = x_speeds[i]
        vy = y_speeds[i]

        # получаем текущие координаты астероида
        # поскольку у картинки в отличие от овала только две координаты (левого верхнего угла),
        # мы сами вычисляем правую нижнюю координату
        x1, y1 = c.coords(asteroid)
        x2 = x1 + 40
        y2 = y1 + 40

        # если астероид на границе по x - развернуть скорость по x
        if x1 <= 0 or x2 >= 640:
            vx *= -1
        # аналогично по y
        if y1 <= 0 or y2 >= 640:
            vy *= -1

        # сохраняем скорости обратно в список
        x_speeds[i] = vx
        y_speeds[i] = vy

        # передвигаем астероид
        c.move(asteroid, vx, vy)

    # повторяем через полсекунды
    c.after(50, move_ball)

# спустя полсекунды (50 мс) после запуска выполнить moveBall
c.after(50, move_ball)

def move_player(key):
    # если нажали a
    if key.char == 'a':
        # сдвигаем прямоугольник влево
        c.move(player, -20, 0)
    # если нажали d
    if key.char == 'd':
        # сдвигаем прямоугольник вправо
        c.move(player, 20, 0)
    # если нажали w
    if key.char == 'w':
        # сдвигаем прямоугольник вверх
        c.move(player, 0, -20)
    # если нажали s
    if key.char == 's':
        # сдвигаем прямоугольник вниз
        c.move(player, 0, 20)

# при нажатии любой клавишы вызываем keyDown
tk.bind("<KeyPress>", move_player)

mainloop()

Добавляем геймплей

Чтобы в игру можно было по-настоящему поиграть, нужно уменьшать жизнь при столкновении с астероидами и засекать время жизни. Но последний этап я по традиции оставлю тебе.

Нужно добавить в самый верх окна поле с временем жизни и проверять каждый астероид на столкновение с игроком. Чтобы немного упростить задачу, я выделил места, которые нужно доделать, комментарией TODO:.

А как обнаруживать столкновения? Самый простой способ - просто считать, что все объекты - это квадраты, тогда задача сводится к пересечению двух квадратов. Небольшая подсказка на картинке ниже, условие довольное большое, но легко записывается с помощью скобочек и операций or и and.

from tkinter import *
import random

window = Tk()

# добавляем статус бар для счета вверху экрана
state_label = StringVar()
Label(window, textvariable=state_label).pack()

c = Canvas(window, width=640, height=640, bg='white')
c.pack()

# загружаем картинки - астероид, небо и игрок
asteroid_image = PhotoImage(file='asteroid.gif')
player_image = PhotoImage(file='player.gif')
sky_image = PhotoImage(file='sky.gif')

# рисуем небо
c.create_image(0, 0, image=sky_image, anchor=NW)

# количество шаров
N = 10

# списки со скоростями
x_speeds = []
y_speeds = []

# список с фигурами
asteroids = []

# создаем N астероидов
for i in range(N):
    # придумываем координаты
    x = random.randint(20, 610)
    y = random.randint(20, 610)

    # создаем астероид - теперь это картинка на базе asteroid_image
    asteroid = c.create_image(x, y, image=asteroid_image, anchor=NW)

    # придумываем скорость
    vx = random.randint(-15, 15)
    vy = random.randint(-15, 15)

    # добавляем астероид в список
    asteroids.append(asteroid)

    # добавляем скорости в списки
    x_speeds.append(vx)
    y_speeds.append(vy)

# создаем игрока на основе картинки
player = c.create_image(300, 300, image=player_image, anchor=NW)

# переменная для хранения жизни
life = 3

# длительности игры
game_time = 0

# выводим стартовое время вверху экрана
state_label.set("Время в игре: " + str(game_time))

def move_ball():

    # TODO: получить левую верхнюю коодинату игрока
    player_x1, player_y1 = ...

    # TODO: вычислить правую нижнюю координату игрока
    player_x2, player_y2 = ...

    # для каждого астероида из N
    for i in range(N):
        # берем номер астероида c номером i из списка
        asteroid = asteroids[i]

        # и его скорости
        vx = x_speeds[i]
        vy = y_speeds[i]

        # получаем текущие координаты астероида
        # поскольку у картинки в отличие от овала только две координаты (левого верхнего угла),
        # мы сами вычисляем правую нижнюю координату
        x1, y1 = c.coords(asteroid)
        x2 = x1 + 40
        y2 = y1 + 40

        # TODO: если астероид пересекается с игроком - уменьшить жизнь
        # TODO: если жизнь на нуле - закрыть игру tk.destroy()

        # если астероид на границе по x - развернуть скорость по x
        if x1 <= 0 or x2 >= 640:
            vx *= -1
        # аналогично по y
        if y1 <= 0 or y2 >= 640:
            vy *= -1

        # сохраняем скорости обратно в список
        x_speeds[i] = vx
        y_speeds[i] = vy

        # передвигаем астероид
        c.move(asteroid, vx, vy)

    # повторяем через полсекунды
    c.after(50, move_ball)

    # TODO: увеличиваем время жизни на 0.05
    # TODO: обновляем поле вверху экрана

# спустя полсекунды (50 мс) после запуска выполнить moveBall
c.after(50, move_ball)

def move_player(key):
    # если нажали a
    if key.char == 'a':
        # сдвигаем прямоугольник влево
        c.move(player, -20, 0)
    # если нажали d
    if key.char == 'd':
        # сдвигаем прямоугольник вправо
        c.move(player, 20, 0)
    # если нажали w
    if key.char == 'w':
        # сдвигаем прямоугольник вверх
        c.move(player, 0, -20)
    # если нажали s
    if key.char == 's':
        # сдвигаем прямоугольник вниз
        c.move(player, 0, 20)

# при нажатии любой клавишы вызываем keyDown
window.bind("<KeyPress>", move_player)

mainloop()