Aktuálně: Postihly zákazy tvou profesi? Poptávka po ajťácích prudce roste, využij slevové akce 30% výuky zdarma!
Discount week - Prosinec

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:

Textura laseru

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

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

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:

Nepřátelské lasery ve Swift hře v SpriteKit

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.


 

Stáhnout

Staženo 6x (953.92 kB)
Aplikace je včetně zdrojových kódů v jazyce Swift

 

Předchozí článek
Střílení raket a další částicové efekty ve SpriteKit
Všechny články v sekci
SpriteKit - Tvorba iOS her ve 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
Aktivity (3)

 

 

Komentáře

Avatar
Radek
Člen
Avatar
Radek:16.12.2019 20:43

Drobný postřeh pokud by někdo řešit pauzování (poměrně jednoduše přes isPaused true/false) - překvapilo mě, že scéna se sice zastaví, ale věci jako zvuk nebo právě timery jedou dál. . Takže během pauzy si nepřátelé krásně nabijí a po odpauzování všichni vystřelí :-)
Jediné co mě napadlo je při pauze invalide() všechny timery a při odpauzování je zase nahodit.

 
Odpovědět
16.12.2019 20:43
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 1 zpráv z 1.