CNDP – Atelier jeux vidéo

Site de support du club Création de jeux vidéo.

Scanner avec votre iPad pour afficher cette page

2024-2025

import SwiftUI

struct GameView : View {
    @ObservedObject var viewModel: GameViewModel
    
    @State private var grid = Array(repeating: Array(repeating: false, count: 3), count: 3)
    
    @State private var score = 0
    @State private var timer:Timer? = nil
    @State private var time = 30
    @State private var gameTimer:Timer? = nil
    
    @State private var gameActive = false
    @State private var activeTile: (row: Int, col: Int)? = nil
    
    
    var body: some View {
        VStack {
            HStack {
                
                Text("Score : \(score)")
                    .font(.title)
                    .padding()
                
                Text("\(time)⏱️")
                    .font(.title)
                    .padding()
            }
            
            gridView
                .padding()
            
            Button(gameActive ? "STOP" : "START") {
                if(gameActive) {
                    stopGame()
                } else {
                    startGame()
                }
            }
            .padding()
            .background(gameActive ? .red : Color.blue)
            .foregroundColor(Color.white)
            .cornerRadius(10)
            .font(.title)
        }
    }
    
    var gridView : some View {
        VStack() {
            ForEach(0..<3) { row in
                HStack(){
                    ForEach(0..<3) { col in 
                        tileView(row:row, col:col)
                    }
                }
            }
        }
    }
    
    func tileView(row: Int, col: Int) -> some View {
        RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
            .foregroundColor(grid[row][col] ? Color.yellow : Color.blue)
            .frame(width: 80, height: 80)
            .onTapGesture {
                tapTile(row:row, col:col)
            }
    }
    
    func tapTile(row:Int, col:Int) {
        guard gameActive else { return }
        if grid[row][col] {
            score += 1
            grid[row][col] = false
            activeTile = nil
        }
    }
    
    func startGame() {
        gameActive = true
        
        gameTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            time = max(0, time-1)
            
            if time == 0 {
                stopGame()
            }
        }
        
        timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
            lightRandomTile()
        }
        
        reset()
    }
    
    func lightRandomTile() {
        // desactiver l'ancienne tuile
        if let active = activeTile {
            grid[active.row][active.col] = false
            
            score = max(0, score - 1)
            activeTile = nil
        }
        
        // Choisi une tile aleatoirement
        let col = Int.random(in: 0..<3)
        let row = Int.random(in: 0..<3)
        
        grid[row][col] = true
        activeTile = (row:row, col:col)
        
    }
    
    func stopGame() {
        gameActive = false
        timer?.invalidate()
        timer = nil
        gameTimer?.invalidate()
        gameTimer = nil
    }
    
    func reset() {
        score = 0
    }
    
    func swichOffAllTiles() {
        // Eteint les cases
        for row in 0..<3 {
            for col in 0..<3 {
                grid[row][col] = false
            }
        }
    }
}

Langage du code : Swift (swift)

2023-2025

Copier le code source (iOS 17.0+)

Créer une nouvelle App sur Swift Playgrounds. Remplacez le code existant en copiant/collant les codes suivants.

Modèle vide

Voici un modèle (template) vide pour vous lancer dans votre propre jeu. Il permet de lancer une vue SpriteKit avec SwiftUI.

Commencez par remplacer le contenu des fichiers MyApp.swift et ContentView.swift par ceux disponible ci-dessous

MyApp.swift

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Langage du code : Swift (swift)

ContentView.swift

import SwiftUI
import SpriteKit

struct ContentView: View {
    var body: some View {
        SpriteView(scene: GameScene(), transition: SKTransition.crossFade(withDuration: 0.5), isPaused: false, preferredFramesPerSecond: 60)
    }
}

Langage du code : Swift (swift)

Enfin ajoutez un fichier Swift GameScene contenant cette base de jeu prête à gérer une caméra et la physique de base.

GameScene.swift

import SwiftUI
import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate {
    var mainCamera = SKCameraNode()
    
    override func didMove(to view: SKView) {
        
        physicsWorld.contactDelegate = self
        
        scene?.size = CGSize(width: 600, height: 800)
        scene?.scaleMode = SKSceneScaleMode.aspectFill
        scene?.camera = mainCamera
    }
}
Langage du code : Swift (swift)

Suika Game

GameScene.swift

import SwiftUI
import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate {
    var mainCamera = SKCameraNode()
    var nextFruitType: FruitType!
    var nextFruit: SKNode!
    var currentFruitType: FruitType!
    var currentFruit: SKNode!
    
    var lastPos:CGPoint!
    
    var score = 0
    var scoreLbl: SKLabelNode!
    
    
    override func didMove(to view: SKView) {
        
        physicsWorld.contactDelegate = self
        
        scene?.size = CGSize(width: 600, height: 800)
        scene?.scaleMode = SKSceneScaleMode.aspectFill
        scene?.camera = mainCamera
        
        addChild(generateTank())
        lastPos = CGPoint(x: 0, y: tankHeight / 2 + 20)
        nextFruitType = FruitType.availableFruits.randomElement()
        setCurrentFruit()
        
        addChild(createUI())
    }
    
    func createUI() -> SKLabelNode {
        scoreLbl = SKLabelNode()
        scoreLbl.position = CGPoint(x: 0, y: 350)
        scoreLbl.fontName = ""
        scoreLbl.color = UIColor.white
        scoreLbl.text = String(score)
        
        return scoreLbl
    }
    
    func setCurrentFruit() {
        currentFruitType = nextFruitType
        currentFruit = makeFruit(fruitType: currentFruitType, hasGravity: false)
        currentFruit.position = lastPos
        addChild(currentFruit)
        
        createNextFruit()
    }
    
    func createNextFruit() {
        nextFruitType = FruitType.availableFruits.randomElement()
        nextFruit?.removeFromParent()
        nextFruit = makeFruit(fruitType: nextFruitType, hasGravity: false)
        nextFruit.position = CGPoint(x: 125, y: 350)
        
        addChild(nextFruit)
    }
    
    var isReleased = false
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if(isReleased) {
            return
        }
        
        let touch = touches.first
        
        if var pos = touch?.location(in: self) {
            pos.y = lastPos.y
            
            if(pos.x > CGFloat(tankWidth) / 2.5) {
                pos.x = CGFloat(tankWidth) / 2.5
            }
            else if(pos.x < -CGFloat(tankWidth) / 2.5) {
                pos.x = -CGFloat(tankWidth) / 2.5
            }
            
            currentFruit.position = pos
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if(isReleased) {
            return
        }
        isReleased = true
        
        let touch = touches.first
        
        if var pos = touch?.location(in: self) {
            pos.y = lastPos.y
            
            if(pos.x > CGFloat(tankWidth) / 2.5) {
                pos.x = CGFloat(tankWidth) / 2.5
            }
            else if(pos.x < -CGFloat(tankWidth) / 2.5) {
                pos.x = -CGFloat(tankWidth) / 2.5
            }
            
            lastPos.x = pos.x
            
            currentFruit.position = pos
            currentFruit.physicsBody?.affectedByGravity = true
            
        }
    }
    
    //physics
    func didBegin(_ contact: SKPhysicsContact) {
        let nodeA = contact.bodyA.node
        let nodeB = contact.bodyB.node
        
        if(nodeA == currentFruit || nodeB == currentFruit) {
            setCurrentFruit()
            isReleased = false
        }
        
        if(nodeA?.name == "fruit" && nodeB?.name == "fruit") {
            let fruitA: FruitType = nodeA?.userData!["fruitType"] as! FruitType
            let fruitB: FruitType = nodeB?.userData!["fruitType"] as! FruitType
            
            if( fruitA == fruitB) {
                
                score += 2 * (fruitA.rawValue + 1)
                scoreLbl.text = String(score)
                
                nodeA?.removeFromParent()
                nodeB?.removeFromParent()
                
                if let nextFruitType = FruitType(rawValue: fruitA.rawValue+1) {
                    let fruit = makeFruit(fruitType: nextFruitType)
                    fruit.position = contact.contactPoint
                    
                    addChild(fruit)
                }
                
            }
            
            
            
        }
    }
}


Langage du code : Swift (swift)

FruitGenerator.swift

Cette fonction s’occupe de générer des fruits à la bonne taille et de la bonne couleur

import SwiftUI
import SpriteKit

enum FruitType: Int, CaseIterable {
    case cerise,
    fraise,
    grenade,
    orange,
    pomme,
    citron,
    peche,
    ananas,
    melon,
    pasteque
    
    static let availableFruits = [cerise, fraise, grenade, orange]
}

func makeFruit(fruitType: FruitType, hasGravity: Bool = true)->SKNode {
    var radius = 10.0
    var imageName: String = "cerise"
    
    switch fruitType {
    case .cerise:
        radius = 10.0
        imageName = "cerise"
    case .fraise:
        radius = 15.0
        imageName = "cerise"
    case .grenade:
        radius = 20.0
        imageName = "cerise"
    case .orange:
        radius = 25.0
        imageName = "cerise"
    case .pomme:
        radius = 30.0
        imageName = "cerise"
    case .citron:
        radius = 35.0
        imageName = "cerise"
    case .peche:
        radius = 40.0
        imageName = "cerise"
    case .ananas:
        radius = 60.0
        imageName = "cerise"
    case .melon:
        radius = 70.0
        imageName = "cerise"
    case .pasteque:
        radius = 80.0
        imageName = "cerise"
    }
    
    let node = SKSpriteNode(imageNamed: imageName)
    node.name = "fruit"
    node.size = CGSize(width: radius*2, height: radius*2)
    
    node.userData = NSMutableDictionary()
    node.userData?.setValue(fruitType, forKey: "fruitType")
    
    node.physicsBody = SKPhysicsBody(circleOfRadius: radius)
    node.physicsBody?.affectedByGravity = hasGravity
    node.physicsBody?.contactTestBitMask = 0x1
    
    return node
}

Langage du code : Swift (swift)

TankGenerator.swift

Cette fonction génère le réservoir à fruit avec la physique qui va bien.

import SwiftUI
import SpriteKit

let tankWidth = 300
let tankHeight = 400

func generateTank()->SKNode {
    let path = CGMutablePath()
    path.move(to: CGPoint(x: 0, y: 0))
    path.addLine(to: CGPoint(x: 0, y: -tankHeight))
    path.addLine(to: CGPoint(x: tankWidth, y: -tankHeight))
    path.addLine(to: CGPoint(x: tankWidth,y: 0))
    
    let node = SKShapeNode(path: path, centered: true)
    node.lineWidth = 10.0
    node.name = "tank"
    
    node.physicsBody = SKPhysicsBody(edgeChainFrom: node.path!)
    
    return node
}

Langage du code : Swift (swift)

MyApp.swift

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Langage du code : Swift (swift)

ContentView.swift

import SwiftUI
import SpriteKit

struct ContentView: View {
    var body: some View {
        SpriteView(scene: GameScene(), transition: SKTransition.crossFade(withDuration: 0.5), isPaused: false, preferredFramesPerSecond: 60)
    }
}

Langage du code : Swift (swift)

Flappy Bird

Télécharger les images

Télécharger les images de Flappy Birds

Remplacez le code existant par celui-ci

import SwiftUI

@main // Indique que c'est le fichier principal qui sera lancé en premier
struct MyApp: App { // Définition du contenu de l'App
    var body: some Scene { // Corps de l'application
        WindowGroup {
            ContentView() // Charge la vue définie dans le fichier ContentView
        }
    }
}Langage du code : JavaScript (javascript)

ContentView

Remplacez le code existant par celui-ci

import SwiftUI    // Paquet gérant l'interface
import SpriteKit  // Paquet gérant le moteur de jeu 2D

struct ContentView: View { // Vue chargéé dans l'application
    var scene: SKScene { // Scene contenant le jeu
        let scene = GameScene() // Scene définie dans le fichier GameScene
        scene.size = CGSize(width: 600, height: 600) // On fixe la taille de la scène en 600px de large par 600px de long
        return scene
    }
    
    var body: some View {
       Text("Mon jeu") // Élément d'interface pour afficher du text
            .font(.largeTitle) // On utilise le style largeTitle pour agrandir le texte.
            
        SpriteView(scene: scene) // Élément d'interface permettant d'afficher une scène SpriteKit
            .scaledToFit() // Agrandi la vue pour qu'elle touche un bord sans déformer le contenu
        
    }
}Langage du code : JavaScript (javascript)

GameScene

Le fichier GameScene n’existe pas. Il faut le créer en appuyant sur l’icône > Fichier Swift

import SwiftUI
import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate { // Scene SpriteKit
    
    var score = 0
    var scoreLabel: SKLabelNode!
    
    var highScore = 0
    var highScoreLabel: SKLabelNode!
    
    struct PhysicsCategory {
        static let none : UInt32 = 0
        static let all : UInt32 = UInt32.max
        static let player : UInt32 = 0b0001
        static let pipe : UInt32 = 0b0010
        static let ground : UInt32 = 0b0100
        static let gate : UInt32 = 0b1000
    }
    
    // Gére le début d'un contact
    func didBegin(_ contact: SKPhysicsContact) {
        if(contact.bodyA.node?.name == "player" && contact.bodyB.node?.name == "pipe") {
            // Nous allons faire recommancer le jeu ici
            startGame()
        }
    }
    
    // Gére la fin d'un contact
    func didEnd(_ contact: SKPhysicsContact) {
        if(contact.bodyA.node?.name == "player" && contact.bodyB.node?.name == "gate") {
            // Nous allons faire recommancer le jeu ici
            
            score = score + 1
            scoreLabel?.text = "\(score)"
            
            if(score > highScore) {
                highScore = score
                UserDefaults.standard.setValue(highScore, forKey: "highScore")
                UserDefaults.standard.synchronize()
                highScoreLabel?.text = "\(highScore)"
            }
            
            
            
        }
    }
    
    // Redémarre le jeu
    func startGame() {
        let scene = GameScene()
        scene.size = CGSize(width: 600, height: 600)
        view?.presentScene(scene, transition: SKTransition.fade(withDuration: 0.5))
    }
    
    // Fonction qui sert à créer le joueur
    func createPlayer()->SKSpriteNode { // Elle renvoie un Noeud qu'on peut ajouter par la suite dans la scène
        let flappyFlyTexture = [SKTexture(imageNamed: "flappy_red01"),
                                SKTexture(imageNamed: "flappy_red02"),
                                SKTexture(imageNamed: "flappy_red03")]
        
        for texture in flappyFlyTexture {
            texture.filteringMode = SKTextureFilteringMode.nearest
        }
        
        let player = SKSpriteNode(imageNamed: "flappy_red01")
        
        let flappyAnim = SKAction.animate(with: flappyFlyTexture, timePerFrame: 0.1)
        player.run(SKAction.repeatForever(flappyAnim))
        
        player.texture?.filteringMode = SKTextureFilteringMode.nearest
        player.size = CGSize(width: 60, height: 50)
        player.position = CGPoint(x: 100, y: 300) // position permet de déplacer le noeud sur la scène. Ici nous le centrons
        player.name = "player" // name permet de récupérer le noeud grace à son nom avec la méthode childNode(withName: "player") par exemple
        player.physicsBody = SKPhysicsBody(rectangleOf: player.size) // PhysicBody est la représentation du noeud dans le moteur physique. Ici c'est un rectangle de la taille du player
        player.physicsBody?.categoryBitMask = PhysicsCategory.player
        player.physicsBody?.collisionBitMask = PhysicsCategory.ground
        
        return player;
    }
    
    func createBG()->SKSpriteNode {
        let bg = SKSpriteNode(imageNamed: "background_night")
        bg.size = CGSize(width: 600, height: 600)
        bg.position = CGPoint(x: 300, y: 300)
        
        return bg
    }
    
    // Fonction qui permet de créer des ronds de rayon radius
    func createRond(radius:Double)->SKShapeNode {
        let rond = SKShapeNode(circleOfRadius: radius) // Constante qui contient un noeud de forme. Ici un cercle.
        rond.strokeColor = .white // Couleur du contour
        rond.fillColor = .red // Couleur du remplissage
        rond.physicsBody = SKPhysicsBody(circleOfRadius: radius) // Corps physique en forme de cercle cette fois-ci
        rond.physicsBody?.isDynamic = false // Si c'est faux, le corps n'est pas déplacé par le moteur physique
        rond.position = CGPoint(x: 600, y: 400)
        
        return rond
    }
    
    func createUI() {
        scoreLabel = SKLabelNode(text: "0")
        scoreLabel.name = "score"
        scoreLabel.position = CGPoint(x: 300, y: 550)
        scoreLabel.fontSize = 50
        scoreLabel.fontName = ""
        addChild(scoreLabel)
        
        highScoreLabel = SKLabelNode(text: "0")
        highScoreLabel.name = "hightscore"
        highScoreLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.right
        highScoreLabel.position = CGPoint(x: 590, y: 570)
        highScoreLabel.fontSize = 30
        highScoreLabel.fontName = ""
        highScoreLabel.zPosition = 200
        addChild(highScoreLabel)
        highScoreLabel?.text = "\(highScore)"
    }
    
    func createGround()->SKSpriteNode {
        // Création d'un sol pour que le joueur repose dessus
        let ground = SKSpriteNode(imageNamed: "ground")
        ground.texture?.filteringMode = SKTextureFilteringMode.nearest
        ground.size = CGSize(width: 600, height: 100)
        ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size)
        ground.physicsBody?.isDynamic = false;
        ground.position = CGPoint(x: 300, y: 0)
        return ground 
    }
    
    func moveGround() {
        let parent = SKNode()
        
        let ground = createGround()
        ground.position = CGPoint(x: 300, y: 0)
        let ground2 = createGround()
        ground2.position = CGPoint(x: 900, y: 0)
        
        parent.addChild(ground)
        parent.addChild(ground2)
        
        let sequence = SKAction.sequence([SKAction.moveTo(x: -600, duration: 3),
                                          SKAction.removeFromParent()])
        parent.run(sequence)
        addChild(parent)
        
        
    }
    
    override func update(_ currentTime: TimeInterval) {
        scoreLabel?.zPosition = 100
    }
    
    // Fonction spéciale de SKScene appelée une seule fois indiquant que la vue a fini de s'afficher pour la premier fois
    override func didMove(to view: SKView) {
        highScore = UserDefaults.standard.integer(forKey: "highScore")
        
        physicsWorld.contactDelegate = self
        // une scène est également un noeud qui contient lui aussi un physicsBody
        self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame) // Nous créons un edgeLoop qui permet de créer une sorte de mur sur le contour de la scène empêchant les objets d'en sortir
        
        addChild(createBG())
        
        let player = createPlayer() // Nous créons le joueur
        addChild(player) // que nous ajoutons à la scène pour qu'il s'affiche et intéragisse avec le moteur physique
        player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
        
        let rond = createRond(radius:50)// Nous créons un cercle de diamètre 50
        //addChild(rond)
        
        // Création des actions pour animer le cercle de gauche à droite
        let action = SKAction.moveTo(x: 0, duration: 2) // Cette action envoie un noeud jusq'à sa position x = 0
        let action2 = SKAction.moveTo(x:600, duration: 2) // Cette action envoie un noeud jusqu'à sa position x = 600
        
        // Une séquence permet d'appeler des actions les unes après les autres
        // Notez l'utilisation des crochets [a,b,c] qui permettent de définir un tableau. Ici le tableau contient les éléments a, b et c. Les éléments sont séparés par des virgules.
        let sequence = SKAction.sequence([action, action2])
        // Pour lancer les actions on utilise la méthode run() d'un noeud.
        rond.run(SKAction.repeatForever(sequence)) // la méthode repeatForever, comme son nom l'indique permet de réperter des actions à l'infini.
        
        // Fonction qui appelle notre créateur d'obstacles à l'infini.
        run(SKAction.repeatForever(SKAction.sequence([
            SKAction.run(createObstacle),
            SKAction.wait(forDuration: 2)
        ])))
        
        run(SKAction.repeatForever(SKAction.sequence([
            SKAction.run(moveGround),
            SKAction.wait(forDuration: 3)
        ])))
        
        
        
        createUI()
    }
    
    // Fonction spécial de UIKit qui permet de récupérer les intéractions entre les doigts et l'écran de l'iPad ou iPhone
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //let touch = touches.first // Voici comment récupérer le premier touché sur l'écran
        
        if let player = self.childNode(withName: "player") { // Nous cherchons le noeud contenant le nom player
            player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 100)) // Nous appliquons une impulsion vers le haut au joueur pour le faire sauter.
        }
        //addChild(CreatePlayer())
    }
    
    // Fonction qui crée des obstable
    func createObstacle() {
        let ecart = 250
        
        let obstacleBottom = SKSpriteNode(imageNamed: "pipe_top")
        obstacleBottom.size = CGSize(width: 100, height: 300)
        
        obstacleBottom.position = CGPoint(x: 0, y: ecart)
        obstacleBottom.name = "pipe"
        obstacleBottom.physicsBody = SKPhysicsBody(rectangleOf: obstacleBottom.size)
        obstacleBottom.physicsBody?.isDynamic = false
        obstacleBottom.physicsBody?.categoryBitMask = PhysicsCategory.pipe
        obstacleBottom.physicsBody?.contactTestBitMask = PhysicsCategory.player
        
        let obstacleTop = SKSpriteNode(imageNamed: "pipe_bottom")
        obstacleTop.size = CGSize(width: 100, height: 300)
        obstacleTop.position = CGPoint(x: 0, y: -ecart)
        obstacleTop.name = "pipe"
        obstacleTop.physicsBody = SKPhysicsBody(rectangleOf: obstacleBottom.size)
        obstacleTop.physicsBody?.isDynamic = false
        obstacleTop.physicsBody?.categoryBitMask = PhysicsCategory.pipe
        obstacleTop.physicsBody?.contactTestBitMask = PhysicsCategory.player
        
        let gate = SKSpriteNode(color: .white, size: CGSize(width: 50, height: (ecart - 300 / 2) * 2)) // Taille aléatoire en hauteur entre 20 et 200 unités
        gate.name = "gate"
        gate.physicsBody = SKPhysicsBody(rectangleOf: gate.size)
        gate.physicsBody?.isDynamic = false
        gate.physicsBody?.collisionBitMask = PhysicsCategory.none
        gate.physicsBody?.categoryBitMask = PhysicsCategory.gate
        gate.physicsBody?.contactTestBitMask = PhysicsCategory.player
        gate.alpha = 0
        
        
        // Créer l'animation de droite vers la gauche grâce aux actions
        let bougeVersLaGauche = SKAction.moveTo(x: -50, duration: 3)
        let supprimer = SKAction.removeFromParent()
        let sequence = SKAction.sequence([bougeVersLaGauche, supprimer])
        
        let parent = SKNode()
        parent.position = CGPoint(x: 650, y: Double.random(in: 200...400))
        parent.addChild(obstacleBottom)
        parent.addChild(obstacleTop)
        parent.addChild(gate)
        
        parent.run(sequence)
        
        addChild(parent)
    }
    
    
}
Langage du code : HTML, XML (xml)

Installer le playground (iOS 17.1)

Votre iPad doit être sur la version iOS 17.1 minimum. Sinon ça ne fonctionnera pas.

  1. Scannez le QR avec l’iPad pour afficher cette page

2. Téléchargez le Playground ⬇️

⚠️ Attention ⚠️
il faut iOS 17.1 minimum

3. Regardez la vidéo pour ajouter le Playground dans Swift Playgrounds

Voir +