TIM.Briques – SpriteKit

Construction d’un jeu vidéo avec SpriteKit

Éléments de contenu

d8ll3
 

 
 
 


Description

Dans ce laboratoire nous verrons,  grace au framework spriteKit, à l’engin de physique, aux particules et au frameWork AVPlayer, comment construire une application de type casse briques.
Au lieu d’utiliser la méthode classique de vérification- à chaque cadre (enterFrame) – d’une intersection entre deux éléments, nous utiliserons plutôt une méthode de délégation de l’engin de physique pour détecter une collision.
 


Sprite Kit

Sprite Kit propose une infrastructure de développement de jeux vidéos 2D en offrant des fonctions d’animations et de rendu.  Cette librairie a été introduite à la version 5 de Xcode.
Avant l’arrivée de Sprite Kit, les développeurs de jeux IOS devaient utiliser des libraires tierces parties comme par exemple, le populaire framework Cocos2d.
Sprite Kit utilise une boucle standard d’animation (enterFrame) pour produire et afficher les rendus.
Voici le cycle de vie d’un cadre – frame –  d’animation:

update_loop_2x

Source: Apple


 
Le framework propose aussi d’autres méthodes permettant d’enrichir l’expérience du jeu: possibilité de jouer des trames sonores, un engin de simulation de physique,  effets spéciaux complexes, utilisation d’atlas de textures et support de particules.
Apple propose deux autres frameworks pour le développement de jeux videos 3D: Metal et Scène Kit.  Il est aussi possible de créer des projets de type OpenGL.
Ce laboratoire couvrira les fonctions de base de Sprite Kit.
 


Étape 1 – Une nouvelle application de type ‘Game’

Dans l’univers de Xcode, bâtir un jeu implique la création d’un nouveau projet Xcode.

Action 1.1
–  Créons un nouveau projet et sélectionnons ‘Game’ comme gabarit de départ:
TIM.Briques.01
 
Action 1.2 –   Nommons le projet et  sélectionnons ‘SpriteKit’ sous ‘Game Technologie’:
TIM.Briques.02
Note: sceneKit et Metal permettent le développement de jeux 3D.
 
Action 1.3 –  Testons l’application (Touchez à l’écran pendant l’exécution)
TIM.Briques.03
 
Action 1.4 –  Analysons le code de départ:

//  GameScene.swift
//  TIM.Briques
//
//  Created by Alain on 2014-10-07.
//  Copyright (c) 2014 Alain. All rights reserved.
import SpriteKit
class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        /* Setup your scene here */
        let myLabel = SKLabelNode(fontNamed:"Chalkduster")
        myLabel.text = "Hello, World!";
        myLabel.fontSize = 65;
        myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
        self.addChild(myLabel)
    }
    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        /* Called when a touch begins */
        for touch: AnyObject in touches {
            let location = touch.locationInNode(self)
            let sprite = SKSpriteNode(imageNamed:"Spaceship")
            sprite.xScale = 0.5
            sprite.yScale = 0.5
            sprite.position = location
            let action = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)
            sprite.runAction(SKAction.repeatActionForever(action))
            self.addChild(sprite)
        }
    }
    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
        println("update")
    }
}

 


Débuter avec une scène vide

Action 1.6 – Remplaçons le code du fichier GameScene.swift par:

import SpriteKit
class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        /* Setup your scene here */
    } // didMoveToView()
    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
    } // update()
} // class GameScene

 
Action 1.7 – Ajoutons la ligne suivante à la classe GameViewController.

//class GameViewController: UIViewController {
//    override func viewDidLoad() {
// ...
            /* Set the scale mode to scale to fit the window */
            scene.scaleMode = .AspectFill
            scene.size = skView.bounds.size
            skView.presentScene(scene)

Note:  la ligne:  ‘scene.size = skView.bounds.size’ indique que nous désirons une scène de taille égale à l’appareil sur lequel nous exécuterons l’application.
Action 1.8 – Ajoutons les ressources suivantes au projet: TIM.Briques.Ressources
 


Tout est question de ‘Noeuds’

Pour ajouter des éléments sur la scène, il suffit de créer des instances de classes étendant la classe SKNode.
Voici une liste des classes proposées:


Ajouter du texte – SKLabelNode

Action 1.9 – Ajoutons un titre (SKLabelNode) sur la scène :

    // Point de départ du code de la scène
    override func didMoveToView(view: SKView) {
        /* Construire la scène du jeu ici */
        // Note: self est sous entendu ici: self.backgroundColor =
        backgroundColor = UIColor(red: 0, green: 0, blue: 140/255, alpha: 1)
        let unTitre = SKLabelNode(fontNamed:"Arial")
        //let unTitre = SKLabelNode(fontNamed:"Hoefler Text")
        unTitre.text = "TIM.Briques";
        unTitre.fontSize = 35;
        //unTitre.fontColor = UIColor.redColor()
        self.addChild(unTitre)
    }

 
Résultat obtenu: 
tim.briques.04
 


Le système de coordonnées de SKScene

Extrait de la documentation d’Apple:
Building Your Scene

You have already learned many things about working with scenes. Here’s a quick recap of the important facts:

  • Scenes (SKScene objects) are used to provide content to be rendered by an SKView object.
  • A scene’s content is created as a tree of node objects. The scene is the root node.
  • When presented by a view, a scene runs actions and simulates physics, then renders the node tree.
  • You create custom scenes by subclassing the SKScene class.

With those basic concepts in mind, it is time to learn more about the node tree and building your scenes.

A Node Provides a Coordinate System to Its Children

When a node is placed in the node tree, its position property places it within a coordinate system provided by its parent. Sprite Kit uses the same coordinate system on both iOS and OS X. Figure 4-1 shows the Sprite Kit coordinate system. Coordinate values are measured in points, as in UIKit or AppKit; where necessary, points are converted to pixels when the scene is rendered. A positive x coordinate goes to the right and a positive y coordinate goes up the screen.

Figure 4-1  Sprite Kit coordinate system

Sprite Kit also has a standard rotation convention. Figure 4-2 shows the polar coordinate convention. An angle of 0 radians specifies the positive x axis. A positive angle is in the counterclockwise direction.
 

Figure 4-2  Polar coordinate conventions (rotation)

When you are working only with Sprite Kit code, a consistent coordinate system means that you can easily share code between an iOS and OS X version of your game. However, it does mean that when you write OS-specific user interface code, you may need to convert between the operating system’s view coordinate conventions and Sprite Kit’s coordinate system. This is most often the case when working with iOS views, which use a different coordinate convention.
 

Only Some Nodes Contain Content

Not all nodes draw content. For example, the SKSpriteNode class draws a sprite, but the SKNode class doesn’t draw anything. You can tell whether a particular node object draws content by reading its frame property. The frame is the visible area of the parent’s coordinate system that the node draws into. If the node draws content, this frame has a nonzero size. For a scene, the frame always reflects the visible portion of the scene’s coordinate space.
If a node has descendants that draw content, it is possible for a node’s subtree to provide content even though it doesn’t provide any content itself. You can call a node’s calculateAccumulatedFrame method to retrieve a rectangle that includes the entire area that a node and all of its descendants draw into.

Creating a Scene

A scene is presented by a view. The scene includes properties that define where the scene’s origin is positioned and the size of the scene. If the scene does not match the view’s size, you can also define how the scene is scaled to fit in the view.

A Scene’s Size Defines Its Visible Area

When a scene is first initialized, its size property is configured by the designated initializer. The size of the scene specifies the size of the visible portion of the scene in points. This is only used to specify the visible portion of the scene. Nodes in the tree can be positioned outside of this area; those nodes are still processed by the scene, but are ignored by the renderer.

Using the Anchor Point to Position the Scene’s Coordinate System in the View

By default, a scene’s origin is placed in the lower-left corner of the view, as shown in Figure 4-3. So, a scene is initialized with a height of 1024 and a width of 768, has the origin (0,0) in the lower-left corner, and the (1024,768) coordinate in the upper-right corner. The frame property holds (0,0)-(1024,768).
A scene’s position property is ignored by Scene Kit because the scene is always the root node for a node tree. Its default value is CGPointZero and you can’t change it. However, you can move the scene’s origin by setting its anchorPoint property. The anchor point is specified in the unit coordinate space and chooses a point in the enclosing view.

Figure 4-3  Default anchor for a scene is in the lower-left corner of the view

The default value for the anchor point is CGPointZero, which places it at the lower-left corner. The scene’s visible coordinate space is (0,0) to (width,height). The default anchor point is most useful for games that do not scroll a scene’s content.
The second-most common anchor point value is (0.5,0.5), which centers the scene’s origin in the middle of the view as shown in Figure 4-4. The scene’s visible coordinate space is (-width/2,-height/2) to (width/2, height/2). Centering the scene on its anchor point is most useful when you want to easily position nodes relative to the center of the screen, such as in a scrolling game.

Figure 4-4  Moving the anchor point to the center of the view

So, to summarize, the anchorPoint and size properties are used to compute the scene’s frame, which holds the visible portion of the scene.
 


Expressions utiles sous SpriteKit

[table delimiter= »| »]
Expression|Description|Construire/Utilisation
CGFloat|Valeur réelle|let x:CGFloat = 3.141592
CGPoint|Représente une coordonnée (x,y)|CGPointMake(2, 3.14)
CGRect|Représente un rectangle sous la forme x,y, largeur, hauteur|CGRectMake(0,0,640,1024)
CGPointZero|Un CGPoint égal à (0,0)|CGPointZero
CGRectGetMidX|Obtenir le centre horizontal d’un cadre|CGRectGetMidX(self.frame)
CGRectGetMidY|Obtenir le centre vertical d’un cadre|CGRectGetMidY(self.frame)
objet.position|De type CGPoint – Obtenir ou modifier la position d’un objet|unObjet.position = CGPointMake(0, 0)
objet.frame|De type CGRect – Obtenir ou modifier la cadre d’un objet|unObjet.frame = CGRectMake(0, 0, 50, 50)
[/table]
 


 
Action 1.10 – Plaçons le titre au centre de la scène.

        unTitre.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));

Résultat obtenu: 
tim.briques.05
Note: Il est possible de modifier le point d’encrage de la scène:

self.anchorPoint = CGPointMake(0.5, 0.5)  // placer au centre

 
Action 1.11 – Plaçons le titre au haut de la scène.

        unTitre.position = CGPoint(x:CGRectGetMidX(frame), y:frame.height - unTitre.frame.height / 2)

Résultat obtenu: 
tim.briques.06
Note: Remarquez que le texte sort un peu de l’écran.  Le rectangle du ‘label’ est plus haut que la zone visible – espace pour souligner (simple et double).

Action 1.12
 
– Ajustons le titre.

        unTitre.position = CGPoint(x:CGRectGetMidX(frame), y:frame.height - unTitre.frame.height);

Résultat obtenu: 
tim.briques.07
 
Note à l’auteur: gestion de la rotation.
 


Ajouter une image – SKSPriteNode

Action 1.13 – Ajoutons un ballon (fourni dans le fichier des ressources du projet) au centre la scène.
balle@2x
 

// Dans la zone des propriétés
    var ballon:SKSpriteNode!
// Dans didMoveToView()
        ballon = SKSpriteNode(imageNamed: "balle")
        ballon.position = CGPoint(x:CGRectGetMidX(frame), y:CGRectGetMidY(frame))
        self.addChild(ballon)

Résultat obtenu: 
tim.briques.08
 


La méthode update() – onEnterFrame

La méthode update est exécutée – si possible – 60 fois par seconde.  C’est une des méthodes de la classe SKScene et elle peut-être surchargée.
Pour changer le nombre d’images par seconde il faut renseigner la propriété frameInterval.
Par exemple, pour diviser  la cadence par 2 (30fps):

override func didMoveToView(view: SKView) {
view.frameInterval = 2

 
Action 1.14 – Ajoutons le code suivant à la méthode ‘update’ de la classe de la scène .

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
        let nouvellePosition = CGPointMake(ballon.position.x + 1, ballon.position.y)
        if nouvellePosition.x > frame.width /* + ballon.frame.width / 2 */
        {
            ballon.position = CGPointMake( 0 /* -ballon.frame.width */, ballon.position.y)
        } else {
        ballon.position = nouvellePosition
        } // if nouvellePosition.x > frame.width
    } // update()

 
Action 1.14b – Testons l’application.
Note: Remarquez que le ballon entre sur la scène sur la moitié de sa largeur.

Action 1.15
 
– Supprimons les commentaires pour tenir compte de la taille du ballon dans le calcul de la position.
d8r6j
 
Action 1.16 – Plaçons en commentaire le code dans la méthode update().
 


Étape 2 – Physique

Le framework SpriteKit propose des propriétés dans les sprites: physicsBody, categoryBitMask,…,  et des classes: SKPhysicsBody, …, permettant de définir un univers physique.
Nous allons utiliser ces propriétés et ces méthodes pour soumettre les sprites aux forces gravitationnelles et pour détecter une collision entre eux.
 
Action 2.1 – Organisons un peu notre du code – Ajouter le ballon avec une méthode:

   func ajouterBallon(){
        ballon = SKSpriteNode(imageNamed: "balle")
        ballon.position = CGPoint(x:CGRectGetMidX(frame), y:frame.height - ballon.frame.height)
        // ballon.physicsBody = SKPhysicsBody(circleOfRadius: ballon.frame.size.width/2)
        self.addChild(ballon)
    }  // ajouterBallon

 
Action 2.2 – Ajoutons le code suivant dans la méthode ajouterBallon pour ajouter un corps physique au ballon:

        ballon.physicsBody = SKPhysicsBody(circleOfRadius: ballon.frame.size.width/2)

Documentation Apple pour SKPhysicsBody
 
Action 2.3 – Testons l’application.
d8s0e
Note: Maintenant que le ballon a un corps physique, il sera soumis à une force gravitationnelle de 9,78 m/s².  Pour ce que cela représente dans un univers de 6 pouces.  
Il est possible de modifier la gravité par programmation.
 


Voici une liste des constructeurs disponibles pour créer un corps physique:

À partir d’un sprite

 

À partir d’une forme vide

 
Action 2.4 –  Ajoutons, au bas de la scène, une palette de jeu:

//  Zone des propriétés de la classe de la scène
    var palette:SKSpriteNode!
    func ajouterPalette(/*size:CGSize*/)
    {
    // Créer une palette
    palette = SKSpriteNode(imageNamed:"rectangle")
    // La positionner
    palette.position = CGPointMake(size.width/2, palette.size.height * 2);
    // Lui ajouter un corps physique
    palette.physicsBody = SKPhysicsBody(rectangleOfSize: palette.frame.size)
    // À Faire: Soustraire l'objet à la gravité de la scène
    // palette.physicsBody!.dynamic = false
    // Ajouter à ;a scène
    self.addChild(palette)
    } // ajouterPalette

Note:  En exécutant le projet, nous allons remarquer que les deux objets vont tomber hors de la scène.
Solution: Il faut soustraire la palette à la gravité.
 
Action 2.5 –  Enlevons le commentaire devant la ligne suivante:

    palette.physicsBody!.dynamic = false

Note: ‘physicsBody’ est une optionnelle.
Action 2.6 – Testons l’application.
dyvap
 
Remarquez que très peu énergie est retourné à la balle suite à l’impact avec la palette.
La classe SKPhysicsBody propose les propriétés suivantes pour le contrôle des différentes règles du corps physique.

Note: Voir la documentation d’Apple.
Action 2.7 – Modifions les propriétés physiques de la balle

// Dans ajouterBalle(), avant l'ajout à la scène.
        // physicsBody est une optionnelle
        if let physique = ballon.physicsBody {
            physique.restitution = 1.0      // L'objet va rebondir avec la même force qu'à l'impact.
            physique.friction =  0.5        // Simule de la friction au contact.
            physique.linearDamping = 0.1    // Simule de la friction (dans l'air, l'eau, ...) en diminuant la vélocité de l'objet.
            physique.allowsRotation = false
        }

 
Action 2.8 – Testons l’application.
dyvwr
 


Déplacement de la palette de jeu

Action 2.9 – Ajoutons la méthode suivante à la classe de la scène:

    override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
        for touch: AnyObject in touches {
            var localisation:CGPoint = touch.locationInNode(self)
            palette.position = localisation
        } // for touch
    } // touchesMoved()

Note: Le point centre de la palette sera renseigné par le point de contact de l’écran.

Action 2.10
 
– Testons l’application.
dyweo
Note: un ‘déplacement’, sur n’importe lequel des objets de la scène, provoquera le déplacement de la palette.
Limitons le déplacement, sur l’axe des ‘y’, à la valeur de départ de la palette.
Action 2.11 – Ajoutons le code suivant à la classe touchesMoved:

            var localisation:CGPoint = touch.locationInNode(self)
            localisation.y = palette.position.y
            palette.position = localisation

 
Action 2.12 – À réaliser en laboratoire:  Modifier le code pour que la palette ne sorte pas, en partie, de la scène (axe de ‘x’)
dywvx
 
[expand title= »Afficher la solution »]

            // Gérer le débordement de l'axe des x.
            if (localisation.x < palette.size.width / 2) {
                localisation.x = palette.size.width / 2;
            }  // Trop à gauche.
            if (localisation.x > size.width - (palette.size.width/2)) {
                localisation.x = size.width - (palette.size.width/2);
            }  // Trop à droite.
            palette.position = localisation

[/expand]


Emprisonner la balle dans un univers fermé

Présentement, il n’y a pas de frontière, à laquelle la balle peut se butter.  Nous adressons cette situation, en ajoutant un corps physique  à la scène.

Action 2.13
 
– Ajoutons la méthode suivante à la classe de la scène:

// Lancer cette méthode dans didMoveToView()
    func preparerLaScene(){
        // Changer la couleur de la scène
        backgroundColor = UIColor(red: 0, green: 0, blue: 140/255, alpha: 1)
        // Donner un corps physique à la scène
        physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)
        // println(frame)
    } // preparerLaScene

 
Action 2.14 – Testons l’application.
dyxen
Note: La balle perd beaucoup d’énergie à chaque collision.  Nous pourrions modifier la ‘restitution’ mais nous allons plutôt utiliser une autre approche, faire évoluer nos objets dans un univers à gravité zero.
 
Action 2.15 – Ajoutons le code suivant à la méthode preparerLaScene():

        physicsBody!.friction = 0.0
        physicsWorld.gravity = CGVectorMake(0.0, 0.0)  // valeur par défaut (0.0, -9.8)

 
Action 2.16 – Testons l’application.
Note: Remarquez que la balle ne tombe plus.
 


Appliquons une direction et une force – un vecteur –  à la balle

Explication
Pour appliquer une force à un sprite, il faut utiliser sa méthode applyImpulse(unVecteur) et lui passer une direction et une force sous la forme d’un vecteur décrivant deltaX et deltaY.
Le vecteur est créé avec la fonction CGVectorMake(deltaX, deltaY)
 
physique01
 
Action 2.17 – Ajoutons le code suivant à la méthode ajouterBalle():

        // Attention: Il faut appliquer l'impulsion lorsque l'objet est dans un univers physique.
        // Après cette ligne:
        // self.addChild(ballon)
        let unVecteur = CGVectorMake(50, -20)
        ballon.physicsBody!.applyImpulse(unVecteur)
        println("Propulsé sur un angle de : \(atan2(unVecteur.dy,unVecteur.dx) * 180 / 3.141592)")
        // Ou bien:
        println("Propulsé sur un angle de : \( Double(atan2(unVecteur.dy,unVecteur.dx) * 180) / M_PI)")

 
Action 2.18 – Testons l’application.
Note: La balle perd de l’énergie sur impact des murs et lors d’un contact avec la palette.
Action 2.19 – Corrigeons la situation:

// Dans la méthode: ajouterBallon
        if let physique = personnage.physicsBody {
            physique.restitution    = 1.0
            physique.friction       =  0
            physique.linearDamping  = 0
            physique.allowsRotation = false
        }
//
// Dans la méthode: ajouterPalette
       if let physique = palette.physicsBody {
            physique.restitution    = 0.1
            physique.friction       = 0.4
         }

 
Action 2.20 – Testons l’application.
dyziy
Note: La balle peut se coincer avec un angle de zero ou de 90.  Nous verrons plus loin comment régler ce problème.


3 – Détection de collisions

Le système de physique de spriteKit propose des méthodes et des propriétés pour la détection de collisions.
Voici la marche à suivre pour mettre en place une détection de collision entre deux sprites:

Les étapes de détection de collisions:

1 – Définir des masques de bits des corps physiques: categoryBitMask
Action 3.1 – Ajoutons, dans la section des propriétés de la classe de la scène, la déclaration suivante:

    // 32 catégories
    struct categoriesPhysiquesDuJeu {
        static let aucun        : UInt32 = 0
        static let tous         : UInt32 = UInt32.max
        static let balle        : UInt32 = 0b1       // 1
        static let palette      : UInt32 = 0b10      // 2
    } // categoriesPhysiquesDuJeu

 
2 – Renseigner les masques:  ‘categoryBitMask‘ et ‘contactTestBitMask’ 
Action 3.2a – Ajoutons le code suivant à la méthode ajouterBallon:

// Dans ajouterBallon:
            physique.categoryBitMask    = categoriesPhysiquesDuJeu.balle
            physique.contactTestBitMask = categoriesPhysiquesDuJeu.palette // | categoriesPhysiquesDuJeu.autre | categoriesPhysiquesDuJeu.autre

Action 3.2b – Ajoutons le code suivant à la méthode ajouterPalette:

// Dans ajouterPalette:
            physique.categoryBitMask    = categoriesPhysiquesDuJeu.palette

 
3 – Mise en place et programmation du ‘contactDelegate’:

 
Action 3.3 – Ajoutons le code suivant à la classe de la scène (attention aux 3 étapes):

// 1 - Abonner la scène au protocole
class GameScene: SKScene, SKPhysicsContactDelegate
// 2 - renseigner le delegate
   func preparerLaScene(){
        ...
        physicsWorld.contactDelegate = self
    } // preparerLaScene
// 3 - Programmer la méthode didBeginContact
    func didBeginContact(contact: SKPhysicsContact) {
        println("Il y a eu contact entre deux objets")
        //println("Il y a eu contact entre: \(contact.bodyA) et \(contact.bodyB)")
        //println("Il y a eu contact entre: \(contact.bodyA.node!.name!) et \(contact.bodyB.node!.name!)")
    }

 
Action 3.4 – Testons l’application.
Résultat dans la console:

Il y a eu contact entre deux objets
Il y a eu contact entre deux objets
Il y a eu contact entre deux objets
Il y a eu contact entre deux objets

 
Exercice: Nommez le ballon, la palette et la scène pour obtenir:

Il y a eu contact entre: La scène et Le ballon
Il y a eu contact entre: La palette et Le ballon
Il y a eu contact entre: La scène et Le ballon
Il y a eu contact entre: La palette et Le ballon
Il y a eu contact entre: La scène et Le ballon

 

 


Déterminer les objets impliqués dans une collision

La méthode didBeginContact va recevoir une référence aux deux objets impliqués dans la collision:  contact.bodyA et contact.bodyB.
En testant la valeur de .categoryBitMask il sera possible de déterminer les objets reçus.
Nous avons donné au personnage (ballon) la plus petite valeur pour son masque de  catégorie.  En testant la taille du masque des deux objets reçus, il sera possible de déterminer quel objet entre bodyA et bodyB est le personnage; celui avec la plus petite valeur de masque.
 
Action 3.5 – Ajoutons le code suivant à la méthode didBeginContact:

// Dans la méthode: didBeginContact()
        var pasLaBalle:SKPhysicsBody
        var laBalle:SKPhysicsBody
        // La balle a la plus petite valeur de masque.
        // Déterminons quel objet est la balle
        if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
            pasLaBalle = contact.bodyB;
            laBalle = contact.bodyA;
        } else {
            pasLaBalle = contact.bodyA;
            laBalle = contact.bodyB;
        }  // Déterminer quel objet est la balle
        // La balle a touché la palette
        if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.palette {
            println("La balle a touché la palette")
            //println("La balle a touché la palette à la position: \(laBalle.node?.position)")
        } // if pasLaBalle.categoryBitMask

 
Action 3.6 – Testons l’application.
Résultat dans la console:

La balle a touché la palette
La balle a touché la palette
La balle a touché la palette
La balle a touché la palette

Note: Pour indiquer qu’un sprite peut entrer en collision avec plusieurs autres sprites, il faut fusionner les bits de masque avec l’opérateur ‘| » – OR bitWise – et placer le résultat dans unSpriteATester.contactTestBitMask.
Par exemple,

corpsPhysiqueDuBallon.contactTestBitMask = categoriesPhysiquesDuJeu.palette | categoriesPhysiquesDuJeu.insecte | categoriesPhysiquesDuJeu.bordure_ecran | categoriesPhysiquesDuJeu.bas_ecran 


Les actions – SKAction

Il est possible, avec spriteKit, de programmer des actions.
Par exemple,

Les actions sont programmées grace à la classe ‘SKAction‘.
 

Ajouter un effet sonore suite à la collision

 
Action 3.7 – Ajoutons le code suivant à la méthode didBeginContact:

        // La balle a touché la palette
        if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.palette {
            println("La balle a touché la palette")
            let playSFX = SKAction.playSoundFileNamed("jump.wav", waitForCompletion:false)
            runAction(playSFX)
         } // if pasLaBalle.categoryBitMask

 
Action 3.8 – Testons l’application.
Note: Vous devriez entendre un son à chaque fois que le personnage entre en collision avec la palette de jeu.
 


4 – Ajoutons les insectes

À cette étape du projet, nous devrions être en mesure d’ajouter des insectes au haut de la scène.
 
Action 4.1 – Ajoutons un nouveau masque physique de bits, permettant de détecter une collision entre la balle et un insecte :

   struct categoriesPhysiquesDuJeu {
        static let aucun        : UInt32 = 0
        static let tous         : UInt32 = UInt32.max
        static let balle        : UInt32 = 0b1       // 1
        static let palette      : UInt32 = 0b10      // 2
        static let insecte      : UInt32 = 0b100     // 4
    } // categoriesPhysiquesDuJeu

 
Action 4.2 – Ajoutons la méthode suivante, pour la création d’insectes au haut de l’écran;

// Note: Lancer la méthode dans didMoveToView()
// À ajouter dans les propriétés de la scène
        let MARGE_HAUT:CGFloat = 70
        let NB_INSECTES_PAR_LIGNE = 6
        var nbNiveau = 2
        var nbPoints = 0
        var nbVies = 3
    func ajouterInsectes(){
        let nbInsectesSurEcran = nbNiveau * NB_INSECTES_PAR_LIGNE
        let fichierInsect = "bug0\(nbNiveau)"
        for ligne in 0..<nbNiveau {
            for indiceInsect in 1...NB_INSECTES_PAR_LIGNE {
                let insecte = SKSpriteNode(imageNamed:fichierInsect)
                insecte.physicsBody = SKPhysicsBody(rectangleOfSize: insecte.frame.size)
                if let physique = insecte.physicsBody {
                    physique.dynamic            = false
                    physique.categoryBitMask    = categoriesPhysiquesDuJeu.insecte
                }
                let xPos = insecte.size.width * CGFloat(indiceInsect) - 15;
                let yPos = size.height - insecte.size.height * CGFloat(ligne) - MARGE_HAUT + 4;
                insecte.position = CGPointMake(xPos, yPos);
                addChild(insecte)
            } // for indiceInsect
        } // ligne
    }  // ajouterInsectes()

Note: Le nombre de lignes d’insectes est fonction de la valeur de la variable nbNiveau.  Par exemple, au niveau 3, il y aura trois lignes d’insectes.
 
Action 4.3 – Testons l’application.
tim.briques.2.01
Note: La balle est coincée entre les insectes.
 
Action 4.4 – Modifions la position de départ de la balle – ainsi que son image:

        ballon = SKSpriteNode(imageNamed: "lezard50px")
        ballon.position = CGPoint(x:ballon.frame.size.width, y:frame.height - 300)  // Idéalement, placer le ballon sous les insectes...

 
Action 4.5 – Testons l’application avec des valeurs entre 1 et 6 pour la variable nbNiveau.
 
Action 4.6 – À faire en laboratoire:  Vous devez programmer la détection de collisions entre le ballon et un insecte puis, retirer l’insecte de la scène.  Indice:  ‘pasLaBalle.node!.removeFromParent()’
Résultat:
dz50a
[expand title= »Afficher la solution »]

         // dans ajouterBallon
            physique.contactTestBitMask = categoriesPhysiquesDuJeu.palette | categoriesPhysiquesDuJeu.insecte
        // dans didBeginContact
        // La balle a touché un insecte
        if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.insecte {
            println("La balle a touché un insecte à la position: \(laBalle.node?.position)")
            let playSFX = SKAction.playSoundFileNamed("jump.wav", waitForCompletion:false)
            runAction(playSFX)
            pasLaBalle.node?.removeFromParent()
        } // if pasLaBalle.categoryBitMask == insecte

[/expand]
 


Ligne de statut pour l’application – vies et niveau

 
Nous allons ajouter, au haut de l’écran, une zone d’affichage permettant de connaitre la valeur des variables nbPoints, nbVies et nbNiveau.
 
Action 4.7 – Ajoutons la propriétés suivante à la classe de la scène principale:

    var labelStatut:SKLabelNode!

 
Action 4.8 – Ajoutons les méthodes suivantes à la classe de la scène principale:

// 1 - Ajouter
    /**
        Ajoutera une instance de labelStatut à la scène: Voir actualiserStatut()
        @param aucun
    */
    func ajouterStatut()
    {
        labelStatut = SKLabelNode(fontNamed:"Chalkduster") // [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        labelStatut.fontSize = 14;
        labelStatut.position = CGPointMake(CGRectGetWidth(self.frame)/2, CGRectGetHeight(self.frame) - 20);
        addChild(labelStatut)
        actualiserStatut()
    }  // ajouterStatut()
    /**
        Affiche, sur la scène, des informations sur le niveau, les vies restantes et le nombre de points accumulés.
    @param aucun
    */
    func actualiserStatut()
    {
        labelStatut.text = NSString(format:"Points: %06d  Niveau: %d  Vies: %d", nbPoints, nbNiveau, nbVies)
    } // actualiserStatut
// 2 - dans : didMoveToView(view: SKView), remplacer ajouterTitre() par:
        ajouterStatut()

Note: J’ai utilisé NSString(format:) pour pouvoir contrôler le nombre de ‘0’ dans le pointage.  %06d assure que les points seront affichés avec 6 caractères.  Si le nombre est petit, alors des zéros seront insérés à gauche du nombre.
 
Action 4.9 – Testons l’application:
tim.briques.2.02
 
Action 4.10 – À faire en laboratoire: À chaque collision avec un insecte, ajouter 5 points au total.
[expand title= »Afficher la solution »]

// dans:    if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.insecte {
               nbPoints+=5
               actualiserStatut()

[/expand]
 
 


Tester la collision au sol

Nous allons maintenant ajouter une mince rectangle, avec un corps physique, au bas de l’écran pour pouvoir déterminer que le personnage (ballon) a manqué la palette.
Dans ce cas, il faudra retrancher un point de vie du total.
Action 4.11 – Ajoutons la méthode suivante à la classe de la scène principale:

    // 1 - dans : struct categoriesPhysiquesDuJeu {
        static let bas_ecran    : UInt32 = 0b1000    // 8
    // 2 - dans : didMoveToView(view: SKView)
        ajouterBasEcran()
    // 3 - Ajouter
    /**
        Tracer un rectangle (0,1,largeurEcran, 3) au bas de l'écran.
        Sert à détecter, via le 'bitMask', si le personnage manque la palette.
    */
    func ajouterBasEcran()
    {
        let basEcran = SKNode.node()
        basEcran.physicsBody = SKPhysicsBody(edgeFromPoint: CGPointMake(0, 1), toPoint:CGPointMake(size.width, 3))
        basEcran.physicsBody!.categoryBitMask = categoriesPhysiquesDuJeu.bas_ecran  // à ajouter à la struct des 'categoriesPhysiques'
        addChild(basEcran)
    } // ajouterBasEcran

 
Action 4.12 – À faire en laboratoire: À chaque collision avec le bas de l’écran, soustraire 1 à nbVies et réafficher le statut.
Note: Pensez; à ajuster le masque de collisions du personnage, à retirer le ballon, à ajouter le ballon.
Résultat désiré:
e1bpu
[expand title= »Afficher la solution »]

// 1 - dans: ajouterBallon
            physique.contactTestBitMask = categoriesPhysiquesDuJeu.palette | categoriesPhysiquesDuJeu.insecte  | categoriesPhysiquesDuJeu.bas_ecran
// 2 - dans: didBeginContact()
       // La balle a touché le bas de l'écran
        if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.bas_ecran {
            laBalle.node?.removeFromParent()
            let playSFX = SKAction.playSoundFileNamed("jump.wav", waitForCompletion:false)
            runAction(playSFX)
            nbVies--
            actualiserStatut()
            ajouterBallon()
        } // if pasLaBalle.categoryBitMask == bas_ecran

[/expand]
 


5 – Passer à une autre scène

Un jeu peut proposer plusieurs scènes.
La méthode presentScene de l’optionnelle ‘view‘ permet de passer à une autre scène du jeu.
Si présente, la méthode ‘didMoveToView‘ de la destination sera exécutée.

Note
: La valeur des variables n’est pas préservée lors du passage d’une scène à l’autre.
Pour conserver l’état des certaines variables, il faut utiliser des variables statiques ou bien passer des paramètres entre les scènes.

La scène « Fin de la partie »

Action 5.1 – Ajoutons une nouvelle classe au projet:

//  FinPartie.swift
import SpriteKit
class FinPartie: SKScene {
    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        // retour à la scène précédente
        let debut = GameScene(size: self.size)
        view?.presentScene(debut, transition: SKTransition.doorsOpenHorizontalWithDuration(0.5))
    }  // touchesBegan
} // class FinPartie

Note:  Il est important de passer la taille de la scène de départ (à la ligne 8) entre les différentes scènes sans quoi nous nous retrouverons avec une scène de taille (0.1,0.1).
 

Passer à la scène finDePartie

Action 5.2 – Ajoutons le méthode suivante à la classe de  la scène principale:

    /**
      Permet de passer à la scène de fin de partie.
      À lancer, lorsque nbVies == 0.
    */
    func finDeLaPartie(){
        // Passer à une autre scène
        let uneScene = FinPartie(size: self.size)  // Créer une scène taille == courante.
        view?.presentScene(uneScene, transition: SKTransition.doorsOpenHorizontalWithDuration(0.5))
    }  // finDeLaPartie()

 
Action 5.3 – À faire en laboratoire: Afficher la scène ‘FinPartie’, lorsque nbVies == 0.
Note: Il faut ajouter le texte suivant à la scène:

Fin de la partie
Touchez l’écran pour recommencer.

 
Résultat désiré:
e1eb0
[expand title= »Afficher la solution sur le test de nbVies »]

// dans:    if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.bas_ecran {
            // nbVies--
             if nbVies == 0 {
                finDeLaPartie()
            }

[/expand]
 
[expand title= »Afficher la solution pour le texte de la scène FinPartie »]

    func ajouterLeTexte(){
        let unTitre = SKLabelNode(fontNamed:"Arial")
        unTitre.text = "Fin de la partie";
        unTitre.fontSize = 32;
        unTitre.position = CGPoint(x:CGRectGetMidX(frame), y:CGRectGetMidY(frame) + unTitre.frame.height);
        self.addChild(unTitre)
        let unTitre2 = SKLabelNode(fontNamed:"Arial")
        unTitre2.fontSize = 20;
        unTitre2.text = "Touchez l'écran pour recommencer.";
        unTitre2.position = CGPoint(x:CGRectGetMidX(frame), y:CGRectGetMidY(frame) - unTitre.frame.height);
        self.addChild(unTitre2)
    }  // ajouterLeTexte()

 
[/expand]
 


À faire en laboratoire

Afficher la scène ‘NiveauSuivant’, pendant 2 sec., lorsque nbInsectes == 0.
Note: Il faut ajouter et programmer la scène ‘NiveauSuivant’.
 
[expand title= »Afficher la solution pour la scène ProchainNiveau »]

import SpriteKit
class ProchainNiveau: SKScene {
    override func didMoveToView(view: SKView) {
        ajouterLeTexte()
        NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: "retour", userInfo: nil, repeats: false)
    } // didMoveToView()
    func ajouterLeTexte(){
        let unTitre = SKLabelNode(fontNamed:"Arial")
        unTitre.text = "Prochain niveau";
        unTitre.fontSize = 32;
        unTitre.position = CGPoint(x:CGRectGetMidX(frame), y:CGRectGetMidY(frame) + unTitre.frame.height);
        self.addChild(unTitre)
    }  // ajouterLeTitre()
    func retour(){
        // retour à la scène précédente
        let debut = GameScene(size: self.size)
        view?.presentScene(debut, transition: SKTransition.doorsOpenHorizontalWithDuration(0.5))
    } // retour()
} // class ProchainNiveau

[/expand]
 
Astuce: Pensez à désactiver le test de collision avec le bas de l’écran pendant l’élaboration de cette étape.
Il faut gérer le compteur d’insectes lors d’une collision avec le personnage:

Note: Pour préserver les points et le niveau, il faut utiliser des variables statiques.
Astuce:  En swift, une variable statique doit-être définie dans une structure ‘struct‘:

struct lesStatiques {
   static var nbNiveau = 1
   static var nbPoints = 0
} // lesStatiques

Note: Pensez à corriger les références à nbNiveau et nbPoints
 
Résultat désiré:
e1g0d
 
[expand title= »Afficher la solution »]

// Effacer les propriétés:
    var nbNiveau = 1
    var nbPoints = 0
// 1 - Ajouter les variables statiques
    struct lesStatiques {
        static var nbNiveau = 1
        static var nbPoints = 0
    } // lesStatiques
Note: Corriger les references (6 erreurs) à nbNiveau et nbPoints
// 2 - Ajouter dans les propriétés
   var nbInsectesSurEcran = 0
// 3 - dans: ajouterInsectes
  nbInsectesSurEcran = lesStatiques.nbNiveau * NB_INSECTES_PAR_LIGNE
// 4 - Ajouter:
     /**
    Permet de passer à la scène NiveauSuivant.
    À lancer, lorsque nbInsectesSurEcran == 0.
    */
    func prochainNiveau(){
        // Passer à une autre scène
        let uneScene = ProchainNiveau(size: self.size)  // Créer une scène taille == courante.
        view?.presentScene(uneScene, transition: SKTransition.doorsOpenHorizontalWithDuration(0.5))
    }  // finDeLaPartie()
// 5 - dans: La balle a touché un insecte
   nbInsectesSurEcran--
   if nbInsectesSurEcran == 0 {
      lesStatiques.nbNiveau++
      prochainNiveau()
    }

[/expand]
 


6 – Fonds d’écran et musique d’ambiance

Nous allons ajouter de la musique d’ambiance et un fond d’écran.
Ces deux éléments seront fonction de nbNiveau.

Action 6.1
 
– Ajoutons la méthode suivante à la classe de la scène principale:

// **** NOTE: À ajouter: import AVFoundation
// **** NOTE: À ajouter aux propriétés de la classe: var playerTrameSonore = AVAudioPlayer()
// **** NOTE: À ajouter à didMoveToView() : ajouterFondEcran()
    func ajouterFondEcran()
        // *******************************************************************************
    {
        let fichierFondEcran = "bg0\(lesStatiques.nbNiveau)"
        let fichierMusiqueAmbiance = "ambiance0\(lesStatiques.nbNiveau)"
        // Créer un sprite à partir du fichier image correspondant au niveau du jeu.
        let background = SKSpriteNode(imageNamed: fichierFondEcran)
        background.size = frame.size
        background.position = CGPoint(x:CGRectGetMidX(frame), y:CGRectGetMidY(frame))
        addChild(background)
        // Ajouter une trame sonore - voir Labo Aquarium 7.6 pour le détail.
        let URLTrameSonore = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(fichierMusiqueAmbiance, ofType: "mp3")!)
        var error:NSError?
        playerTrameSonore = AVAudioPlayer(contentsOfURL: URLTrameSonore, error: &error)
        playerTrameSonore.prepareToPlay()     // Charger le fichier son en mémoire
        playerTrameSonore.numberOfLoops = -1  // Boucle à l'infini avec une valeur négative
        playerTrameSonore.volume = 0.2
        playerTrameSonore.play()
    } // ajouterFondEcran()

 
Action 6.2 – Testons l’application
Note: Pour constater un changement de trame sonore et d’image de fond, il faut passez d’un niveau à l’autre.
 


7 – Gestion de la détérioration du vecteur du personnage

Vous avez peut-être remarqué, qu’à l’occasion, l’angle de déplacement du personnage est bloqué à l’horizontal ou à la vertical.
Pour corriger ce problème nous pourrions, lors d’une collision, mesurer l’angle du personnage – unObjet.velocity.dy, et dx  – et le corriger au besoin.

Action 7.1
 
– Ajoutons le code suivant dans didBeginContact:

// 1 - NOTE: ajouter le masque de bits suivant à:
// struct categoriesPhysiquesDuJeu {
   static let bordure_ecran: UInt32 = 0b10000   // 16
// 2 - NOTE: modifier le masque de contact du ballon:
       physique.contactTestBitMask = categoriesPhysiquesDuJeu.palette | categoriesPhysiquesDuJeu.insecte  | categoriesPhysiquesDuJeu.bas_ecran | categoriesPhysiquesDuJeu.bordure_ecran
// 3 - NOTE: ajouter le test suivant dans didBeginContact()
        // Le personnage a touché la bordure
        if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.bordure_ecran {
            let anglePersonnage = Double(atan2(laBalle.velocity.dy, laBalle.velocity.dx) * 180) / M_PI
            println("Touché la bordure velo.dx = \(laBalle.velocity.dx), velo.dy= \(laBalle.velocity.dy) - angle = \(anglePersonnage)")
            // Tester si l'angle de la trajectoire s'approche trop de la ligne horizontale.
            // velocity.dY entre -30 et 30 retourne un angle trop droit qui risque de coincer le personnage
            if (laBalle.velocity.dy > -30 && laBalle.velocity.dy < 30) {
                NSLog("Angle trop bas, réajustement...");
                laBalle.velocity = CGVectorMake(laBalle.velocity.dx, laBalle.velocity.dy + 400);
            }  // Angle trop bas
            // Tester si l'angle de la trajectoire s'approche trop de la ligne verticale.
            // velocity.dx entre -5 et 5 retourne un angle trop droit qui risque de coincer le personnage
            if (laBalle.velocity.dx > -5 && laBalle.velocity.dx < 5) {
                NSLog("Angle trop droit, réajustement...");
                laBalle.velocity = CGVectorMake(laBalle.velocity.dx + 100 , laBalle.velocity.dy);
            }  // Angle trop droit
        }  // Le personnage a touché la bordure de la scène

Note:   À vous d’améliorer le code précédent!
Si le personnage sort du cadre de la scène, il peut être replacé de la façon suivante:
Action 7.2 – Ajoutons le code suivant à la méthode update:

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
        if  ballon.position.x < 0 ||
            ballon.position.y < 0 ||
            ballon.position.x > self.size.width ||
            ballon.position.y > self.size.height
        {
            ballon.removeFromParent()
            ajouterBallon()
        }
     } // update()

 
 


8 – Ajoutons des particules

Xcode permet de créer et de configurer des particules en mode GUI.
Ces particules pourront par la suite être ajoutées à une des scènes.
NOTE:  Ne pas faire cette étape avec la version 6.0x de Xcode.  Un bug connu va corrompre votre projet.
Utiliser plutôt la version 6.1+ de Xcode.
Action 8.0.a – Ouvrir le projet, étape 7 complétée, et remplacer le contenu du fichier ‘GameViewController.swift’  par:

//  GameViewController.swift
//  TIM.Briques
import UIKit
import SpriteKit
extension SKNode {
    class func unarchiveFromFile(file : NSString) -> SKNode? {
        if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
            var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
            var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
            archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
            let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as GameScene
            archiver.finishDecoding()
            return scene
        } else {
            return nil
        }
    }
}
class GameViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        println("viewDidLoad")
        if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
            println("GameScene.unarchiveFromFile('GameScene')")
            // Configure the view.
            let skView = self.view as SKView
            skView.showsFPS = true
            skView.showsNodeCount = true
            /* Sprite Kit applies additional optimizations to improve rendering performance */
            skView.ignoresSiblingOrder = true
            /* Set the scale mode to scale to fit the window */
            scene.scaleMode = .AspectFill
            scene.size = skView.bounds.size
            skView.presentScene(scene)
        } else
        {
            println("Erreur sur: GameScene.unarchiveFromFile('GameScene')")
        }
    }
    override func shouldAutorotate() -> Bool {
        return true
    }
    override func supportedInterfaceOrientations() -> Int {
        if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
            return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
        } else {
            return Int(UIInterfaceOrientationMask.All.rawValue)
        }
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
}

Note: Une syntaxe différente entre 6.01 et 6.1 provoque des erreurs lorsque nous ouvrons notre projet avec la version 6.1 de Xcode
Action 8.0.b – Remplaçons la ligne suivante:

// dans: func ajouterBasEcran() {
       // REMPLACER: let basEcran = SKNode.node()  par,
        let basEcran = SKNode()

Note: Il ne devrait plus y avoir d’erreurs dans le projet.
 



Action 8.1
 
– Ajoutons un fichier de particules au projet:
particules.01
 
Action 8.2 – Renseignons le type de particule à créer:
particules.02
Action 8.2b – Nommons le fichier  » ‘etoiles.sks' ».
Action 8.3 – Observons le résultat présenté dans l’éditeur de Xcode.
e1k33
Note:  Si vous ne voyez pas ce qui précède, assurez-vous que le fichier ‘etoiles.sks’ est sélectionné dans l’explorateur du projet.
 
Il y a de nombreuses propriétés permettant de changer l’apparence et le comportement des particules.
La façon la plus simple pour les modifier est d’utiliser le panneau  ‘inspecteur SKNode’.
Note: Référez-vous à la documentation d’Apple  pour le détail de l’éditeur de particules.
 
Action 8.4 – Ajustons les propriétés de notre particule:
particules.03
 
Action 8.5 – Programmons l’affichage des particules suite à une collision du personnage avec un insecte.  Ajoutons le méthode suivante:

    /**
        Afficher des particules à la position 'coordonnees'
    */
    func etincelles(var coordonnees:CGPoint)
    {
        let _etincelles = SKEmitterNode(fileNamed:"etoiles.sks")
        _etincelles.position = coordonnees;
        addChild(_etincelles)
    }  // etincelles()

 
Action 8.6 – Ajoutons le code suivant à la méthode didBeginContact:

       // La balle a touché un insecte
        if pasLaBalle.categoryBitMask == categoriesPhysiquesDuJeu.insecte {
            println("La balle a touché un insecte")
            // Attention:  Placer avant le removeFromParent!
            etincelles(pasLaBalle.node!.position)
            ...
        }

Note: ‘pasLaballe.node!.position’ permet de connaitre la position de l’insecte.  Nous voulons afficher les particules à cette position.
Voila, il ne reste plus qu’à tester l’application finale!
d8ll3
Télécharger le projet terminé: TIM.Briques.solution
 


Fin du document – (c) Alain Boudreault – Révision 2014.12.02