Al fine di implementare correttamente il nostro gioco, dovremo scrivere 3 classi:
- Mouse, rappresentante il nostro personaggio principale;
- Coin, rappresentante le monete da collezionare;
- Block, rappresentante i blocchi da evitare durante la navigazione.
Queste classi estenderanno SKSpriteNode
perché, di fatto, rappresentano degli sprite visibili sullo schermo. Le classi conterranno anche i comportamenti dello sprite che rappresentano.
Infine dovremo sovrascrivere il contenuto attualmente presente in GameScene per definire la logica di gioco e per gestire l’input dell'utente.
La classe Mouse
La classe che implementeremo in questa lezione sarà Mouse
. Iniziamo quindi selezionando, su Xcode, il gruppo SpaceMouse dal Project Navigator.
Scegliamo quindi File > New > File…, e selezioniamo la voce Swift File, confermando con Next.
Digitiamo a questo punto il nome Mouse nel campo Save As, e clicchiamo su Create.
Sovrascriviamo quindi il contenuto del file con il codice seguente:
import SpriteKit
class Mouse: SKSpriteNode {
private let textureFalling = SKTexture(imageNamed: "rocketmouse_fall01")
private let textureFlying = SKTexture(imageNamed: "rocketmouse_run02")
private let textureDown = SKTexture(imageNamed: "rocketmouse_dead02")
private let fire = SKSpriteNode(imageNamed: "flame1")
private unowned var gameScene: GameScene
init(gameScene: GameScene) {
fire.position = CGPoint(x: -37, y: -39)
fire.zPosition = -1
self.gameScene = gameScene
super.init(texture: textureFalling, color: .clear, size: textureFalling.size())
self.addChild(fire)
let physicsBody = SKPhysicsBody(texture: textureFalling, size: textureFalling.size())
physicsBody.contactTestBitMask = 1
self.physicsBody = physicsBody
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func fly() {
self.removeAllActions()
let showFlyGraphics = SKAction.run { [unowned self] in
self.texture = self.textureFlying
self.fire.isHidden = false
}
let up = SKAction.moveBy(x: 0, y: 50, duration: 0.2)
let showFallGraphics = SKAction.run { [unowned self] in
self.texture = self.textureFalling
self.fire.isHidden = true
}
let fall = SKAction.moveTo(y: gameScene.frame.minY + self.frame.height / 2, duration: 1)
fall.timingMode = .easeIn
let showTextureDown = SKAction.run { [unowned self] in
self.texture = self.textureDown
}
let sequence = SKAction.sequence([showFlyGraphics, up, showFallGraphics, fall, showTextureDown])
self.run(sequence)
}
func die () {
let showFallGraphics = SKAction.run { [unowned self] in
self.texture = self.textureFalling
self.fire.isHidden = true
}
let fall = SKAction.moveTo(y: gameScene.frame.minY + self.frame.height / 2, duration: 0.2)
fall.timingMode = .easeIn
let showTextureDead = SKAction.run { [unowned self] in
self.texture = self.textureDown
}
let die = SKAction.run { [unowned self] in
self.gameScene.didDie()
}
run(SKAction.sequence([showFallGraphics, fall, showTextureDead, die]))
}
}
Di seguito analizziamo in modo approfondito il suddetto codice.
Le property
private let textureFalling = SKTexture(imageNamed: "rocketmouse_fall01")
private let textureFlying = SKTexture(imageNamed: "rocketmouse_run02")
private let textureDown = SKTexture(imageNamed: "rocketmouse_dead02")
Le prime 3 property contengono le texture che useremo per rappresentare gli stati del nostro personaggio. La prima verrà visualizzata quando esso sta cadendo, la seconda mentre (dopo aver attivato il razzo) starà volando verso l’alto, e la terza mentre si trova a contatto con il terreno.
Caricare un’immagine e inizializzare una texture è un’operazione che impegna le risorse del nostro sistema, quindi è bene avere le texture già pronte
durante l’esecuzione del gioco ed evitare di doverle creare esattamente nel momento in cui è necessario.
Teniamo a mente che un gioco come questo, che gira a 60 fotogrammi al secondo, ha a disposizione 16 millisecondi per preparare il prossimo frame, quindi è bene eseguire tutte le operazioni onerose prima che il giocatore inizi la partita.
La decisione di tenere sempre in memoria le 3 texture ha delle ripercussioni sull’occupazione della memoria, quindi questa strategia va valutata di volta in volta per trovare il giusto bilanciamento tra velocità e occupazione della RAM. In questo caso, ogni modello di iPhone supportato da iOS 10 ha RAM a sufficienza per tenere tranquillamente in memoria le 3 texture.
private let fire = SKSpriteNode(imageNamed: "flame1")
Questa property viene popolata con lo sprite che rappresenta la fiamma del razzo. Lo sprite verrà aggiunto a Mouse e rimarrà sempre presente. Semplicemente, lo imposteremo come visibile solo quando il razzo è acceso, e lo renderemo invisibile quando è spento.
private unowned var gameScene: GameScene
Avremo bisogno di conoscere le dimensioni della scena del gioco. Inoltre, dovremo notificare la classe GameScene
quando Mouse muore. Per questo motivo, manteniamo anche qui un riferimento alla GameScene
principale.
È interessante notare che la property gameScene
è definita unowned
. Questo è necessario per evitare che ARC (il sistema che gestisce la memoria nella app iOS) crei un ciclo di riferimenti che precluderebbe la deallocazioni della memoria di Mouse e GameScene.
Gli Initializer
init(gameScene: GameScene) {
fire.position = CGPoint(x: -37, y: -39)
fire.zPosition = -1
self.gameScene = gameScene
super.init(texture: textureFalling, color: .clear, size: textureFalling.size())
self.addChild(fire)
let physicsBody = SKPhysicsBody(texture: textureFalling, size: textureFalling.size())
physicsBody.contactTestBitMask = 1
self.physicsBody = physicsBody
}
Il blocco di codice precedente rappresenta l’initializer e contiene la logica per costruire un oggetto di tipo Mouse
. Al suo interno viene aggiunto lo sprite della fiamma del razzo. Viene impostato il riferimento alla GameScene
principale e viene creato un corpo fisico che ci sarà utile per il rilevamento delle collisioni.
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Swift ci obbliga a fare l’override di un secondo initializer. Di fatto, però, non lo useremo mai; quindi possiamo inserire un fatalError
al suo interno. Questo ci permette di compilare il codice.
Il metodo fly()
func fly() {
self.removeAllActions()
let showFlyGraphics = SKAction.run { [unowned self] in
self.texture = self.textureFlying
self.fire.isHidden = false
}
let up = SKAction.moveBy(x: 0, y: 50, duration: 0.2)
let showFallGraphics = SKAction.run { [unowned self] in
self.texture = self.textureFalling
self.fire.isHidden = true
}
let fall = SKAction.moveTo(y: gameScene.frame.minY + self.frame.height / 2, duration: 1)
fall.timingMode = .easeIn
let showTextureDown = SKAction.run { [unowned self] in
self.texture = self.textureDown
}
let sequence = SKAction.sequence([showFlyGraphics, up, showFallGraphics, fall, showTextureDown])
self.run(sequence)
}
Questo metodo verrà invocato da GameScene
quando il giocatore tocca lo schermo. Tutte le volte che si accende il razzo, il nostro Mouse riceve una spinta verso l’alto per poi ricadere. Le action in questo metodo provocano esattamente quell’animazione.
Il metodo die()
func die () {
let showFallGraphics = SKAction.run { [unowned self] in
self.texture = self.textureFalling
self.fire.isHidden = true
}
let fall = SKAction.moveTo(y: gameScene.frame.minY + self.frame.height / 2, duration: 0.2)
fall.timingMode = .easeIn
let showTextureDead = SKAction.run { [unowned self] in
self.texture = self.textureDown
}
let die = SKAction.run { [unowned self] in
self.gameScene.didDie()
}
run(SKAction.sequence([showFallGraphics, fall, showTextureDead, die]))
}
Il metodo die()
crea ed esegue l’animazione che fa precipitare Mouse verso il terreno. Il metodo verrà invocato da GameScene
non appena viene rilevata una collisione con un blocco. Al termine dell’animazione, la classe GameScene
esegue l’istruzione seguente:
self.gameScene.didDie()
Ciò comunica il completamento della sequenza di action. A questo punto, GameScene
potrà visualizzare il testo Game Over.
Ovviamente, se proviamo a compilare il progetto adesso, otterremo un errore. Ciò è dovuto al fatto che non abbiamo ancora definito il metodo didDie
. Vedremo questa parte nella prossime lezione.