Gra izometryczna cz.3 – Kolizje

przez Adam Sobociński w dniu 9 listopada 2011 w kategorii: python

Wiemy już jak tworzyć duszki i poruszać nimi. Ale dało się zauważyć, że duszkom coś brakuje. Kolizji.
Czym są kolizje? Jeśli próbujesz wyjść z pokoju przez ścianę, to nastąpi kolizja ze ścianą, która nie wypuści cie z niego. Jeśli kolizji by nie było, jak w naszym ostatnim przykładzie, to można by było przechodzić przez ściany jak duchy.
Kiedy więc zachodzi kolizja? Gdy dwa obiekty w grze, zachodzą na siebie, czyli jakaś część duszka, znajduje się w tym samym miejscu ekranu, co inny duszek.

Metody wykrywania kolizji
Jest wiele sposobów wykrywania kolizji. Najdokładniejszym jest sprawdzanie każdego piksela, duszka z innym duszkiem. Metoda ta ma jednak wady. Jest bardzo czasochłonna. W grze, gdzie wszystko dynamicznie się zmienia, nie jest konieczna aż taka dokładność.

Kolizja okręgów
Najprostszym sposobem jest przybliżenie obiektu do okręgu.


Jak to zaimplementować? Ponieważ każdy obrazek jest czworokątem, dzielimy jego przekątną przez 2. W wyniku otrzymamy promień okręgu dla kolizji. Przekątna tworzy 2 trójkąty prostokątne, więc do obliczania jej długości, możesz wykorzystać wzór promien = SQRT(a2+b2)/2. Gdzie SQRT to pierwiastek kwadratowy, a2 i b2 to przyprostokątne trójkąta podniesione do kwadratu.
Jeśli teraz dodamy promień jednego obiektu, do promienia drugiego obiektu, otrzymamy odległość, na którą mogą się maksymalnie zbliżyć duszki do siebie. Pozostaje nam tylko obliczyć odległość między środkami tych okręgów. Pomoże nam tu twierdzenie Pitagorasa, które mówi ze suma kwadratów długości przyprostokątnych, równa się kwadratowi długości przeciwprostokątnej.

Jak widać na rysunku, aby obliczyć odległość środków, musimy obliczyć długość przyprostokątnych. Aby wyznaczyć te długości, odejmujemy od siebie współrzędne (x2-x1) i (y2-y1) i zgodnie z prawem Pitagorasa podnosimy do kwadratu bok1 = (x2-x1)^2, bok2 = (y2-y1)^2. Po dodaniu do siebie obu boków bok1+bok2, otrzymamy długość przeciwprostokątnej, która jest odległością, między środkami tych okręgów, podniesioną do kwadratu.
Mamy więc odległości między punktami i zgodnie z tym co napisałem wyżej o promieniach, musimy sumę promieni obu okręgów podnieść do kwadratu (r1+r2)^2. Jest to konieczne, ponieważ obie długości przyprostokątnych także są podniesione do kwadratu. Ostatnią rzeczą jaką trzeba zrobić, to sprawdzić czy odległość między środkami, jest większa lub równa sumie promieni okręgów, wtedy mamy kolizję.

odleglosc = (x2-x1)^2 + (y2-y1)^2
promienie = (r1+r2)^2

kolizja nastąpi, gdy odleglosc jest większa niż suma długości promieni (promienie)

Poniżej gotowa funkcja wykrywania kolizji między okręgami.

  def collision(x1,y1,r1,x2,y2,r2):
    if (x2-x1)**2+(y2-y1)**2<=(r1+r2)**2:
        return True
    else:
        return False

Jak widać po kodzie jest to naprawdę proste. Do funkcji przekazujemy współrzędne xy obiektów oraz promień r. Przykładowy kod użycia kolizji okręgów:

import pygame                # importujemy biblioteki pygame
from pygame.locals import *  # importujemy nazwy [QUIT, KEYDOWN,K_ESCAPE] itp.
from sys import exit         # importujemy funkcje systemowa exit

screen_size = (800,600)      # ustalamy rozmiar ekranu

class IsoGame(object):
    def __init__(self):
        pygame.init()       # incjalizujemy biblioteke pygame
        flag = DOUBLEBUF    # wlaczamy tryb podwojnego buforowania

        # tworzymy bufor na  grafike
        self.surface = pygame.display.set_mode(screen_size,flag)

        # zmienna stanu gry
        self.gamestate = 1  # 1 - run, 0 - exit
        self.images = [None] *2
        self.images[0] = pygame.image.load('circle1.png')
        self.images[1] = pygame.image.load('circle2.png')
        self.circle_x = 220  # pozycja x duszka
        self.circle_y = 150  # pozycja y duszka

        # obliczamy promien duszka
        self.radius = self.images[0].get_width()/2

        self.speed = 1.2     # szybkosc poruszania duszka
        self.player_x = 50   # pozycja x duszka na ekranie
        self.player_y = 30   # pozycja y duszka na ekranie

        self.loop()                             # glowna petla gry

    def move(self,dirx,diry):
       """ poruszanie duszkiem """
       dx = self.player_x + (dirx * self.speed)
       dy = self.player_y + (diry * self.speed)
       if self.collision(dx,dy,self.radius,self.circle_x,self.circle_y,self.radius):
	       return
       self.player_x = dx
       self.player_y = dy

    def game_exit(self):
        """ funkcja przerywa dzialanie gry i wychodzi do systemu"""
        exit()

    def collision(x1,y1,r1,x2,y2,r2):
       if (x2-x1)**2+(y2-y1)**2<=(r1+r2)**2:
          return True
       else:
          return False

    def loop(self):
        """ glowna petla gry """
        while self.gamestate==1:
	   player_anim = 0
           for event in pygame.event.get():
               if event.type==QUIT or (event.type==KEYDOWN and event.key==K_ESCAPE):
                   self.gamestate=0

           keys = pygame.key.get_pressed() # odczytujemy stan klawiszy

           if keys[K_s]:
              self.move(0,1)  # ruch w dol

           if keys[K_w]:
              self.move(0,-1)   # ruch w gore

           if keys[K_d]:
              self.move(1,0)  # ruch w prawo

           if keys[K_a]:
              self.move(-1,0)   # ruch w lewo

           self.surface.fill((0,0,0))  # czyscimy ekran, malo wydajne ale wystarczy

           # umieszczamy gracza
           self.surface.blit(self.images[1],(self.player_x,self.player_y))

           self.surface.blit(self.images[0], (self.circle_x,self.circle_y))
           pygame.display.flip()   # przenosimy bufor na ekran

        self.game_exit()

if __name__ == '__main__':
   IsoGame()

Pobierz paczkę z przykładem isogame_tut3.zip

Wykrywanie kolizji okręgów nadaje się dobrze, gdy duszki mają zbliżony kształt do okręgu. Gdy są kwadratami, prostokątami, czy w postaci igły, to będzie bardzo niedokładne. Do takich obiektów, można zastosować kolizje między prostokątami.

Kolizja prostokątów
W tej metodzie, kolizja nastąpi wtedy, gdy jeden róg prostokąta znajdzie się w polu innego prostokąta.

Poniżej funkcja Collision(), która zwraca informacje, czy wystąpiła kolizja między prostokątami.

  def collision(self,x1,y1,w1,h1,x2,y2,w2,h2):
        if x1 >= x2+w2: return True
        if x1+w1 <= x2: return True
        if y1 >= y2+h2: return True
        if y1+h1 <= y2: return True
        return False

Kod jest prosty i myślę, że nie wymaga komentarza. Funkcja przyjmuje argumenty w postaci pozycji prostokąta x1,x2 – współrzędna x lewego górnego rogu pierwszego i drugiego prostokąta. y1,y2 – współrzędna y lewego górnego rogu pierwszego i drugiego prostokąta. h1,h2 – wysokość pierwszego i drugiego prostokąta. w1,w2 – szerokość pierwszego i drugiego prostokąta. Zasadza działania polega na tym, że przy każdym ruchu, porównywana jest pozycja prostokątów względem siebie, jeśli któryś warunek jest spełniony, to znaczy, że nastąpiła kolizja. Poniżej kod, który pozwala przetestować działanie kolizji prostokątów.

 import pygame                # importujemy biblioteki pygame
from pygame.locals import *  # importujemy nazwy [QUIT, KEYDOWN,K_ESCAPE] itp.
from sys import exit         # importujemy funkcje systemowa exit

screen_size = (800,600)      # ustalamy rozmiar ekranu

class IsoGame(object):
    def __init__(self):
        pygame.init()       # incjalizujemy biblioteke pygame
        flag = DOUBLEBUF    # wlaczamy tryb podwojnego buforowania

        # tworzymy bufor na  grafike
        self.surface = pygame.display.set_mode(screen_size,flag)

        # zmienna stanu gry
        self.gamestate = 1  # 1 - run, 0 - exit
        self.images = [None] *2
        self.images[0] = pygame.image.load('rectangle1.png')
        self.images[1] = pygame.image.load('rectangle2.png')
        self.sprite_x = 220   # pozycja x duszka
        self.sprite_y = 130    # pozycja y duszka
        self.speed = 1.2      # szybkosc poruszania duszka
        self.player_x = 320   # pozycja x duszka na ekranie
        self.player_y = 250   # pozycja y duszka na ekranie

        self.loop()                             # glowna petla gry

    def move(self,dirx,diry):
       """ poruszanie duszkiem """
       dx = self.player_x + (dirx * self.speed)
       dy = self.player_y + (diry * self.speed)
       if not self.collision(dx,dy,100,100,self.sprite_x,self.sprite_y,100,100):
	       return
       self.player_x = dx
       self.player_y = dy

    def game_exit(self):
        """ funkcja przerywa dzialanie gry i wychodzi do systemu"""
        exit()

    def collision(self,x1,y1,w1,h1,x2,y2,w2,h2):
        if x1 >= x2+w2:
            return True
        if x1+w1 <= x2:
            return True
        if y1 >= y2+h2:
            return True
        if y1+h1 <= y2:
            return True

        return False


    def loop(self):
        """ glowna petla gry """
        while self.gamestate==1:
	   player_anim = 0
           for event in pygame.event.get():
               if event.type==QUIT or (event.type==KEYDOWN and event.key==K_ESCAPE):
                   self.gamestate=0

           keys = pygame.key.get_pressed() # odczytujemy stan klawiszy

           if keys[K_s]:
              self.move(0,1)  # ruch w dol

           if keys[K_w]:
              self.move(0,-1)   # ruch w gore

           if keys[K_d]:
              self.move(1,0)  # ruch w prawo

           if keys[K_a]:
              self.move(-1,0)   # ruch w lewo

           self.surface.fill((0,0,0))  # czyscimy ekran, malo wydajne ale wystarczy

           # umieszczamy gracza
           self.surface.blit(self.images[1],(self.player_x,self.player_y))

           self.surface.blit(self.images[0], (self.sprite_x,self.sprite_y))    
           pygame.display.flip()   # przenosimy bufor na ekran

        self.game_exit()

if __name__ == '__main__':
   IsoGame() 

Dla uproszczenia kodu, wpisałem na stałe wysokość i szerokość prostokątów, ale jest to w naszym przypadku, rozmiar wczytaj grafiki. Wiec możemy ustawić dynamiczną wielkość prostokąta, zależnie od wczytanego obrazka.

sprite_width = images[0].get_width()
sprite_height = images[0].get_height()

Przykładowy kod z obrazkami do pobrania: isogame_tut3a

Jako zadanie domowe, przerobić przykładowy program, aby wielkość prostokąta była pobierana bezpośrednio z wczytanego obrazka :)

Kolizja okręgu z prostokątem
Ostatnią metodę, jaką opiszę, to kolizje prostokąta z okręgiem.

Najpierw wyznaczamy najbliższy wierzchołek prostokąta do środka okręgu. A następnie obliczamy odległość między tym wierzchołkiem a środkiem okręgu, za pomocą twierdzenia Pitagorasa. Funkcja wykrywająca kolizje wygląda tak:

def collision(x,y,w,h,cx,cy,r):
   # ustalamy pozycje najblizszego wierzcholka do srodka okregu
   tx = cx
   ty = cy
   if cx < x: tx = x        
   if cx >= (x + w): tx = x + w
   if cy < y: ty = y    
   if cy >= (y + h): ty = y + h

   # sprawdzamy odleglosc miedzy wierzcholkiem a srodkiem okregu
   # jesli mniejsza niz promien, oznacza to kolizje
   if (cx-tx)**2+(cy-ty)**2 < r**2:  return True
   return False

Kod jest prosty, odpowiednio skomentowany, widać co i jak. Udostępniam przykładowy program, który pokazuje wykorzystanie naszej funkcji, kolizji okręgu z prostokątem.

Pobierz przykładowy program isogame_tut3b

Uff. Doszliśmy do końca. Na koniec coś przyjemniejszego, wykorzystanie naszej wiedzy w małym demku, w którym poruszasz się statkiem i omijasz asteroidy. W programie jest wiele uproszczeń, które w normalnej produkcji się nie robi, ale może być bazą do dalszego rozwoju. Zapraszam do analizy kodu. Ja tylko wyjasnię linie 82

if self.collision(self.player.x+40,self.player.y,self.player.width-40,\
       self.player.height,self.asteroids[n].x+32,self.asteroids[n].y+32,32):

Zastosowałem tu wspomniane uproszczenia, dodałem do pozycji sprawdzania kolizji na osi x +40, aby nie było kolizji z ogniem, wydobywającego sie z silnika statku, automatycznie trzeba obciąć width, aby kolizja mieściła się w zakresie wielkości obrazka. Podobnie zrobiłem przy asteroidzie, dodałem do pozycji x i y +32. Ponieważ asteroida ma 64x64px, a x,y wskazuje na lewy górny róg obrazka, trzeba dodać połowa wysokości i szerokość obrazka, aby kolizja była sprawdzana od środka okręgu a nie od krawędzi. Ostatnia wartość to promień asteroidy. Można to to zrobić bardziej elegancko, i pobierać te wartości dynamicznie, ale nie chciałem zaciemniać i tak dość rozbudowanego jak na początek kodu, a który nie jest celem tego kursu :)

Pobierz demo asteroids.zip

Pygame, ma wbudowany moduł RECT, który obsługuje kolizje prostokątów, jako zadanie domowe, zastosować ten moduł w naszym demie Asteroids.
Mam nadzieję, że z ciekowością i zapałem przebrnęliście przez te 3 lekcje, bo na tym kończymy obsługę grafiki w czystym Pygame. Kolejny etap, to przejście na obsługę OpenGL, który daje wyraźną różnice w wydajności i do naszych celów bardziej się nada. W kolejnej lekcji dowiesz się jak wyświetlać duszki w OpenGL.

Podziel się na:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Blogplay
  • Blogger.com
  • Gadu-Gadu Live


6 odpowiedzi na „“Gra izometryczna cz.3 – Kolizje””

  1. Slawek pisze:

    Strona jest fajna i bardzo przejrzysta, ale pod artykuami brakuje linku „Dalej” albo „Przejdz do nastepnej lekcji”

    • Adam Sobociński pisze:

      Na razie lekcji jest mało, wszystkie widać z boku na liście :)
      Ale dzięki za spostrzeżenie, dodam odpowiednie linki.

  2. pyhtonrules pisze:

    Hello, nice tutorial.
    But when I import the sample .py’s there are often indent errors.

  3. kateb pisze:

    zamiast:
    kolizja nastąpi, gdy odleglosc jest wieksza niż promienie,

    powinno być:
    kolizja nastąpi, gdy odleglosc jest mniejsza niż suma długości promieni

    • Adam Sobociński pisze:

      Dzięki za uwagi. Tekst poprawiłem, natomiast co do kodu, mój edytor chyba ustawiał po swojemu wcięcia, bo już kilka osób zwracało na to uwagę. Dziwne jest to o tyle, że u mnie wszystko działa poprawnie. Dlatego przeszedłem na Eclipse.

Dodaj komentarz

Current month ye@r day *