Lekce 7 - Dokončení kolizí ve SpriteKit
V minulé lekci, Přidání fyziky a detekce kolizí ve SpriteKit, jsme skončili u metody didBegin()
,
která nás informuje o kolizi mezi objekty, pro které jsme notifikace
zapnuli.
Dnes hru zprovozníme, takže hráč bude moci konečně ničit nepřátele.
Zjištění kolidujícího objektu
Od SpriteKit dostaneme v metodě didBegin()
parametr
contact
typu SKPhysicsContact
, který nás informuje o
tom, jaké dva objekty kolidovaly. Tradiční implementace této metody v první
řadě vytáhne oba SKNode
objekty pomocí guard
:
guard let nodeA = contact.bodyA.node else { return } guard let nodeB = contact.bodyB.node else { return }
Díky tomu víme, že můžeme kolizi zpracovat, protože máme oba objekty. Jenže, co z toho je raketa? Laser? Loď hráče?
Názvy objektů
Všechny SKNode
mají vlastnost name
, která nám v
tomto pomůže. Vrátíme se tedy nejdříve do třídy Player
a v
init()
nastavíme vlastnost name
:
name = "player"
Podobně si nastavíme rakety:
let missile1 = SKSpriteNode(imageNamed: "playerMissile") missile1.name = "missile" let missile2 = missile1.copy() as! SKSpriteNode
Využíváme kopírování, takže jméno stačí nastavit pouze jednou.
Naše hra má dost jednoduché kolize a pokud bychom zjistili, že jeden objekt je loď hráče, tak druhý musí být logicky laser. Přesto doporučuji pojmenovat si poctivě všechny objekty, ať je kód ve výsledku čitelnější.
Zbývá nastavit jména ve třídě Enemy
:
enemy.name = "enemy"
A laser:
laser.name = "laser"
didBegin()
Po krátké odbočce se můžeme vrátit do metody didBegin()
.
Zpracování kolize rozdělíme a nejdříve vyřešíme situaci, kdy raketa
trefí nepřítele.
Pro přehlednost si připravíme metodu, která se postará právě o tuto
situaci. V didBegin()
ji tedy stačí pouze správně poslat oba
objekty a bod, kde došlo ke kontaktu:
func missileHit(_ missile: SKNode, enemy: SKNode, at point: CGPoint) {
}
A zjištění, jaký z objektů je raketa a jaký nepřítel, vypadá pak následovně:
if nodeA.name == "missile" || nodeB.name == "missile" { if nodeA.name == "enemy" { missileHit(nodeB, enemy: nodeA, at: contact.contactPoint) } else { missileHit(nodeA, enemy: nodeB, at: contact.contactPoint) } }
Zásah
Pojďme na kolizi nějak reagovat. V metodě missileHit()
pro
začátek při kolizi odstraníme oba objekty ze scény:
missile.removeFromParent() enemy.removeFromParent()
Pole enemies
Jenže nepřátele máme uložené také v poli enemies
, které
slouží pro snadný přístup k nim, když chceme, aby stříleli. Máme na
výběr, buď se starat o to, abychom zasažené nepřátele odstranili také z
pole, nebo to vymyslet lépe, aby pole nebylo potřeba.
GameScene
nabízí vlastnost children
obsahující
všechny objekty ve scéně. My ale používáme enemyAnchor
pro
snadnou animaci pohybu nepřátel, takže už je vlastně v jedné kolekci
máme.
Jednoduše tedy proměnnou enemies
smažeme. Dále je třeba
upravit createEnemyWave()
a odstranit přidávání nových
nepřátel do již smazaného pole. Poslední nutnou úpravou je metoda
enemyFireTimerTick()
.
Místo pole enemies
budeme přes for
cyklus
iterovat enemyAnchor.children
, který ale vrací pole typu
[SKNode]
. Je hromada způsobů, jak se s tímto vypořádat. Mohli
bychom třeba přetypování provádět přes guard
nebo
if let
v těle cyklu.
My ale využijeme silné možnosti, tzv. pattern matching, pomocí
case let
zápisu. Rovnou si jej ukážeme celý:
for case let child as Enemy in enemyAnchor.children
Jestli vás for case let
za sebou vyděsilo, ani se nevidím.
Poprvé jsem na tento zápis koukal dost nevěřícně. Postará se o to, aby
lokální proměnná child
byla typu Enemy
a my s ní
mohli pracovat jako dříve. Samozřejmě je nutné přejmenovat dřívější
lokální proměnnou enemy
v těle cyklu na child
.
Pomocí case let
můžete ve for
cyklu
jednoduše rozbalovat Optional
hodnoty, stačilo by místo
child
napsat child?
. Pokud bychom pracovali s polem
Optional
hodnot, v těle cyklu by již Optional
nebylo. Stejně tak se case let
dá využít v
konstrukci switch
.
Hru můžeme zapnout a přesvědčit se, že rakety konečně likvidují protivníky:

Oprava kolizí
Hádám, že jste si všimli, že kolize fungují divně při zásahu rakety. Někdy dokonce jakoby raketa ignorovala nepřítele a letěla dál.
SpriteKit nám nabízí poměrně jednoduchou možnost, jak ověřit
nastavení fyzikální reprezentace pro objekty. Přejdeme tedy do
GameViewController.swift
a v metodě viewDidLoad()
najdeme řádek view.showsNodeCount = true
.
Jak prozrazuje název, určuje, jestli má scéna zobrazovat počet objektů. To je pro případ, abychom si ověřili, že korektně odebíráme ty již nepotřebné. Přidáme další řádek a to:
view.showsPhysics = true
Jedná se o další debug konfiguraci pro zobrazení fyziky. Aktuálně nastavené fyzikální reprezentace objektů budou zobrazeny světle modrým obrysem. Zapneme hru a okamžitě vidíme, kde je problém:

Jak sami vidíte, fyzikální reprezentace a vykreslení nepřátel nesedí,
zbytek funguje dobře. Problém je v nastavení anchorPoint
pro
naše nepřátele. Neuvědomil jsem si v dřívější lekci, že jde pouze o
vizuální nastavení vykreslování a později nastane problém s fyzikou. Tak
jsme si alespoň mohli ukázat, jak problémy hledat.
Změna anchorPoint
Pokud bychom vytvářeli fyzikální reprezentaci jako obdélník nebo kruh,
šlo by posunout střed a tím problém vyřešit. My ale používáme texturu,
takže nám nezbývá nic jiného, než nastavení anchorPoint
vrátit zpět a upravit kód generování vlny nepřátel, aby byly na
středu.
Úpravy tedy provedeme v metodě createEnemyWave()
. Nejdříve
odstraníme nastavení anchorPoint
a potom upravíme počítání
xPosition
v těle cyklu takto:
let xPosition = xOffset + CGFloat(i) * (enemySize + enemySpacing) + enemySize / 2
Vlastně jsme přidali přičtení poloviny šířky nepřítele, jako
kompenzaci za upravený anchorPoint
.
Můžeme zkusit:

Teď už budou kolize fungovat podle očekávání
V příští lekci, Další částicové efekty ve SpriteKit, přidáme částicové efekty exploze a laseru
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 8x (987.57 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift