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:
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:
Action 1.2– Nommons le projet et sélectionnons ‘SpriteKit’ sous ‘Game Technologie’: Note:sceneKit et Metal permettent le développement de jeux 3D.
Action 1.3– Testons l’application (Touchez à l’écran pendant l’exécution)
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:
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:
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.
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.
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.
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.
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.
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.
Action 1.13– Ajoutons un ballon (fourni dans le fichier des ressources du projet) au centre la scène.
// 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:
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):
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.
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:
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
bodyWithCircleOfRadius:
bodyWithCircleOfRadius:center:
bodyWithRectangleOfSize:
bodyWithRectangleOfSize:center:
bodyWithBodies:
bodyWithPolygonFromPath:
bodyWithTexture:size:
bodyWithTexture:alphaThreshold:size:
À partir d’une forme vide
bodyWithEdgeLoopFromRect:
bodyWithEdgeFromPoint:toPoint:
bodyWithEdgeLoopFromPath:
bodyWithEdgeChainFromPath:
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.
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.
mass
density
area
friction
restitution
linearDamping
angularDamping
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.
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.
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’)
[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.
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():
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)
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.
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:
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’:
souscrire au protocole SKPhysicsContactDelegate
renseigner le delegate
implémenter la méthode didBeginContact
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,
Il est possible, avec spriteKit, de programmer des actions.
Par exemple,
déplacer un sprite,
appliquer un effet de fondu,
jouer un effet sonore,
…
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.
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:
[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:
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é:
[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é:
[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 »]
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:
ajouter une propriété
renseigner la propriété dans la méthode ajouterInsectes
décrémenter la propriété lors d’une collision avec le personnage
suite à une collision, si == 0 alors augmenter la valeur de nbNiveau puis afficher ‘NiveauSuivant’
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‘:
Note: Pensez à corriger les références à nbNiveau et nbPoints
Résultat désiré:
[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:
Action 8.2– Renseignons le type de particule à créer:
Action 8.2b– Nommons le fichier » ‘etoiles.sks' ». Action 8.3– Observons le résultat présenté dans l’éditeur de Xcode.
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:
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!