introduction
Dans ce didacticiel, le cinquième et dernier volet de la série SpriteKit From Scratch, nous examinons quelques techniques avancées que vous pouvez utiliser pour optimiser vos jeux basés sur SpriteKit afin d’améliorer les performances et l’expérience utilisateur.
Ce didacticiel nécessite que vous exécutiez Xcode 7.3 ou version ultérieure, qui inclut Swift 2.2 et les SDK iOS 9.3, tvOS 9.2 et OS X 10.11.4. Pour suivre, vous pouvez soit utiliser le projet que vous avez créé dans le didacticiel précédent, soit télécharger une nouvelle copie depuis GitHub.
Les graphiques utilisés pour le jeu de cette série se trouvent sur GraphiqueRiver. GraphicRiver est une excellente source pour trouver des illustrations et des graphiques pour vos jeux.
1. Atlas de textures
Afin d’optimiser l’utilisation de la mémoire de votre jeu, SpriteKit fournit la fonctionnalité d’atlas de textures sous la forme de SKTextureAtlas
classe. Ces atlas combinent efficacement les textures que vous spécifiez en une seule et grande texture qui prend moins de mémoire que les textures individuelles seules.
Heureusement, Xcode peut créer des atlas de textures très facilement pour vous. Cela se fait dans les mêmes catalogues d’actifs que ceux utilisés pour d’autres images et ressources de vos jeux. Ouvrez votre projet et accédez au Actifs.xcassets catalogue d’actifs. En bas de la barre latérale gauche, cliquez sur l’icône + et sélectionnez le Nouvel Atlas des Sprites option.


En conséquence, un nouveau dossier est ajouté au catalogue d’actifs. Cliquez une fois sur le dossier pour le sélectionner et cliquez à nouveau pour le renommer. Nomme le Obstacles. Ensuite, faites glisser le Obstacle 1 et Obstacle 2 ressources dans ce dossier. Vous pouvez également supprimer le blanc Lutin actif que Xcode génère si vous le souhaitez, mais ce n’est pas obligatoire. Une fois terminé, votre Obstacles l’atlas de texture devrait ressembler à ceci:


Il est maintenant temps d’utiliser l’atlas de textures dans le code. Ouvert MainScene.swift et ajoutez la propriété suivante au MainScene
classe. Nous initialisons un atlas de textures en utilisant le nom que nous avons entré dans notre catalogue d’actifs.
let obstaclesAtlas = SKTextureAtlas(named: "Obstacles")
Bien que cela ne soit pas nécessaire, vous pouvez précharger les données d’un atlas de textures en mémoire avant de l’utiliser. Cela permet à votre jeu d’éliminer tout décalage qui pourrait se produire lors du chargement de l’atlas de textures et de la récupération de la première texture. Le préchargement d’un atlas de textures se fait avec une seule méthode et vous pouvez également exécuter un bloc de code personnalisé une fois le chargement terminé.
dans le MainScene
classe, ajoutez le code suivant à la fin de la didMoveToView(_:)
méthode:
override func didMoveToView(view: SKView) { ... obstaclesAtlas.preloadWithCompletionHandler { // Do something once texture atlas has loaded } }
Pour récupérer une texture à partir d’un atlas de textures, vous utilisez le textureNamed(_:)
méthode avec le nom que vous avez spécifié dans le catalogue d’actifs en tant que paramètre. Mettons à jour le spawnObstacle(_:)
méthode dans le MainScene
class pour utiliser l’atlas de textures que nous avons créé il y a un instant. Nous récupérons la texture de l’atlas de textures et l’utilisons pour créer un nœud de sprite.
func spawnObstacle(timer: NSTimer) { if player.hidden { timer.invalidate() return } let spriteGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 2) let texture = obstaclesAtlas.textureNamed("Obstacle (spriteGenerator)") let obstacle = SKSpriteNode(texture: texture) obstacle.xScale = 0.3 obstacle.yScale = 0.3 let physicsBody = SKPhysicsBody(circleOfRadius: 15) physicsBody.contactTestBitMask = 0x00000001 physicsBody.pinned = true physicsBody.allowsRotation = false obstacle.physicsBody = physicsBody let center = size.width/2.0, difference = CGFloat(85.0) var x: CGFloat = 0 let laneGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 3) switch laneGenerator.nextInt() { case 1: x = center - difference case 2: x = center case 3: x = center + difference default: fatalError("Number outside of [1, 3] generated") } obstacle.position = CGPoint(x: x, y: (player.position.y + 800)) addChild(obstacle) obstacle.lightingBitMask = 0xFFFFFFFF obstacle.shadowCastBitMask = 0xFFFFFFFF }
Notez que si votre jeu tire parti des ressources à la demande (ODR), vous pouvez facilement spécifier une ou plusieurs balises pour chaque atlas de textures. Une fois que vous avez accédé aux bonnes balises de ressources avec les API ODR, vous pouvez ensuite utiliser votre atlas de textures comme nous l’avons fait dans le spawnObstacle(_:)
méthode. Vous pouvez en savoir plus sur les ressources à la demande dans un autre de mes tutoriels.
2. Sauvegarde et chargement de scènes
SpriteKit vous offre également la possibilité de sauvegarder et de charger facilement des scènes depuis et vers un stockage persistant. Cela permet aux joueurs de quitter votre jeu, de le relancer plus tard et d’être toujours au même stade de votre jeu qu’avant.
La sauvegarde et le chargement de votre jeu sont gérés par le NSCoding
protocole, que le SKScene
la classe est déjà conforme à. L’implémentation par SpriteKit des méthodes requises par ce protocole permet automatiquement à tous les détails de votre scène d’être enregistrés et chargés très facilement. Si vous le souhaitez, vous pouvez également remplacer ces méthodes pour enregistrer certaines données personnalisées avec votre scène.
Parce que notre jeu est très basique, nous allons utiliser un simple Bool
valeur pour indiquer si la voiture s’est écrasée. Cela vous montre comment enregistrer et charger des données personnalisées liées à une scène. Ajoutez les deux méthodes suivantes du NSCoding
protocole au MainScene
classe.
// MARK: - NSCoding Protocol required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) let carHasCrashed = aDecoder.decodeBoolForKey("carCrashed") print("car crashed: (carHasCrashed)") } override func encodeWithCoder(aCoder: NSCoder) { super.encodeWithCoder(aCoder) let carHasCrashed = player.hidden aCoder.encodeBool(carHasCrashed, forKey: "carCrashed") }
Si vous ne connaissez pas le NSCoding
protocole, le encodeWithCoder(_:)
gère la sauvegarde de votre scène et de l’initialiseur avec un seul NSCoder
Le paramètre gère le chargement.
Ensuite, ajoutez la méthode suivante au MainScene
classe. le saveScene()
méthode crée un NSData
représentation de la scène, en utilisant le NSKeyedArchiver
classe. Pour simplifier les choses, nous stockons les données dans NSUserDefaults
.
func saveScene() { let sceneData = NSKeyedArchiver.archivedDataWithRootObject(self) NSUserDefaults.standardUserDefaults().setObject(sceneData, forKey: "currentScene") }
Ensuite, remplacez la mise en œuvre de didBeginContactMethod(_:)
dans le MainScene
classe avec ce qui suit:
func didBeginContact(contact: SKPhysicsContact) { if contact.bodyA.node == player || contact.bodyB.node == player { if let explosionPath = NSBundle.mainBundle().pathForResource("Explosion", ofType: "sks"), let smokePath = NSBundle.mainBundle().pathForResource("Smoke", ofType: "sks"), let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile(explosionPath) as? SKEmitterNode, let smoke = NSKeyedUnarchiver.unarchiveObjectWithFile(smokePath) as? SKEmitterNode { player.removeAllActions() player.hidden = true player.physicsBody?.categoryBitMask = 0 camera?.removeAllActions() explosion.position = player.position smoke.position = player.position addChild(smoke) addChild(explosion) saveScene() } } }
La première modification apportée à cette méthode est la modification du nœud du joueur categoryBitMask
plutôt que de le supprimer entièrement de la scène. Cela garantit que lors du rechargement de la scène, le nœud du joueur est toujours là, même s’il n’est pas visible, mais que les collisions en double ne sont pas détectées. L’autre changement effectué est d’appeler le saveScene()
méthode que nous avons définie précédemment une fois que la logique d’explosion personnalisée a été exécutée.
Enfin, ouvrez ViewController.swift et remplacez le viewDidLoad()
méthode avec l’implémentation suivante:
override func viewDidLoad() { super.viewDidLoad() let skView = SKView(frame: view.frame) var scene: MainScene? if let savedSceneData = NSUserDefaults.standardUserDefaults().objectForKey("currentScene") as? NSData, let savedScene = NSKeyedUnarchiver.unarchiveObjectWithData(savedSceneData) as? MainScene { scene = savedScene } else if let url = NSBundle.mainBundle().URLForResource("MainScene", withExtension: "sks"), let newSceneData = NSData(contentsOfURL: url), let newScene = NSKeyedUnarchiver.unarchiveObjectWithData(newSceneData) as? MainScene { scene = newScene } skView.presentScene(scene) view.insertSubview(skView, atIndex: 0) let left = LeftLane(player: scene!.player) let middle = MiddleLane(player: scene!.player) let right = RightLane(player: scene!.player) stateMachine = LaneStateMachine(states: [left, middle, right]) stateMachine?.enterState(MiddleLane) }
Lors du chargement de la scène, nous vérifions d’abord s’il y a des données enregistrées dans le standard NSUserDefaults
. Si tel est le cas, nous récupérons ces données et recréons le MainScene
objet utilisant le NSKeyedUnarchiver
classe. Sinon, nous obtenons l’URL du fichier de scène que nous avons créé dans Xcode et nous en chargeons les données de la même manière.
Exécutez votre application et rencontrez un obstacle avec votre voiture. À ce stade, vous ne voyez pas de différence. Exécutez à nouveau votre application, cependant, et vous devriez voir que votre scène a été restaurée exactement comme elle était lorsque vous venez de planter la voiture.
3. La boucle d’animation
Avant le rendu de chaque image de votre jeu, SpriteKit exécute une série de processus dans un ordre particulier. Ce groupe de processus est appelé boucle d’animation. Ces processus tiennent compte des actions, des propriétés physiques et des contraintes que vous avez ajoutées à votre scène.
Si, pour une raison quelconque, vous devez exécuter du code personnalisé entre l’un de ces processus, vous pouvez soit remplacer certaines méthodes spécifiques de votre SKScene
sous-classe ou spécifiez un délégué conforme à la SKSceneDelegate
protocole. Notez que, si vous affectez un délégué à votre scène, les implémentations de classe des méthodes suivantes ne sont pas appelées.
Les processus de boucle d’animation sont les suivants:
Étape 1
La scène appelle son update(_:)
méthode. Cette méthode a un seul NSTimeInterval
paramètre, qui vous donne l’heure actuelle du système. Cet intervalle de temps peut être utile car il vous permet de calculer le temps nécessaire au rendu de votre image précédente.
Si la valeur est supérieure à 1 / 60e de seconde, votre jeu ne fonctionne pas aux 60 images par seconde (FPS) que SpriteKit vise. Cela signifie que vous devrez peut-être modifier certains aspects de votre scène (par exemple, les particules, le nombre de nœuds) pour réduire sa complexité.
Étape 2
La scène exécute et calcule les actions que vous avez ajoutées à vos nœuds et les positionne en conséquence.
Étape 3
La scène appelle son didEvaluateActions()
méthode. C’est ici que vous pouvez exécuter n’importe quelle logique personnalisée avant que SpriteKit ne continue avec la boucle d’animation.
Étape 4
La scène effectue ses simulations physiques et modifie votre scène en conséquence.
Étape 5
La scène appelle son didSimulatePhysics()
, que vous pouvez remplacer avec la didEvaluateActions()
méthode.
Étape 6
La scène applique les contraintes que vous avez ajoutées à vos nœuds.
Étape 7
La scène appelle son didApplyConstraints()
méthode, que vous pouvez remplacer.
Étape 8
La scène appelle son didFinishUpdate()
méthode, que vous pouvez également remplacer. Il s’agit de la dernière méthode dans laquelle vous pouvez modifier votre scène avant que son apparence pour cette image ne soit finalisée.
Étape 9
Enfin, la scène rend son contenu et met à jour son contenant SKView
en conséquence.
Il est important de noter que si vous utilisez un SKSceneDelegate
objet plutôt qu’une sous-classe personnalisée, chaque méthode gagne un paramètre supplémentaire et change légèrement son nom. Le paramètre supplémentaire est un SKScene
object, qui vous permet de déterminer la scène avec laquelle la méthode est exécutée. Les méthodes définies par le SKSceneDelegate
protocole sont nommés comme suit:
update(_:forScene:)
-
didEvaluateActionsForScene(_:)
didSimulatePhysicsForScene(_:)
-
didApplyConstraintsForScene(_:)
-
didFinishUpdateForScene(_:)
Même si vous n’utilisez pas ces méthodes pour apporter des modifications à votre scène, elles peuvent toujours être très utiles pour le débogage. Si votre jeu est constamment en retard et que la fréquence d’images diminue à un moment particulier de votre jeu, vous pouvez remplacer toute combinaison des méthodes ci-dessus et trouver l’intervalle de temps entre chacune d’elles. Cela vous permet de déterminer avec précision si ce sont précisément vos actions, votre physique, vos contraintes ou vos graphiques qui sont trop complexes pour que votre jeu fonctionne à 60 FPS.
4. Meilleures pratiques de performance
Dessin par lots
Lors du rendu de votre scène, SpriteKit, par défaut, parcourt les nœuds de votre scène children
tableau et les dessine sur l’écran dans le même ordre qu’ils sont dans le tableau. Ce processus est également répété et mis en boucle pour tous les nœuds enfants qu’un nœud particulier peut avoir.
L’énumération individuelle des nœuds enfants signifie que SpriteKit exécute un appel de dessin pour chaque nœud. Alors que pour les scènes simples, cette méthode de rendu n’a pas d’impact significatif sur les performances, à mesure que votre scène gagne plus de nœuds, ce processus devient très inefficace.
Pour rendre le rendu plus efficace, vous pouvez organiser les nœuds de votre scène en couches distinctes. Cela se fait via le zPosition
propriété du SKNode
classe. Plus un nœud est haut zPosition
c’est-à-dire que plus il est “proche” de l’écran, ce qui signifie qu’il est rendu au-dessus des autres nœuds de votre scène. De même, le nœud avec le plus bas zPosition
dans une scène apparaît tout à fait “en arrière” et peut être chevauché par n’importe quel autre nœud.
Après avoir organisé les nœuds en couches, vous pouvez définir un SKView
objets ignoreSiblingOrder
propriété à true
. Cela se traduit par SpriteKit en utilisant le zPosition
valeurs pour rendre une scène plutôt que l’ordre de la children
tableau. Ce processus est beaucoup plus efficace que tous les nœuds avec le même zPosition
sont regroupés en un seul appel de tirage au lieu d’en avoir un pour chaque nœud.
Il est important de noter que le zPosition
la valeur d’un nœud peut être négative si nécessaire. Les nœuds de votre scène sont toujours rendus par ordre croissant zPosition
.
Évitez les animations personnalisées
Les deux SKAction
et SKConstraint
Les classes contiennent un grand nombre de règles que vous pouvez ajouter à une scène pour créer des animations. Faisant partie du framework SpriteKit, ils sont optimisés autant que possible et s’intègrent parfaitement dans la boucle d’animation de SpriteKit.
Le large éventail d’actions et de contraintes qui vous est fourni permet presque toutes les animations possibles que vous pourriez souhaiter. Pour ces raisons, il est recommandé de toujours utiliser des actions et des contraintes dans vos scènes pour créer des animations plutôt que d’exécuter une logique personnalisée ailleurs dans votre code.
Dans certains cas, en particulier si vous avez besoin d’animer un groupe de nœuds raisonnablement grand, les champs de force physique peuvent même produire le résultat souhaité. Les champs de force sont encore plus efficaces car ils sont calculés avec le reste des simulations physiques de SpriteKit.
Masques de bits
Vos scènes peuvent être encore plus optimisées en utilisant uniquement les masques de bits appropriés pour les nœuds de votre scène. En plus d’être cruciaux pour la détection des collisions physiques, les masques binaires déterminent également comment les simulations physiques et l’éclairage réguliers affectent les nœuds d’une scène.
Pour n’importe quelle paire de nœuds dans une scène, qu’ils se heurtent ou non, SpriteKit surveille où ils sont les uns par rapport aux autres. Cela signifie que, s’il reste avec les masques par défaut avec tous les bits activés, SpriteKit garde une trace de l’emplacement de chaque nœud dans votre scène par rapport à tous les autres nœuds. Vous pouvez grandement simplifier les simulations physiques de SpriteKit en définissant des masques de bits appropriés afin que seules les relations entre les nœuds susceptibles de se heurter soient suivies.
De même, une lumière dans SpriteKit n’affecte un nœud que si la logique AND
de leurs masques de bits de catégorie est une valeur non nulle. En modifiant ces catégories, de sorte que seuls les nœuds les plus importants de votre scène soient affectés par une lumière particulière, vous pouvez réduire considérablement la complexité d’une scène.
Conclusion
Vous devriez maintenant savoir comment optimiser davantage vos jeux SpriteKit en utilisant des techniques plus avancées, telles que les atlas de texture, le dessin par lots et les masques de bits optimisés. Vous devez également être à l’aise avec l’enregistrement et le chargement de scènes pour offrir à vos joueurs une meilleure expérience globale.
Tout au long de cette série, nous avons examiné de nombreuses fonctionnalités et fonctionnalités du framework SpriteKit sous iOS, tvOS et OS X. Il existe des sujets encore plus avancés au-delà de la portée de cette série, tels que les shaders OpenGL ES et Metal personnalisés. comme champs physiques et articulations.
Si vous souhaitez en savoir plus sur ces sujets, je vous recommande de commencer par le Référence du framework SpriteKit et renseignez-vous sur les classes pertinentes.
Comme toujours, assurez-vous de laisser vos commentaires et commentaires dans les commentaires ci-dessous.
Laisser un commentaire