Lekce 9 - Přidání parallax efektu a životů hráče ve SpriteKit
V předchozí lekci, Další částicové efekty ve SpriteKit, jsme dokončili snad jednu z nejobsáhlejších částí naší iOS hry ve Swift - fyziku a kolize.
K dokončení Galaxy Invaders již zbývají jen drobnosti.
Střely zničených nepřátel
V první řadě vyřešíme problém laserové střely, kterou nepřítel vystřelí krátce před svým zničením. Taková střela se může objevit tam, kde již nepřítel není, což působí, jako by se tam najednou objevila sama. Střela totiž čeká neviditelná, díky čemuž nepřátelé nestřílí ve stejných intervalech.
Ideálně potřebujeme mechanismus, který střelu zruší v případě, že již má dojít k jejímu zviditelnění (skutečnému vystřelení) a daný nepřítel je v tu chvíli již zničený.
Uložení aktuální střely
Začneme přidáním vlastnosti currentShot
třídě
Enemy
:
weak var currentShot: SKNode?
Ta v sobě bude dočasně držet aktuální střelu. Pro jistotu je
weak
, aby případně nebránila správné dealokaci.
Při vytvoření střely v metodě createLaserFire()
ji
nastavíme:
currentShot = laser
Úprava laserMovement
Ve třídě Enemy
již zbývá jen úprava proměnné
laserMovement
, vracející sekvenci s pohybem nepřátelské
střely. Tuto sekvenci potřebujeme předat instanci nepřítele, pro kterou
střelu vytváříme, takže ji přeměníme na statickou funkci:
static func laserMovement(for enemy: Enemy) -> SKAction
A přidáme jednu SKAction
. Upravené tělo funkce vypadá
následovně:
let randomWait = SKAction.wait(forDuration: Double.random(in: 0...2)) let setCurrentShotNil = SKAction.run { [weak enemy] in enemy?.currentShot = nil } let fadeIn = SKAction.fadeIn(withDuration: 0.2) let move = SKAction.moveBy(x: 0, y: -1500, duration: 2.5) let remove = SKAction.removeFromParent() return SKAction.sequence([randomWait, setCurrentShotNil, fadeIn, move, remove])
Přidali jsme akci setCurrentShotNil
, kde díky
SKAction.run
můžeme provést jakýkoliv kus kódu. Zde nastavíme
currentShot
na nil
. Tuto akci spustíme v sekvenci
hned po náhodném čekání.
Pokud má zničený nepřítel nastavenou proměnnou
currentShot
, tak to tedy znamená, že střela stále neviditelně
čeká a ještě nebyla na nil
nastavena.
Zavolání metody
Teď musíme upravit, jak se metoda volá. Přejdeme tedy do
GameScene
a nejprve upravíme metodu
enemyFireTimerTick()
, konkrétně tento řádek:
laser.run(Enemy.laserMovement(for: child))
Potom stačí poslední úprava ve funkci missileHit()
. Třeba
hned za explozi přidáme novou if let
konstrukci:
if let enemy = enemy as? Enemy { enemy.currentShot?.removeAllActions() enemy.currentShot?.removeFromParent() }
Pokud střela nepřítele stále neviditelně čeká, odstraníme z ní
všechny akce a celkově ji odebereme. Pokud již čekání proběhlo, tak
dřívější akce setCurrentShotNil
nastavila proměnnou na
nil
a nemůže se nám stát, že bychom omylem odebrali střelu,
která již letí směrem k lodi hráče.
Bylo to sice komplikovanější, ale odstranili jsme problém a ukázali si
další možnosti SKAction
.
Parallax efekt
Tento efekt najdete ve spoustě 2D her, protože je jednoduchý a krásně vytváří efekt hloubky. Nejde přitom o nic komplexního. Základní varianta používá dvě nebo více nekonečných pozadí, která jsou průhledná a překrývají se. Pozadí se stále dokola pohybují, ale každé jinou rychlostí. Je to podobné, jako když jedete např. vlakem a díváte se ven z okénka. Sloupy kolem trati se míhají velmi rychle, vzdálenější stromy pomaleji a kopce v dálce se posouvají již jen velmi pomalu. Právě tímto vzniká efekt hloubky pozadí za hrou:

My tedy vlastně potřebujeme zajistit dvě nekonečně scrollující pozadí, každé s jinou rychlostí. To základní s vesmírem, které zabírá celou herní obrazovku, již máme. Bude ho tedy stačit pouze rozpohybovat. Druhé si můžete vytvořit nebo použít mé pozadí s meteory níže.
První pozadí - Vesmír
Nekonečné scrollování může znít jako výzva, jedná se ale pouze o
dvě textury, které se pohybují za sebou a vrací zpět na začátek. To je
celé. Uvidíte, že pomocí SKAction
to bude hotové raz dva.
Konfigurační metoda
Začneme s deklarací metody, která pozadí připraví:
func configureParallax(for imageName: String, duration: TimeInterval, zPosition: CGFloat) { }
Tuto metodu pak jen zavoláme pro jednotlivá pozadí, abychom nemuseli programovat každé zvlášť. A jako první vytvoříme první objekt pozadí:
let firstNode = SKSpriteNode(imageNamed: imageName)
firstNode.zPosition = zPosition
firstNode.anchorPoint = .zero
Nastavujeme také anchorPoint
pro snadnější výpočty. Potom
již stačí pomocí kopírování vytvořit identické pozadí a přidat obě
do scény. Obě pozadí, která na sebe navazují, budeme posouvat dolů a
jakmile jedno vyjede z obrazovky, přemístíme jej nad to druhé. Tímto
způsobem se budou stále vyměňovat a ve viditelné části obrazovky to bude
vypadat, že je pozadí nekonečné.
let secondNode = firstNode.copy() as! SKSpriteNode secondNode.position.y += secondNode.size.height - 1 addChild(firstNode) addChild(secondNode)
Druhou texturu posuneme nahoru o její výšku a odečteme pixel, abychom náhodou neměli mezeru v našem efektu.
Posledním krokem je definice nekonečné sekvence SKAction
a
jejich spuštění pro obě textury pozadí:
let move = SKAction.moveBy(x: 0, y: -firstNode.size.height, duration: duration) let reset = SKAction.moveBy(x: 0, y: firstNode.size.height, duration: 0) let loop = SKAction.sequence([move, reset]) let forever = SKAction.repeatForever(loop) firstNode.run(forever) secondNode.run(forever)
didMove()
Nyní zbývá v didMove()
zavolat nastavení parallax efektu a
smazat starší createBackground()
:
configureParallax(for: "background", duration: 25, zPosition: -5)
A vyzkoušíme:

Abyste mohli názorně vidět, co se děje, nahradil jsem dočasně naše pozadí dvojicí různobarevného a zrychlil:

Druhé pozadí - Meteory
Pro parallax samozřejmě potřebujeme alespoň dvě pozadí. Jelikož celou
přípravu máme hotovou, bude přidání meteorů otázka pouze dalšího
zavolání metody configureParallax()
:
configureParallax(for: "meteors", duration: 15, zPosition: -4)
Použitý obrázek je ke stažení níže:

A výsledek:

Životy hráče
Náš hráč je stále nesmrtelný. Tuto superschopnost mu nyní odebereme a bude se muset spolehnout na tradiční tři životy.
Aby hráč věděl, kolik životů mu zbývá, vypíšeme si je jako text.
Životy bychom samozřejmě mohli reprezentovat třeba obrázky srdcí nebo
miniaturou lodi hráče, takto si ale ukážeme práci s
SKLabelNode
, která slouží právě k zobrazování textu.
Definice labelu a životů
Nejdříve si v GameScene
definujeme proměnnou pro
SKLabelNode
a rovnou také pro počet životů:
let livesLabel = SKLabelNode(fontNamed: "Verdana-Bold") var lives: Int = 3 { didSet { livesLabel.text = "LIVES: \(lives)" } }
Pomocí didSet()
automaticky nastavíme text na aktuální
počet životů.
Přidání do scény
Dále si připravíme metodu, která livesLabel
nastaví a
přidá do scény:
func setupLivesLabel() { livesLabel.verticalAlignmentMode = .top livesLabel.position = CGPoint(x: frame.midX, y: frame.maxY - 70) addChild(livesLabel) lives = 3 }
Nastavení verticalAlignmentMode
určuje, od jakého okraje se
počítá pozice na ose Y. Existuje ještě
horizontalAlignmentMode
, které to samé dělá pro osu X. Ve
výchozím stavu jsou nastaveny na hodnotu .center
. Takže náš
label nastavíme k horní hraně a na horizontální střed.
Explicitně je tu ještě nastavení proměnné lives
, aby
došlo k aktivaci didSet()
a korektně se nastavil text.
Zavolání metody
Zbývá metodu zavolat uvnitř didMove()
:
setupLivesLabel()
Poslední, co v této lekci provedeme, je snížení životů o jeden, pokud
je hráč zasažen laserem. To uděláme v metodě
laserHitPlayer()
:
lives -= 1
Korektnější by samozřejmě bylo mít životy ve třídě
Player
, ale pak bychom museli složitěji aktualizovat label. Pro
naše potřeby současné řešení stačí.

V další lekci, Poškození hráče, menu hry a restart ve SpriteKit, přidáme poškození, konec hry a ukážeme si, jak vytvořit herní menu.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 4x (1000.31 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift