20. Заканчиваем игру Jumper (почти Doodle)

Продолжаем делать игру «Jumper».
В прошлом уроке мы создали прыгающий мячик и платформы. Теперь пришло время организовать взаимодействие мячика и платформ.

from tkinter import *
from random import randrange as rnd 
import time

root = Tk()
fr = Frame(root)
root.geometry('800x600')
canv = Canvas(root, bg = 'white')
canv.pack(fill=BOTH,expand=1)
 
def click(event):
        global j
        j.vx = (event.x - j.x)/10
        j.vy = (event.y - j.y)/10
         
canv.bind('<1>',click) 

class Jumper():
        def __init__(self):
                self.x = 40
                self.y = 150
                self.r = 20
                self.id = canv.create_oval(self.x-self.r,self.y-self.r,self.x+self.r,self.y+self.r,fill='orange') 
                self.vy = 0
                self.vx = 6

        def move(self): 
                self.vy += 1.5
                self.y += self.vy
                self.x += self.vx
                self.vx *= 0.99
                if self.y > 550:
                        self.vy *= -0.7
                        self.vx *= 0.8
                        self.y = 550
                if self.x > 750:
                        self.x = 750
                        self.vx *= -0.7
                if self.x < 50:
                        self.x = 50
                        self.vx *= -0.7
                canv.coords(self.id,self.x-self.r,self.y-self.r,self.x+self.r,self.y+self.r)
                
class Platform():
        def __init__(self):
                self.x = 300
                self.y = 0
                self.vx = 0
                self.vy = 2
                self.w = 120
                self.h = 20
                self.id = canv.create_rectangle(self.x,self.y,self.x+self.w,self.y+self.h,fill='green',width=0) 
        
        def move(self):
                self.y += self.vy
                canv.coords(self.id,self.x,self.y,self.x+self.w,self.y+self.h)
        

j = Jumper()
pls = [] # список для хранения всех платформ

def new_platform():
        p = Platform() # создали платформу
        p.x = rnd(50,700) # переместили в случайное место по горизонтали
        pls.append(p) # добавили в список
        root.after(2000,new_platform) #  через 2 сек появится новая платформа

new_platform() # создаем первую платформу и запускаем процесс непрерывного их создания
while 1:
        j.move()
        for pl in pls: # перебрать все платформы в списке
                pl.move() # каждой дать команду переместиться
        canv.update()
        time.sleep(0.03)
 
 
mainloop()

Для того, чтобы определить, касается ли мячик платформы, есть два способа. Можно определять по координатам (как мы и сделаем), а можно через методы Tkinter. Во всех библиотеках (движках) для создания игр есть способы для определения столкновений. Tkinter не является библиотекой для создания игр, но его Canvas обладает интересными возможностями, среди которых есть и проверка столкновений, но пользоваться мы ею не будем.

Первая мысль, которая приходит в голову — это проверить на равенство координаты платформы и мяча. Но, во-первых, вероятность того, что центр круга совпадет с левым верхним углом прямоугольника не очень высока, а во-вторых, нам это и не нужно.

Снимок экрана от 2016-01-21 14:29:27

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

Еще варианты попаданий:

Снимок экрана от 2016-01-21 14:36:52

Для начала попробуем считать попаданием ситуацию, в которой центр круга находится внутри прямоугольника. Это самое простое:

Снимок экрана от 2016-01-21 14:40:04

Изменим основной цикл так:

while 1:
        j.move()
        for pl in pls: # перебрать все платформы в списке
                pl.move() # каждой дать команду переместиться
                if pl.x <= j.x <= pl.x+pl.w and pl.y <= j.y <= pl.y+pl.h: # если центр круга внутри платформы
                        j.vx = 0 
                        j.vy = 0

Немного странно, не так ли?
Мячик застревает внутри платформы. Попробуем его усадить повыше:

                        j.vx = 0
                        j.vy = 0
                        j.y = pl.y-j.r

Сможете объяснить то, что произошло? Почему мячик дергается?
Измените проверку попадания мячика на платформу:

                if pl.x <= j.x <= pl.x+pl.w and pl.y-j.r <= j.y <= pl.y+pl.h: # если центр круга внутри платформы

Мячик продолжает дергаться, но уже не так сильно.
Почему?

                if pl.x <= j.x <= pl.x+pl.w and pl.y-j.r <= j.y <= pl.y+pl.h: # если центр круга внутри платформы
                        j.vx = 0
                        j.vy = pl.vy
                        j.y = pl.y-j.r 

Почему перестал дергаться?

Продолжаем улучшать код: сделаем так, чтобы при прыжке вверх мячик не цеплялся за платформу.
Для этого добавим проверку направление полета шарика:

                if j.vy >= 0 and pl.x <= j.x <= pl.x+pl.w and pl.y-j.r <= j.y <= pl.y+pl.h: # если центр круга внутри платформы
                        j.vx = 0
                        j.vy = pl.vy
                        j.y = pl.y-j.r 

Другую проблему попробуйте исправить сами: при появлении платформа на мгновение появляется и только потом перемещается в «своё» место и это мелькание заметно. Сделайте так, чтобы мелькания не было, чтобы платформа сразу появлялась там, где нужно. Это не очень сложно.

А пока мы добавим разнообразие в поведение платформ: заставим некоторые из перемещаться по горизонтали.

class Platform():
        def __init__(self):
                self.x = 300
                self.y = 0
                if rnd(2) == 1: # решаем, сделать ли платформу движущейся
                        self.vx = 1
                        color = 'orange'
                else:
                        self.vx = 0
                        color = 'green'
                self.vy = 2
                self.w = 120
                self.h = 20
                self.id = canv.create_rectangle(self.x,self.y,self.x+self.w,self.y+self.h,fill=color,width=0) 
        
        def move(self):
                self.y += self.vy
                self.x += self.# двигаем платформу
                canv.coords(self.id,self.x,self.y,self.x+self.w,self.y+self.h)

Появилось три проблемы: шарик соскальзывает с платформы; а платформы уезжают за экран; платформы едут только направо.

        def move(self):
                self.y += self.vy
                self.x += self.vx
                if self.x > 790 - self.w or self.x < 10:
                        self.vx *= -1
                canv.coords(self.id,self.x,self.y,self.x+self.w,self.y+self.h)

Платформы стали отражаться от вертикальных стен.

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

class Platform():
        def __init__(self):
                self.x = 300
                self.y = 0
                self.vx = random()*6-3
                color = 'green'
                self.vy = 2
                self.w = 120
                self.h = 20
                self.id = canv.create_rectangle(self.x,self.y,self.x+self.w,self.y+self.h,fill=color,width=0) 

Появилась новая функция: random(). Ее нужно импортировать из модуля random:

from tkinter import *
from random import randrange as rnd, random
import time

Дело в том, что randrange возвращает только целое число, а это даст всего несколько вариантов скорости: -2,-1,0,1,2,3. Если же мы хотим, чтобы скорость могла быть любой, в том числе и дробной: -2.1, -1.7, и т.д. то нужно использовать функцию random(). Она возвращает случайное число [0;1). Т.е. не включая единицу. Чтобы получить любое число мы умножим на расстояние между крайними точками (которые мы хотим получить) и сдвинем влево на половину этого расстояния. Если random() даст 0, то по той формуле получится 0*6-3=-3. Это самое маленькое значение, которое можно получить (той формулой). Если же random() даст 1 (точнее 0.999999),т.е. самое большое значение, которое может дать, то по формуле получится 1(условно)*6 -3 = 6-3 = 3. Т.е. эта формула даст случайные значения в [-3;3), что нам и требовалось.

Еще увеличим разнообразие: добавим падающие платформы и выделим их цветом:

class Platform():
        def __init__(self):
                self.x = 300
                self.y = 0
                self.vx = random()*6-3
                color = 'green'
                self.vy = 2
                self.w = 120
                self.h = 20
                self.mode = rnd(2) # сделать ли падающей?
                self.ay = 0 # ускорение падения. Пока не падает
                if self.mode == 0: # если падающая, то - оранжевым
                        color = 'orange'
                self.id = canv.create_rectangle(self.x,self.y,self.x+self.w,self.y+self.h,fill=color,width=0) 
        
        def move(self):
                self.y += self.vy
                self.vy += self.ay # пока платформа не стала падать, в ay будет 0
                self.x += self.vx
                if self.x > 790 - self.w or self.x < 10:
                        self.vx *= -1
                canv.coords(self.id,self.x,self.y,self.x+self.w,self.y+self.h)

А теперь при посадке на платформу «прилепим» шарик, чтобы он не соскальзывал.

while 1:
        j.move()
        for pl in pls: # перебрать все платформы в списке
                pl.move() # каждой дать команду переместиться
                if j.vy >= 0 and pl.x <= j.x <= pl.x+pl.w and pl.y-j.r <= j.y <= pl.y+pl.h: # если центр круга внутри платформы
                        j.vx = pl.vx # взять горизонтальную скорость платформы
                        j.vy = pl.vy  # взять вертикальную скорость платформы
                        j.y = pl.y-j.r 
                        if pl.mode == 0: # если мяч прикоснулся к падающей платформе, то она начинает падать:
                                pl.ay = 1

Прежде, чем закончить, сделаем еще одну важную вещь: чтобы мячик не прыгал в воздухе, а мог прыгать только после того, как прикоснулся к платформе:

class Jumper():
        def __init__(self):
                self.x = 40
                self.y = 150
                self.r = 20
                self.id = canv.create_oval(self.x-self.r,self.y-self.r,self.x+self.r,self.y+self.r,fill='orange') 
                self.vy = 0
                self.vx = 6
                self.can_jump = 1 # может ли прыгать
while 1:
        j.move()
        for pl in pls: # перебрать все платформы в списке
                pl.move() # каждой дать команду переместиться
                if j.vy >= 0 and pl.x <= j.x <= pl.x+pl.w and pl.y-j.r <= j.y <= pl.y+pl.h: # если центр круга внутри платформы
                        j.vx = pl.vx # взять горизонтальную скорость платформы
                        j.vy = pl.vy  # взять вертикальную скорость платформы
                        j.y = pl.y-j.r 
                        if pl.mode == 0: # если мяч прикоснулся к падающей платформе, то она начинает падать:
                                pl.ay = 1
                        j.can_jump = 1 # снова можно прыгать
        canv.update()
        time.sleep(0.03)
def click(event):
        global j
        if j.can_jump != 0:
                j.vx = (event.x - j.x)/10
                j.vy = (event.y - j.y)/10
                j.can_jump = 0 # если прыгнул, то еще раз прыгать нельзя, пока не коснулся платформы

Все вместе:

from tkinter import *
from random import randrange as rnd, random
import time

root = Tk()
fr = Frame(root)
root.geometry('800x600')
canv = Canvas(root, bg = 'white')
canv.pack(fill=BOTH,expand=1)
 
def click(event):
        global j
        if j.can_jump != 0:
                j.vx = (event.x - j.x)/10
                j.vy = (event.y - j.y)/10
                j.can_jump = 0 # если прыгнул, то еще раз прыгать нельзя, пока не коснулся платформы
         
canv.bind('<1>',click) 

class Jumper():
        def __init__(self):
                self.x = 40
                self.y = 150
                self.r = 20
                self.id = canv.create_oval(self.x-self.r,self.y-self.r,self.x+self.r,self.y+self.r,fill='orange') 
                self.vy = 0
                self.vx = 6
                self.can_jump = 1 # может ли прыгать

        def move(self): 
                self.vy += 1.5
                self.y += self.vy
                self.x += self.vx
                self.vx *= 0.99
                if self.y > 550:
                        self.vy *= -0.7
                        self.vx *= 0.8
                        self.y = 550
                if self.x > 750:
                        self.x = 750
                        self.vx *= -0.7
                if self.x < 50:
                        self.x = 50
                        self.vx *= -0.7
                canv.coords(self.id,self.x-self.r,self.y-self.r,self.x+self.r,self.y+self.r)
                
                
class Platform():
        def __init__(self):
                self.x = 300
                self.y = -50
                self.vx = random()*6-3
                color = 'green'
                self.vy = 2
                self.w = 120
                self.h = 20
                self.mode = rnd(2) # сделать ли падающей?
                self.ay = 0 # ускорение падения. Пока не падает
                if self.mode == 0: # если падающая, то - оранжевым
                        color = 'orange'
                self.id = canv.create_rectangle(self.x,self.y,self.x+self.w,self.y+self.h,fill=color,width=0) 
        
        def move(self):
                self.y += self.vy
                self.vy += self.ay # пока платформа не стала падать, в ay будет 0
                self.x += self.vx
                if self.x > 790 - self.w or self.x < 10:
                        self.vx *= -1
                canv.coords(self.id,self.x,self.y,self.x+self.w,self.y+self.h)
        

j = Jumper()
pls = [] # список для хранения всех платформ

def new_platform():
        p = Platform() # создали платформу
        p.x = rnd(50,700) # переместили в случайное место по горизонтали
        pls.append(p) # добавили в список
        root.after(2000,new_platform) #  через 2 сек появится новая платформа

new_platform() # создаем первую платформу и запускаем процесс непрерывного их создания
while 1:
        j.move()
        for pl in pls: # перебрать все платформы в списке
                pl.move() # каждой дать команду переместиться
                if j.vy >= 0 and pl.x <= j.x <= pl.x+pl.w and pl.y-j.r <= j.y <= pl.y+pl.h: # если центр круга внутри платформы
                        j.vx = pl.vx # взять горизонтальную скорость платформы
                        j.vy = pl.vy  # взять вертикальную скорость платформы
                        j.y = pl.y-j.r 
                        if pl.mode == 0: # если мяч прикоснулся к падающей платформе, то она начинает падать:
                                pl.ay = 1
                        j.can_jump = 1 # снова можно прыгать
        canv.update()
        time.sleep(0.03)
 
 
mainloop()

Что можно сделать еще?
Можно добавить счетчик прыжков, пройденных платформ, секунд.
Можно сделать меню и падение мяча вниз без остановки на нижней границе. Можно изменить управление: сделать управление с клавиатуры.
Это все можно сделать.
Но ключевой момент данного урока это классы.
Классы позволяют объединить свойства и методы в один контейнер. Это удобно. Еще классы могут быть унаследованы от других, но это тема совсем другого урока.

One thought on “20. Заканчиваем игру Jumper (почти Doodle)

  1. Уведомление: Аноним

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *