C# týden Slevový týden - Březen
Využij náš slevový týden a získej až 30 % bodů navíc zdarma! Zároveň také probíhá C# týden se slevou na e-learning až 80 %
Hledáme fulltime programátora do ITnetwork týmu -100% homeoffice, 100% časově flexibilní #bezdeadlinu Mám zájem!

Lekce 10 - Poškození hráče, menu hry a restart ve SpriteKit

V předchozí lekci, Přidání parallax efektu a životů hráče ve SpriteKit, jsme implementovali životy hráče. Dnes se budeme věnovat poškození lodi, hernímu menu a restartu hry.

Poškození hráče

Prvně si ukážeme, jak simulovat poškození.

Textury

Balíček textur od Kenney.nl obsahuje také varianty poškození pro jednotlivé lodě hráče. Jedná se vlastně o obrázky, kterými překryjeme hlavní texturu a tím bude poškození vidět. Další možností by bylo mít kompletní obrázky poškozené lodi a měnit vlastnost texture u SKSpriteNode.

Překryvné obrázky poškození lodě vypadají takto a na lodi budou indikovat kolik ji ještě zbývá životů:

applyDamage()

Ve třídě Player si přidáme metodu applyDamage(). Ta bude jako parametr očekávat zbývající životy, takže v našem případě buď 2, 1 nebo 0. Ještě před implementací si připravíme instanci SKSpriteNode pro první poškození:

private let damageNode = SKSpriteNode(imageNamed: "playerShip2_damage1")

A metodu implementujeme:

switch livesLeft {
        case 3:
            break
        case 2:
            damageNode.zPosition = 5
            addChild(damageNode)
        case 1:
            damageNode.texture = SKTexture(imageNamed: "playerShip2_damage2")
        case 0:
            damageNode.texture = SKTexture(imageNamed: "playerShip2_damage3")
        default:
            assertionFailure("Invalid parameter")
}

Při prvním zásahu přidáme poškození do scény, aby překrylo loď hráče a při dalších jen upravíme texturu poškození.

Větev default jsme vyřešili s pomocí šikovné funkce assertionFailure(), abychom byli upozorněni v případě, že odněkud pošleme neplatný parametr. case 3 je zde z důvodu, že metodu budeme volat přes didSet v GameScene a na začátku nastavujeme lives na 3, aby se propsal text také do SKLabelNode.

Společně s assert() se jedná o skvělé pomocníky při vývoji, můžete se pomocí nich totiž ujišťovat, že je program v takovém stavu, v jakém očekáváte. assert() totiž jako první parametr očekává podmínku. V release buildu tyto funkce vůbec nejsou, takže uživatelům aplikace/hra nespadne, pokud se stane něco neočekávaného.

Zavolání metody

Upravíme tedy proměnnou lives a při změně nastavíme poškození hráči:

var lives: Int = 3 {
        didSet {
            livesLabel.text = "LIVES: \(lives)"
            player.applyDamage(livesLeft: lives)
        }
}

A hru můžeme vyzkoušet:

Menu

Určitě budete souhlasit, že vrhnout hráče po zapnutí hry hned do boje není úplně ideální. Chtělo by to nějaké menu.

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

Menu bychom mohli zkusit udělat přes SpriteKit, ovšem nabízí se jednodušší možnost. Sice jsme pracovali v posledních dílech výhradně v GameScene, ale na pozadí je UIViewController, v našem případě pojmenovaný GameViewController, a tradiční UIKit aplikace s Main.storyboard.

Nový View Controller

Hlavní menu tedy vytvoříme jako obyčejnou UIKit obrazovku. Tato část se netýká SpriteKit, takže ji projdeme v rychlosti. Práce s UIKit je případně detailně popsána v kurzu Vyvíjíme iOS aplikace ve Swift. Do Main.storyboard přetáhneme nový View Controller a nastavíme ho jako initial (zaškrtnutím "Is Initial View Controller", poznáte to také podle šipky ukazující na tuto obrazovku).

Bude nám stačit pozadí (klasický UIImageView), další obrázek pro logo, které jsem připravil v Affinity Photo a konečně tlačítko (UIButton), ze kterého přetáhneme segue (za držení klávesy Control, viz ukázka níže) rovnou na druhou obrazovku. Zde jsem zvolil "Present Modally" a "Full Screen". Vlastně ani nepotřebujeme třídu pro tuto obrazovku.

Vytvoření Segue z tlačítka na druhou obrazovku

UIImageView pro pozadí jsem pomocí AutoLayout nastavil 0 od všech hran (přímo k Superview, takže je nutné odškrtnout margin a ignorovat Safe Area). Nezapomeňte nastavit Content Mode na "Aspect Fill", aby obrázek zaplnil celé okno. UIImageView s logem hry stačí vycentrovat a nastavit mu třeba 50 bodů od horní hrany. Tlačítko opět vycentrujeme a nastavíme mu vertikální odsazení od loga. Tím je layout hotový.

Výsledek vypadá takto:

Herní menu pro SpriteKit iOS hru ve Swift

SpriteKit menu by fungovalo tak, že bychom např. vytvořili novou scénu, která by měla tyto samé prvky (pomocí SKSpriteNode) a po detekci výběru play bychom pomocí SpriteKit přepnuli na jinou scénu. Výhodou tohoto postupu by bylo, že bychom například mohli dosáhnout efektu, že logo "odlétne" pryč a místo něj přilétne první vlna nepřátel.

Vrácení hráče zpět do menu

Nyní potřebujeme způsob, jak hráče vrátit zpět do menu. Pro zjednodušení zobrazíme dialog jen když dojde k jeho zabití nepřáteli. Ve skutečné hře bychom měli pro tyto potřeby ještě další tlačítko.

Zobrazení dialogu můžeme vyřešit přes UIAlertViewController, ale ten nevytvoříme z herní scény. Potřebujeme tedy vyřešit komunikaci mezi scénou a GameViewController. Nejjednodušší bude držet si referenci ve scéně pro snadný přístup. Zde ale pozor, protože reference musí být weak. View controller si totiž již drží silnou referenci herní scény, takže by jinak došlo k retain cycle.

weak var controller: GameViewController?

GameViewController

Přejdeme do GameViewController a upravíme viewDidLoad(). Již nám nestačí získat jakoukoliv herní scénu, takže přidáme přetypování na konec if let a nastavení reference:

if let scene = SKScene(fileNamed: "GameScene") as? GameScene {
    // Set the scale mode to scale to fit the window
    scene.scaleMode = .aspectFill
    scene.controller = self
..

A přidáme metodu pro zobrazení konce hry:

func showGameOver() {
        let ac = UIAlertController(title: "Game over!", message: nil, preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "Retry", style: .default, handler: { (_) in
            // doplníme
        }))
        ac.addAction(UIAlertAction(title: "Menu", style: .cancel, handler: { [weak self] (_) in
            self?.dismiss(animated: true, completion: nil)
        }))
        present(ac, animated: true)
}

GameScene

Zbytek vyřešíme v GameScene. Přidáme si proměnnou pro hlídání, jestli náhodou hra neskončila:

var isGameOver = false

Pokud ano, tak deaktivujeme kolize, respektive reagování na ně hned na začátku metody didBegin():

guard !isGameOver else { return }

A zbývá přidat logiku na začátek bloku didSet proměnné lives:

if lives < 0 {
    isGameOver = true
    controller?.showGameOver()
    return
}

Jakmile hráč ztratí všechny životy, zobrazíme náš dialog, který zatím funguje pouze pro vrácení se do menu.

Restart hry

Zbývá nám dořešit situaci, kdy uživatel zvolí "Retry" volbu v dialogu. V takovém případě chceme hru resetovat.

Mohlo by vás napadnout prostě vrátit zpět počet životů, znovu vytvořit nepřátele, odstranit poškození a tak podobně. To je ale zbytečně moc práce a riskujete, že na něco zapomenete. Mnohem lepší je prostě zobrazit novou instanci scény, SpriteKit nám k tomu poskytne i hezké animace.

Takže se vrátíme zpět do GameViewController a vytvoříme zde metodu restartGame(). Dále si přidáme ještě metodu createGameScene(), ať neduplikujeme kód z viewDidLoad().

createGameScene()

Kód metody je následující:

func createGameScene() -> GameScene? {
        if let scene = SKScene(fileNamed: "GameScene") as? GameScene {
            // Set the scale mode to scale to fit the window
            scene.scaleMode = .aspectFill
            scene.controller = self

            return scene
        }
        return nil
}

Většina metody pochází z viewDidLoad(), kam během chvilky doplníme nové vytvoření scény za pomoci nově vytvořené metody createGameScene().

Logika je na jednom místě a můžeme ji později rozšířit a nebát se, že třeba restart hry bude fungovat jinak než její prvotní zapnutí. Samozřejmě ještě upravíme vnitřek viewDidLoad():

if let scene = createGameScene() {
    // Present the scene
    view.presentScene(scene)
}

restartGame()

A můžeme se vrátit k restartGame() a vytvořit novou scénu s animovaným přechodem:

func restartGame() {
    if let scene = createGameScene() {
        let transition = SKTransition.flipHorizontal(withDuration: 1)
        skView.presentScene(scene, transition: transition)
    }
}

SKTransition nabízí spoustu efektů, takže se rozhodně nebojte experimentovat. Protože potřebujeme scénu prezentovat z SKView a ne z UIView (který je ve skutečnosti SKView), připravíme si vlastnost pro snadné získání:

var skView: SKView {
    return view as! SKView
}

Samozřejmě nesmíme zapomenout restartGame() zavolat z dialogu, na místě, kde jsem nechal komentář // doplníme.

restartGame()

Můžete vyzkoušet, že volba "Retry" funguje korektně :-)

Zbývá nám již jen poslední lekce, Nekonečné vlny nepřátel a jejich animace ve SpriteKit, kde upravíme vlny nepřátel a řekneme si, jak by se hra dala ještě vylepšit.


 

Stáhnout

Staženo 4x (1.07 MB)
Aplikace je včetně zdrojových kódů v jazyce Swift

 

 

Článek pro vás napsal Filip Němeček
Avatar
Jak se ti líbí článek?
1 hlasů
Autor se věnuje vývoji iOS aplikací (občas macOS) či těch webových ve frameworku Django. Twitter: @nemecek_f | GitHub nemecek-filip - mrkněte na veřejné projekty
Předchozí článek
Přidání parallax efektu a životů hráče ve SpriteKit
Všechny články v sekci
Tvorba iOS her ve Swift
Miniatura
Následující článek
Nekonečné vlny nepřátel a jejich animace ve SpriteKit
Aktivity (6)

 

 

Komentáře

Avatar
Radek
Člen
Avatar
Radek:3. února 21:49

Nefungovalo mi to úplně správně, nakonec jsem tam udělal dvě drobné změny.
Jakmile vyskočí alert, tak bylo na pozadí slyšet, jak tam ty nepřátelé pořád střílí. Zkusil jsem to vyřešit přes pauzu, případně zastavit timer nepřátelské střelby, odebrat hráči akce a jeho vlastní node.

Druhá věc, že mi nefungoval správně restart hry (ani když jsem si stáhl projekt a použil vzorový GameViewContro­ller). Možná to mám někde tak zcustomizované, že se to třískalo. Prostě se vždy použila ta původní gamescene před prohrou.
Nakonec jsem to udělal tak, že tu prohranou scénu dám na nil a tím zajistím, že příště se udělá nová.

Nakonec ještě postřeh k Apple TV: od té doby kdy jsem dal UIView jako menu, tak mi nefunguje úplně správně registrace gamepad ovladačů, původně jsem měl jen dva řádky připojení/odpojení ovladačů, fungovalo to správně včetně startu hry s již připojeným ovladačem.

NotificationCenter.default.addObserver(self, selector: #selector(connectControllers), name: NSNotification.Name.GCControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(disconnectControllers), name: NSNotification.Name.GCControllerDidDisconnect, object: nil)

Po té, co je první UIView, tak musím ještě zavolat zvlášť connectContro­llers() protože pokud byl ovladač připojen již v době startu hry, tak se o tom hra nedozví.
Věřím, že způsobeno nějakou mou nezkušeností a jde to elegantněji observery na lepším místě.

 
Odpovědět
3. února 21:49
Avatar
Filip Němeček
Redaktor
Avatar
Odpovídá na Radek
Filip Němeček:3. února 22:00

Díky za doplnění ohledně dobrodružství s Apple TV :-) To s tou scénou je zvláštní, korektně se mi restartovala, když jsem zkoušel. Měl jsi tam tu referenci jako weak ve scéně?

 
Odpovědět
3. února 22:00
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
Radek
Člen
Avatar
Odpovídá na Filip Němeček
Radek:3. února 22:29

jo jo, v GameScene:

weak var controller: GameViewController?

ale když to šlo vyřešit jinak, dal jsem to :-)

 
Odpovědět
3. února 22:29
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 3 zpráv z 3.