Lekce 5 - Nepřátelé střílí zpět a dokonce laserem ve SpriteKit
V předchozí lekci, Střílení raket a další částicové efekty ve SpriteKit, jsme vyzbrojili hráče raketami, kterým jsme přidali částicové efekty a ozvučení.
Hráč už je vyzbrojený a je jedině fér, aby nepřátelé mohli střílet také. Abychom nedělali znovu to samé jako u hráče, vyzbrojíme nepřátele lasery. Pro změnu budou střílet jen jednu střelu.
Třída Enemy
Postup bude poměrně podobný jako v případě hráče, takže si ve
složce Models
vytvoříme třídu Enemy
. Nezapomeňte
přidat import SpriteKit
.
Instanciace
Protože konstruktory jsou ve Swiftu celkem komplikované (pravidla,
convenience init
, required init
a tak), vyřešíme
vytváření nových nepřátel pomocí statické metody. Ta nám dovolí při
vytváření určit, jakou variantu nepřítele chceme:
class Enemy: SKSpriteNode { static func create(variant: Int) -> Enemy { precondition(1...3 ~= variant, "Invalid enemy variant") let texture = SKTexture(imageNamed: "enemyBlack\(variant)") let enemy = Enemy(texture: texture, color: .clear, size: texture.size()) return enemy } override init(texture: SKTexture?, color: UIColor, size: CGSize) { super.init(texture: texture, color: .clear, size: texture?.size() ?? CGSize.zero) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
Já mám tři typy nepřátel, tak jsem rovnou přidal možnost určit, jaký se má použít.
Protože lze jako parametr zadat jakékoliv celé číslo, tak jsem přidal
kontrolu pomocí precondition
. Je to taková šikovná metoda na
ověřování vstupů. Pokud podmínka neplatí, tak dojde k vyvolání
výjimky. Swift ji hodně používá interně, třeba když použijete špatný
index pro přístup do pole.
A také jsem ji použil jako takovou záminku pro představení stylového
operátoru ~=
Skvěle se hodí pro případ, kdy chceme zjistit, jestli dané číslo spadá
mezi určité rozmezí. Jinak bychom potřebovali 2x
if
a
&&
.
Samozřejmě, kdybychom měli komplexnější systém
nepřátel, tak bychom jejich typy mohli vyjádřit pomocí enum
a
následně jim nastavit jiné vlastnosti apod. Pro naše potřeby ale toto
řešení dostačuje.
Laserová střela
Laser jsem použil tento:

Jako vždy je od skvělého Kenney.nl. Má verze je otočená, aby odpovídala směru nepřátel. Otočení vašeho laseru můžete provést skrze program Preview, který je součástí macOS.
Vytvoření laseru
A jdeme na metodu createLaserFire()
. Ta bude jednodušší,
protože budeme střílet pouze jeden laser:
func createLaserFire(at point: CGPoint) -> SKNode { let laser = SKSpriteNode(imageNamed: "laser") laser.zPosition = -2 laser.position = CGPoint(x: point.x + size.width / 2, y: point.y) laser.alpha = 0 return laser }
Proč je potřeba point
jako parametr, když u hráče a raket
jsme jej nepotřebovali? Protože nepřátelé jsou součástí
enemyAnchor
a jejich pozice je vzhledem k této kotvě, takže se
nedá použít pro přidání laseru do herní scény. Také jsem udělal laser
zatím neviditelný, dále uvidíte proč
Pohyb laseru
Přidáme si ještě akci pro pohyb laseru:
static var laserMovement: SKAction { let randomWait = SKAction.wait(forDuration: Double.random(in: 0...2)) 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, fadeIn, move, remove]) }
Je trochu komplikovanější než u hráče. Začínáme náhodným
čekáním, aby nepřátelé nestříleli stejně a máme zde
fadeIn()
akci, protože jinak by byly ve scéně vidět
připravené laser střely. Respektive nebyly, protože jim nastavujeme
alpha
na 0. Zároveň se jedná o fajn ukázku síly
SKAction
a jejich řetězení do sekvencí.
Toto čekání by potenciálně mohlo být problém, protože se nepřátelé stihnou po vytvoření pohnout, ti naši se ale nehýbou tolik, takže to nevadí.
Střelba
Přesuneme se do GameScene.swift
a začneme střílet lasery. Co
vlastně potřebujeme? Vlnu nepřátel již máme, pokud ale všichni najednou
vystřelí laser, tak náš hráč bude nadávat, že hra je moc těžká a již
ji nezapne... Zatím také nemáme možnost, jak se v kódu k objektům
nepřátel dostat.
Pole nepřátel
Vytvoříme si proměnnou, do které všechny nepřátele uložíme:
var enemies = [Enemy]()
A nyní stačí pole naplnit při vytváření. V cyklu v metodě
createEnemyWave()
nejdříve upravíme první řádek v cyklu,
protože po použití copy()
musíme objekt přetypovat na
Enemy
a ne na SKSpriteNode
:
let newEnemy = enemyTemplate.copy() as! Enemy
A potom, před přidáním nepřátel do scény pomocí
addChild()
, doplníme přidání do pole:
enemies.append(newEnemy)
Časovač
Podobně jako v případě hráče si vytvoříme Timer
:
var enemyFireTimer: Timer?
Připravíme metodu, kterou timer bude spouštět:
@objc func enemyFireTimerTick() {
}
A nastavíme náš nový timer v didMove()
:
enemyFireTimer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(enemyFireTimerTick), userInfo: nil, repeats: true)
A zbývá vyřešit, jak udělat, aby vždy vystřelila jen část nepřátel.
Celkem jsem si oblíbil speciální proměnné, které slouží jako určitá
pravděpodobnost. Například šanci 1
z 10
můžeme
naprogramovat takto:
var oneInTenChance: Bool { return Int.random(in: 1...10) < 2 }
A teď implementace enemyFireTimerTick()
:
@objc func enemyFireTimerTick() { for enemy in enemies { guard oneInTenChance else { continue } let laser = enemy.createLaserFire(at: enemy.convert(enemy.anchorPoint, to: self)) addChild(laser) laser.run(Enemy.laserMovement) } }
Projdeme cyklem všechny nepřátele a zkontrolujeme
oneInTenChance
. Pokud náhoda dovolí, tak vytvoříme laserovou
střelu na pozici nepřítele. Všimněte si metody convert()
dostupné na SKNode
. Díky ní můžeme anchorPoint
v
souřadnicovém systému enemyAnchor
převést na souřadnici ve
scéně.
Samozřejmě pravidlo pro střílení si můžete udělat podle sebe. To už je potom otázka návrhu hratelnosti. Třeba můžete timer spouštět méně často, ale zvýšit pravděpodobnost výstřelu.
Můžeme zapnout a vyzkoušet:

Pokud byste chtěli "spravedlivou nahodilost" nebo nějakou rozumnou distribuci, máme k dispozici propracovanou knihovnu GameplayKit, která nabízí opravdu hromadu možností. Jenže její zakomponování by bylo docela náročné. Jde spíš o to, abyste věděli, že něco takového existuje.
Zvuky laserů
Zvuky výstřelu laseru jsem použil z audio balíčku od Kenney.nl, kde jich je hned devět.
Třeba pět si jich přidáme a budeme vybírat náhodně, ať pořád nezní
stejně. Bohužel SpriteKit neumí přehrávat soubory .ogg
, takže
jsem zvuky musel převést do .wav
.
Osobně používám utilitu ffmpeg, kterou jsem nainstaloval přes brew. Pak můžete přímo přes Terminal převádět audio a video soubory.
Přehrání zvuků
Nejdříve si tedy připravíme pole SKAction
a z něj potom
náhodně vybereme, jakou zvukovou akci přehrajeme při výstřelu.
Nejkratší zápis může vypadat takto:
let laserSoundActions = (1...5).map({ SKAction.playSoundFileNamed("laser\($0)", waitForCompletion: false)})
Nejdříve vytvoříme rozmezí (neboli Range
) od 1
do 5
a potom pomocí funkce map()
využijeme tato
čísla k vytvoření názvů našich zvukových efektů. Výsledkem je
konstantní pole pěti SKAction
.
Pokud se vám tento zápis nelíbí, chápu, že není pro každého,
můžete si třeba připravit pole názvů vašich zvukových efektů a
map()
volat nad ním.
A teď již stačí náhodnou akci spustit, abychom efekt slyšeli. Na konec
těla cyklu v metodě enemyFireTimerTick()
si přijme tento
řádek:
run(laserSoundActions.randomElement()!)
Proč vykřičník? Protože randomElement()
vrací optional,
jelikož může být metoda zavolána nad prázdným polem a potom nejde vrátit
náhodný prvek. My ale máme konstantní pole, které jsme si sami vytvořili,
takže je 100% jisté, že v něm prvky jsou
V příštím pokračování, Přidání fyziky a detekce kolizí ve SpriteKit, se dostaneme k fyzice. Ta nám totiž umožní detekovat kolize a naše pracně připravené rakety a lasery budou mít konečně nějaký užitek.
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 11x (953.92 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift