SKPhysicsBody

5 Grudnia 2017

Koopa Mario Dragon

Dzień dobry ponownie! 👊
To już czwarty wpis z serii dotyczącej podstaw framework’a SpriteKit, służącego do tworzenia gier 2D na platformę iOS. Na początek jedna uwaga – jeśli z jakiś niewyjaśnionych powodów zjawiłeś się tutaj, kompletnie zielony w temacie 🍏 to odsyłam Cię do poprzednich wpisów z tej serii, gdzie poznasz podstawy dotyczące tworzenia scen, korzystania z edytora oraz podstawy animacji i dodawania tekstur do sceny.

Wcześniejsze wpisy z serii:

W dzisiejszym wpisie zaś zajmiemy się już konkretną robotą 🚀 – bo będziemy do naszych elementów dodawać tzw. SKPhysicsBody, czyli fizykę naszych postaci. Dzięki temu dowiesz się jak działa grawitacja w grach iOS, jak wykrywać kolizję między elementami i parę innych, ciekawych rzeczy.

Zachęcony? 😁 No to to kodu 💻!

Dodawanie SKPhysicsBody do elementu

Jedną z największych zalet używania SpriteKit jest to, że dostarcza on świetny silnik fizyki dla elementów dodawanych do świata gry. Główną klasą przeznaczoną do zabawy z tym zagadnieniem jest właśnie wspomniana klasa SKPhysicsBody.

SKPhysicsBody ➡️ to obiekt, który dostarcza symulację fizyki do node’a. To oznacza, że zawsze obiekt tej klasy musi być powiązany z konkretnym node’m przed dodaniem jakiejkolwiek fizyki.

Jak to działa w rzeczywistości?
Załóżmy zatem, że tworzymy obiekt na naszej scenie, a potem odwołujemy sie do niego w kodzie. Jeśli nie wisz jeszcze, jak odwołać się do elementu sceny z klasy, to znaczy że coś przegapiłeś 😁👉 Zerknij do poprzedniego wpisu.

Kiedy wiemy, że obiekt istnieje na naszej scenie, możemy dodać do niego SKPhysicsBody ↙️️


    override func didMove(to view: SKView) {
        if let kupa = self.childNode(withName: "kupa") as? SKSpriteNode {
            kupa.physicsBody = SKPhysicsBody(rectangleOf: kupa.size)
        }
    }

A oto efekt:

Dlaczego tak? Bo właśnie tak działa dodanie ciała do elementu. Staje się on m.in. podatny na grawitację 🎈. Pamiętasz gdzie ustawiało się parametr grawitacji? Można to zrobić z poziomu edytora scen w Xcode o tu ⬇️

Ustawienie grawitacji w edytorze Xcode

Właściwości i funkcje

Kiedy mamy już dodany obiekt SKPhysicsBody do naszego sprite’a, możemy do niego dodawać ciekawe efekty oraz używać dostępnych metod. Jedna z nich jest np. applyImpulse() która pozwala dodać wektor ruchu do naszego elementu:

kupa.physicsBody?.applyImpulse(CGVector(dx: -20, dy: 0))

Nasza postać będzie poruszałą się w lewą stronę z wektorem o wartości -20 dla osi X.

SKPhysicsBody posiada szereg właściwości i metod, które mogę Cię interesować w zależności od tego, jakiego zachowania oczekujesz od swojej postaci. Wszystkie właściwości są oczywiście opisane w dokuemntacji od Apple tutaj.

Jak widzisz, możesz tak naprawdę zdefiniować większość właściwości jakie potrzebujesz do symulacji zachowania swojego bohatera, jak masa czy gęstość. Patrząc na dokumentację przypomniałem sobie o jeszcze jednej ważnej funkcji – isDynamic().

Funkcja ta definiuje, czy ciało dla naszego node’a ma być dynamiczne, czy statyczne. Jaka różnica?🤔

Ciało statyczne ignoguje wszystkie impulsy i siły na nie działające, jak np. właśnie siła grawitacji. Czyli w prostych słowach – bohaterowie, wrogowie, żyjące stworzonka etc. powinny posiadać tę właśność ustawioną na true (domyślna wartość☝️).

Kolizje

Skoro wiemy już, że dodanie ciała do node’a powoduje, że staje się on podatny na działanie różnych sił, to czas zadziałać na niego również innym obiektem 😼.

Jednakże, trzeba przemyśleć sposób odróżniania od siebie poszczególnych elementów. Ponieważ SKPhysicsBody to osobne obiekty, nie wystarczy że będziemy posługiwać się identyfikatorami powiązanych node’ów.

Do identyfikowania ciał naszych elementow będą służyły Bitmaski – czyli indentyfikatory w postaci liczb, zapisane szestnastkowo, np. 0x00000001 (dziesiętnie “1”).
Bitmaski możemy podzielić w naszym przypadku na kilka kategorii:

  • Kategorie – możemy odentyfikować nasze ciała po kategoriach, np gracz, wróg, moneta etc.
  • Kolizje – bitmaski reprezentujące kolizję między elementami.
  • Contact Test – bitmaska służąca przechwytywaniu zdarzeń kolizji.

Jeśli uczysz się SpriteKit, pewnie nie wiele rozumiesz w tym momencie z tej teorii, ale to normalne 👽 na tym etapie. Ogólnie na ten moment pamiętaj, że:

  • CategoryBitmask – Do jakiej kategorii należę?
  • CollisionBitmast – Z kim mogę kolidować
  • ContactTestBitmask – Daj znać systemowi że zderzyłem się z…

Praktyka czyni mistrza

Ok, czas na trochę praktyki 💪💻👍. Żeby zobaczyć jak działają kolizję, muszę stworzyć najpierw kilka obiektów, które będą ze sobą kolidowały. Dla mnie będzie to król Kupa (Koopa) z mario, most oraz grzybek, który nie koliduje z mostem, czyli spadnie do lawy 😄.

Dla każdego z tych trzech obiektów tworzę CategoryBitmask (zmienne typu UInt32).


    private var kupa: SKSpriteNode?
    private var mushroom: SKSpriteNode?
    private var brigde: SKSpriteNode?
    
    let kupaCategory: UInt32 =      0x00000001 << 0  
    let mushroomCategory: UInt32 =  0x0000001 << 1   
    let bridgeCategory: UInt32 =    0x00000001 << 2 
    

NOTE: Symbol “<<" oznacza przesunięcie bitowe w zapisie szestnastkowym. W skrócie działa to tak, że ostatni bit zostaje przesunięty o jedną pozycję w lewo, zatem pierwsza zmienna jest przesunięta o 0 pozycji w lewo, więc w zasadzie

0x00000001 << 0 = 1

Druga jest już przesunięta o 1 bit w lewo, więc:

(0x00000001 << 1) = 2 bo (0x00000001 << 1) = 0x00000010 = 2

End NOTE 👇

Teraz muszę każdemu z elementów przypisać kategorię do powiązanego z nim obiektu SKPhysicsBody:


    func createNodes(){
        if let kupa = self.childNode(withName: "kupa") as? SKSpriteNode,
            let mushroom = self.childNode(withName: "mushroom") as? SKSpriteNode,
            let bridge = self.childNode(withName: "bridge") as? SKSpriteNode{
            
            kupa.physicsBody = SKPhysicsBody(rectangleOf: kupa.size)
            kupa.physicsBody?.categoryBitMask = kupaCategory
            
            mushroom.physicsBody = SKPhysicsBody(rectangleOf: mushroom.size)
            mushroom.physicsBody?.categoryBitMask = mushroomCategory
            
            bridge.physicsBody = SKPhysicsBody(rectangleOf: bridge.size)
            bridge.physicsBody?.isDynamic = false
            bridge.physicsBody?.restitution = 0.75
            bridge.physicsBody?.categoryBitMask = bridgeCategory
        
        }
    }

Ok, mamy to. Teraz na końcu tej funkcji dopisuję linie odpowiadające za przypisanie bitmaski dla kolizji. Przypominam - chcę żeby smok zatrzymał się na moście, grzybek ma spaść. Zatem muszę przyporządkować collisionBitMask mostu do smoka, zaś dla grzybka przyporządkować jakiś randomowy numer. Dopisuję dwie linie:


    // Collision
    kupa.physicsBody?.collisionBitMask = bridgeCategory
    
    // Test - Set random mask to see no collision
    mushroom.physicsBody?.collisionBitMask = 0x1 << 5

Teraz odpalam to w symulatorze i... 😎. Master!

Wykonywanie akcji w momencie kolizji

Teraz rzecz ostatnia na dziś, ale napewno dla Ciebie bardzo ciekawa. Mianowicie chodzi o wykrycie konkretnej kolizji i wykonanie jakiejś akcji. To może być np. kolizja z monetą, przez co powinieneś zwiększyć ilość posiadanych monet przez gracza, albo z przeciwnikiem i wtedy ➡️ dead ☠️.

Do tego właśnie służy ostatnia z wymienionych wcześniej typów bitmasków ➡️ ContactTestBitmask. I tutaj ważny, ważny NOTE: Nie używaj ich zbyt często dla zbyt dużej liczby obiektów. Ten typ wykrywania kolizji jest bardzo wydajnościożerny, i będzie powodował spadek wydajności programu.

Ale do rzeczy - do wykrycia zdarzeń kolizji potrzebujemy zaadoptować protokół posiadający odpowiednie metody. Ten protokół to SKPhysicsContactDelegate, implementujący funkcję didBegin(_ contact: SKPhysicsContact)

Żeby przyporządkować naszą klasę do prokotołu, użyjemy zmiennej physicsWorld którą posiada nasza klasa SKScene. Czyli w metodzie didMove wklejamy linijkę:

self.physicsWorld.contactDelegate = self

No i teraz trzeba jeszcze dodać ContactTestBitmask do jakiegoś elementu. Zostańmy w zamku króla Koopy i niech nasz sprite reprezentujący most dostanie taką bitmaske:


    // ContactTestBitMask
    bridge.physicsBody?.contactTestBitMask = kupaCategory

to oznacza, że zostanie przechwycone zdarzenie kolizji między mostem a smoczkiem. Teraz w metodzie didBegin możemy jakoś przechwycić to zdarzenie i spowodować akcję. Ale hm... 🤔 w tej metodzie jest tylko parametr contact... to trochę mało.

No ale musimy wykorzystać co mamy. Zmienna typu SKPhysicsContact posiada parametry bodyA oraz bodyB, czyli dwa elementy, które skolidowały się ze sobą. W takim razie przy pomocy operatora porównania ( | ) sprawdzę, czy te dwa elementy posiadają tę samą kategorię bitmaski:


    let collison: UInt32 = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
    

I ostatnia rzecz to porównanie kategorii, które miały się ze sobą spotkać. W moim przypadku to była kupaCategory oraz brigdeCategory. Jeżeli wynik operacji porównania jest ten sam - mamy kolizje!


    func didBegin(_ contact: SKPhysicsContact) {
        let collison: UInt32 = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
        
        if collison == kupaCategory | bridgeCategory{
            print("COLLISION")
        }
    }

Test i jest! Wszystko działa pięknie 😎 przynajmniej u mnie...

Przechwytywanie zdarzeń kolizji

Podsumowanie

Dzisiaj był kawał naprawdę wartościowej wiedzy, bo tak naprawdę cały core tworzenia gier na iOS to zabawa z fizyką node'ów. Dlatego właśnie obiekt SKPhysicsBody jest ważny i to właśnie tutaj zaczyna się prawdziwa zabawa.

Podsumowaując ∑ do tworzenia fizyki służy obiekt SKPhysicsBody, który musi być powiązany z konkretnym node'm. SKPhysicsBody może przyjmować 3 rodzaje kategorii BitMaski, które odpowiadają za odpowiednio - kategorię, kolizję i zdarzenie.

SKPhysicsBody posiada parametr isDynamic, który określa czy element ma być odporny na działanie sił fizyki (false) czy też nie (true).

I teraz 👉 stała formułka znajdująca się pod każdym postem w tej serii ➡️ wszystko czego się tutaj uczysz przekładaj na swój projekt! Pamiętaj że przez praktykę się człowiek uczy, nie przez czytanie suchej teorii.

Na koniec jak zawsze proszę o jakiś feedback 🙏 czy się podobało, co sądzisz o całej serii. Jeśli realizujesz jakiś projekt, podziel się tym również wrzucając link do GIT'a etc. Możesz również podagać ze mną na Tweetku, Insta lub napisać coś ciekawego na fanpage'u na Facebook'u.

Jeszcze raz dzięki i do następnego!
@jkornat

Wprowadzenie do SpriteKit #4

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *