Animacje, poruszanie i akcje kolizyjne

20 grudnia 2017

SpriteKit

Święta tuż tuż 🎄 ale to nie oznacza, że nie można jeszcze trochę popracować 😁. W ramach przygotowań do Bożego Narodzenia wrzucam dla Ciebie kolejną część z cyklu “Wprowadzenie do SpriteKit”, czyli serii postów dotyczących programowania gier na iOS z pomocą natywnego framework’a SpriteKit.

Jeśli jesteś tu po raz pierwszy, to zapraszam Cię do zerknięcia na poprzednie artykuły z tej serii, o tutaj:

Dzisiaj zaś będzie trochę powtórzenia (dotyczącego kolizji 🔥), pokażę Ci jak w praktyce działać w przypadku kolizji, jak animować tekstury oraz co to jest emitter i jak go wykorzystać w swoim projekcie.

Dodam też że jest to przedostatni już wpis w tej serii… zleciało mega szybko, chciaż bawiłem i bawię się przy tej niej świetnie 😁.

Nie przedłużając ➡️ do kodu!

Wykrywanie kolizji – raz jeszcze

Na wstępie przypomnę Ci w krótkich żołnierskich słowach w jaki sposób wykrywać kolizję w SpriteKit oraz jak reagować na nie, gdy już zajdą.

Otóż do wykrywania kolizji używamy tzw. identyfikatorów BitMask zapisanych szesnastkowo. Każde physicsBody musi posiadać jakąś kategorię BitMask (bitMaskCategory), wiadomo – żeby wykryć kolizję z ciałem o innej kategorii.

Wracam do naszego przykładu z przed 2 tygodni (wpis o tutaj) ➡️ stworzyliśmy wtedy kategorię dla mostu (bridge) oraz dla króla Koopy i dzięki elementowi SKPhysicsBody ustawiliśmy kolizyjną bitmaskę. Dzięki temu Koopa rzucany z powietrza wyląduje na moście.

Koopa loaded

Kolizję wykrywaliśmy zaś w metodzie:

func didBegin(_ contact: SKPhysicsContact) { } 

Poprzez odwołania do kategorii bitmask elementów contact.bodyA oraz contact.bodyB, o tak:


func didBegin(_ contact: SKPhysicsContact) {
    let collison: UInt32 = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
    
    if collison == kupaCategory | bridgeCategory{
        // Do something
    }
}

I tutaj nowa rzecz – w jaki sposób wykryć, które ciało jest które? 🤔
Ponieważ kategorie naszych ciał to są zwykłe liczby, tyle że zapisane szesnastkowo, to przecież możemy je porównać. Deklaracja naszych identyfikatorów dla kategorii wygląda tak:


let kupaCategory: UInt32 =      0x00000001 << 0  // 1
let playerCategory: UInt32 =  0x0000001 << 1   // 2
let bridgeCategory: UInt32 =    0x00000001 << 2  // 4

Zatem kategoria dla smoka jest najniższa (wynosi 1) a dla mostu ➡️ 4.


func didBegin(_ contact: SKPhysicsContact) {
    
    var koopa: SKPhysicsBody?
    var brigde: SKPhysicsBody?
        
    if contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask{
        brigde = contact.bodyA
        koopa = contact.bodyB
    } else {
        brigde = contact.bodyB
        koopa = contact.bodyA
    } 
}

No i mamy to! Wiadomo które ciało jest które i możemy teraz podejmować jakieś akcje.

Powtórzenie z kolizji było o tyle istotne, że w dalszej części będziemy już dodawać konkretne działania, kiedy zostanie wykryta kolizja.

Animacja tekstur

Jak sobie zajrzysz do App Store w poszukiwaniu jakieś gierki na czas dojazdów środkami komunikacji miejskiej do pracy to napewno zwrócisz w niej uwagę na animacje postaci. Jak ludzik się porusza, to np. porusza swoimi niezgrabnymi nogami, albo gdy podskakuje to z ręką uniesioną do góry.

No właśnie, czas trochę poanimować te tekstury, żeby to wyglądało profesjonalnie. Ja dzisiaj działam dalej z Mario ♥️. Będę chciał go przeciwstawić ze smokiem Koopą.

Na początek oczywiście dodaję sprite’a i ustawiam jego parametry i teskturę.

Mario added to editor

I oczywiście dodaję sprite’a w kodzie, zapisuję identyfikator dla kategorii oraz tworzę SKPhysicsBody.

NOTE: Nie pamiętasz? Zajrzyj do poprzedniego wspisu : tutaj

Na tem moment wygląda to tak:


import SpriteKit
import GameplayKit
    
class GameScene: SKScene, SKPhysicsContactDelegate {
    
    private var kupa: SKSpriteNode?
    private var player: SKSpriteNode?
    private var brigde: SKSpriteNode?

    let kupaCategory: UInt32 =      0x00000001 << 0  // 1
    let playerCategory: UInt32 =  0x0000001 << 1   // 2
    let bridgeCategory: UInt32 =    0x00000001 << 2  // 4
    
    override func didMove(to view: SKView) {
        self.physicsWorld.contactDelegate = self
        createNodes()
    }
        
    func createNodes(){
        if let kupa = self.childNode(withName: "kupa") as? SKSpriteNode,
            let player = self.childNode(withName: "player") as? SKSpriteNode,
            let bridge = self.childNode(withName: "bridge") as? SKSpriteNode{
                
            self.player = player
            playerStartPosition = CGPoint(x: 80, y: 300)
            
            kupa.physicsBody = SKPhysicsBody(rectangleOf: kupa.size)
            kupa.physicsBody?.categoryBitMask = kupaCategory
             
            self.player?.physicsBody = SKPhysicsBody(rectangleOf: player.size)
            self.player?.physicsBody?.categoryBitMask = playerCategory
                
            bridge.physicsBody = SKPhysicsBody(rectangleOf: bridge.size)
            bridge.physicsBody?.isDynamic = false
            bridge.physicsBody?.restitution = 0.75
            bridge.physicsBody?.categoryBitMask = bridgeCategory
        
            // Collision
            kupa.physicsBody?.collisionBitMask = bridgeCategory
            
            // Test - Set random mask to see no collision
            self.player?.physicsBody?.collisionBitMask = bridgeCategory
            
            // ContactTestBitMask
            player.physicsBody?.contactTestBitMask = kupaCategory
                
        }
    }
    ...

Jeśli w tym momencie skompilujesz apkę to twoje postacie powinny pojawić się naprzeciw siebie…

Koopa vs Mario

Teraz 👉 co chcę zrobić:
Jeżeli użytkownik dotyka ekranu, chcę żeby Mario animował swój ruch do przodu. W sensie chód (tak się mówi? 😁).
Skoro dotyk, to od razu powinna przyjść Ci na myśl metoda:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)

I dobrze. Teraz jak zrobić animację.
Na początek musisz mieć przygotowane tekstury, które będą się podmieniały imitując ruch. Ja przygotowałem sobie takie:

Mario textures

Pierwsza to tekstura stojącego bohatera.

Dwie następne to właśnie te, które będą podmieniane w momencie, jak dziarski hydraulik będzie się poruszał.

Kiedy masz już gotowe tekstury, to teraz te które odpowiadają za ruch wrzuć do jednego katalogu. Nadaj mu nazwę jaką chcesz (byle łatwo by Ci było odwołać się do tej nazwy w kodzie 😂) zakończoną rozszerzeniem:

.atlas

np.

mario.atlas

Next ➡️ wrzuć katalog do swojego projektu z grą:

Project Structure

Ok, teraz do kodu ➡️ tworzę sobie metodę initMoveAnimation() w której tworzę nowy obiekt NSTextureAtlas. Będzie też potrzeba zmienna globalna, która przechowa nam nasze tekstury.

Zatem – początek naszej klasy wygląda tak:


class GameScene: SKScene, SKPhysicsContactDelegate {
        
    private var textureArray: [SKTexture]?
    private var atlas: SKTextureAtlas?
    
    ...
    
override func didMove(to view: SKView) {
    self.physicsWorld.contactDelegate = self
    createNodes()
    initMoveAnimation()
}    
    ...

a metoda initMoveAnimation() tak:


    func initMoveAnimation(){
        atlas = SKTextureAtlas(named: "mario")
        
        textureArray = [SKTexture]()
        textureArray?.append(SKTexture(imageNamed: atlas!.textureNames[0]))
        textureArray?.append(SKTexture(imageNamed: atlas!.textureNames[1]))
        
    }

Dobrze, wszystkie tekstury znajdują się teraz w kolekcji textureArray.
Przechodzimy zatem do metody touchesBegan(), a w niej po kolei:

1. zmiana testury gracza na pierwszą z kolekcji;
2. stworzenie akcji (SKAction) podmieniającą tekstury;
3. stworzenie akcji zapętlającej podmienianie;
4. przypisanie akcji do obiektu player.

Proste? Proste 😊 ⬇️


override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    player?.texture? = SKTexture(imageNamed: atlas!.textureNames[0]) // Ad.1
    let moveAnimation = SKAction.animate(with: textureArray!, timePerFrame: 0.08) // Ad.2
    let repear = SKAction.repeatForever(moveAnimation) // Ad.3
    player?.run(repear) // Ad.4
}

Ok, odpalamy 🚀 i? I fajnie, tylko że Mario się nie zatrzymuje.
No nie, bo zapomnieliśmy zdjąć z niego akcję po tym, jak użytownik odklei paluch od ekranu. Poza tym dodajmy jeszczę powrót do swojej wyjściowej tekstury:


    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        player?.removeAllActions()
        player?.texture? = SKTexture(imageNamed: "mario_stand")
    }

I teraz to już like a charm 🔥

Skoro już wiemy co i jak, to w bardzo prosty sposób możemy dodać naszemu bohaterowi jeszcze możliwość poruszania do przodu. W metodzie touchesBegan() po dodaniu animacji zmiany tekstur dodajmy jeszcze akcję poruszania:


    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        player?.texture? = SKTexture(imageNamed: atlas!.textureNames[0])
        let moveAnimation = SKAction.animate(with: textureArray!, timePerFrame: 0.08)
        let repear = SKAction.repeatForever(moveAnimation)
        player?.run(repear)
    
        // Move 
        let action = SKAction.moveBy(x: 0.4, y: 0, duration: 0.001)
        let repeatAction = SKAction.repeatForever(action)
        player?.run(repeatAction)
    }

No i spoko! Kolejny etap to zatrzymanie w momencie spotkania ze smoczydłem 🐲.

Kolizja, wybuch i restart

Kolizję umiesz już dodać, więc tutaj magii nie ma:

    func didBegin(_ contact: SKPhysicsContact) {
        let collison: UInt32 = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
        
        if collison == kupaCategory | playerCategory{
            // die Mario 😥
        }
    }

Trzeba się zastanowić nad metodą dead().
Po pierwsze – podobnie jak przy oderwaniu palca, trzeba zdjąć z player’a wszystkie dodane wcześniej akcje.
Po drugie – chciałbym przenieść Mario znowu na początek. Do tego przyda się dodatkowa zmienna, która na samym początku zapisze nam pozycję startową (playerStartPosition: CGPoint).
Po trzecie – chciałbym zobaczyć wybuch przy spotkaniu z Koopą.
Po czwarte – w momencie przenoszenia z miejsca zgonu na start Mario ma być niewidoczny dla użytkownika przez sekundę.

Dobra, wymagania mamy zebrane. Żeby osiągnąć punkt pierwszy należy jak zawesze zrobić removeAllAction() na naszym player’ze.
Punkt drugi – dodamy jednorazową akcję move(to: CGPoint, duration: Double).
Jako pierwszy parametr użyjemy naszej zmiennej playerStartPosition.
Trzy ➡️ to zostawmy na koniec
Cztery – tak jak w przypadku zwykłych aplikacji użyjemy parametru isHidden oraz GCD do opóźnienia akcji w czasie.

Bez punktu trzeciego będzie to wyglądało tak:


    func dead() {
        player?.removeAllActions() // 1
        player?.isHidden = true // 4
        
        let goToStartAction = SKAction.move(to: playerStartPosition!, duration: 0.5) // 2
        player?.run(goToStartAction) // 2 cd
        
        // 4 cd
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1) ) {
            self.player?.isHidden = false
            self.removeChildren(in: [fire!])
        }
        
    }

Sprawdzam…. 👍.
To było proste, ale za to daje już mega fajny efekt. W tym momencie powinieneś już kminić tysiąc zastosowań i różnych pomysłów na gierki przygodowe 😁.

NOTE: Tak na marginesie jak coś stworzycie to się dzielcie tą informację!

Emitter

Emitter, a właściwie SKEmitterNode (Doc) to takie bardzo fajne narzędzie do tworzenia efektów wizualnych z poziomu Xcode. Nie będę się zagłębiał w każdy element dostępny do modyfikacji w edytorze, bo można o tym pisać dużo i tworzyć cuda 😁.

Na ten moment chcę stworzyć prostą animację wybuchu, która będzie się pojawiała w momencie spotkania naszych bohaterów w jednym punkcie.

Żeby stworzyć emitter należy dodać nowy plik do naszego projektu, a następnie wybrać opcję SpriteKit Particle:

Particle

Następnie wybrać sobie jakiś Template. Ja wybieram Fire. Potem nazwa emittera i ok, gotowe. Jak widzisz w strukturze projektu pojawił się plik .sks. Jak otworzysz plik w edytorze to ujrzysz właśnie wybrany przez siebie efekt.

Fajne to, nie? 😊 Ja nie komplikuje sprawy i biorę prostą animację ognia. Ty się baw do woli!

Emitter dziedziczy z Node, dlatego zachowuje się jak dotychczas znane Ci już obiekty. Zatem żeby dodać ten efekt, należy najpierw stworzyć obiekt SKEmitterNode, a następnie wrzucić go jako parametr do metody self.addChild.


    let fire = SKEmitterNode(fileNamed: "Fire")
    fire?.position = (player?.position)!
    self.addChild(fire!)

Jak widzisz warto by jeszcze nadać mu pozycję wyjściową. Ponieważ ten kod dokładnie znajdzie się w metodzie dead(), to pozycja emittera będzie ostanią znaną pozycją gracza przed śmiercią.

Natomiast po odrodzeniu gracza trzeba ten emitter z naszej sceny wywalić 🗑.

Dla porządku, cała metoda wygląda tak:


    func dead() {
        player?.removeAllActions()
        player?.isHidden = true
    
        let fire = SKEmitterNode(fileNamed: "Fire")
        fire?.position = (player?.position)!
        self.addChild(fire!)
        
        let goToStartAction = SKAction.move(to: playerStartPosition!, duration: 0.5)
        player?.run(goToStartAction)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1) ) {
            self.player?.isHidden = false
            self.removeChildren(in: [fire!])
        }
        
    }

I teraz gdzie tą metodę wywołać? A no tu, gdzie ma się zadziać, czyli przy detekcji kolizji:


    func didBegin(_ contact: SKPhysicsContact) {
        let collison: UInt32 = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
        
        if collison == kupaCategory | playerCategory{
            dead()
        }
    }

Ok. Chyba wszystko. Testujemy? 🙌

Ależ działa! Wszystko co zaplanowaliśmy na początku zostało zrealizowane.

Podsumowanie

Po dzisiejszym wpisie jesteś już w stanie zrobić kilka następujących rzeczy:

1. Dodawać animację do tesktury.
2. Wykrywać i reagować na kolicję dotyczącą konkretnych elementów sceny.
3. Dodawać i edytować emittery.

Tak całkowicie na poważnie – jeżeli skrupulatnie przeszedłeś przez wszystkie dotychczasowe wpisy, jesteś już w stanie stworzyć swoją pierwszą autorską grę! Poważnie!

W ostatnim wpisie, który mam nadzieję pojawi się jescze przed nowym rokiem, pokażę Ci jak za zaimplementować losowe ruchy np. przeciwników oraz jak łączyć ze sobą poprawnie kilka kolizji. Zrobimy też prosty panel nawigacyjny twojej gry oraz dodamy prosty mechanizm naliczania punktów.

Postaram się także w momencie publikacji ostatniego wpisu wrzucić wszystko na blogowy Git, żebyś mógł tak w całości “przekartkować” projekcik.

Tymczasem jak zawsze bardzo serdecznie zaczęcam Cię do dzielenia się spostrzeżeniami ze światem i (oczywiście) ze mną 😊.

Daj znać na portalach

Lub wpadnij na fanpage na Facebook’u!

Dzięki i do następnego wpisu!
@jkornat

Kategorie: SpriteKitSwift

Dodaj komentarz

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