Categories
Playground SpriteKit SWIFT

Pong like game as iPad SWIFT Playground

Last week on WWDC Apple showed a short demo about a Swift Playground for iOS, which was part of iOS 10 on iPads. They focused on using playgrounds to teach kids and students coding. That seems a little bit unspectacular and is a great understatement. Swift Playground support a lot of advanced iOS libraries like SpriteKit, SceneKit, UIKit

I was wondering if this can really replace a Mac with XCode to develop for my favorite platform. In fact I’m dreaming about only carrying my iPad with me and develop directly on an iPad.

 

So here we are: My first game SWIFT Playground completely written  on an iPad: Pong (updated for iOS 10 Beta 5)

 

1. Open the Playground App on your iPad:

PlaygroundLogo

2. Create a new SWIFT Playground:

IMG_2575iPad Create a new Playground

 

3. Here is the SourceCode. Just copy it in your playground and hit start. I’ll not introduce the basics of SpriteKit. You can check my other posts to get an introduction.

import SpriteKit
import PlaygroundSupport

// Declare some global constants
let width = 800 as CGFloat
let height = 1200 as CGFloat
let racketHeight = 150 as CGFloat
let ballRadius = 20 as CGFloat

// Three types of collision objects possible
enum CollisionTypes: UInt32 {
    case Ball = 1
    case Wall = 2
    case Racket = 4
}

// Racket direction
enum Direction: Int{
    case None = 0
    case Up = 1
    case Down = 2
}

// SpriteKit scene
class gameScene: SKScene, SKPhysicsContactDelegate {
    let racketSpeed = 500.0 
    var direction = Direction.None
    var score = 0
    var gameRunning = false 
    
    // Screen elements
    var racket: SKShapeNode?
    var ball: SKShapeNode?
    let scoreLabel = SKLabelNode()
    
    // Initialize objects during first start
    override func sceneDidLoad() {
        super.sceneDidLoad()
        var resr = SKShapeNode()
        scoreLabel.fontSize = 40
        scoreLabel.position = CGPoint(x: width/2, y: height - 100)
        self.addChild(scoreLabel)
        
        createWalls()
        createBall(position: CGPoint(x: width / 2, y: height / 2))
        createRacket()
        startNewGame()
        self.physicsWorld.contactDelegate = self
    }
    
    // Create the ball sprite 
    func createBall(position: CGPoint) {
        let physicsBody = SKPhysicsBody(circleOfRadius: ballRadius)
        ball = SKShapeNode(circleOfRadius: ballRadius)
        physicsBody.categoryBitMask = CollisionTypes.Ball.rawValue
        physicsBody.collisionBitMask = CollisionTypes.Wall.rawValue | CollisionTypes.Ball.rawValue | CollisionTypes.Racket.rawValue
        physicsBody.affectedByGravity = false
        physicsBody.restitution = 1
        physicsBody.linearDamping = 0
        physicsBody.velocity = CGVector(dx: -500, dy: 500)
        ball!.physicsBody = physicsBody
        ball!.position = position
        ball!.fillColor = SKColor.white
    }
    
    // Create the walls
    func createWalls() {
        createWall(rect: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: ballRadius, height: height)))
        createWall(rect: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: width, height: ballRadius)))
        createWall(rect: CGRect(origin: CGPoint(x: 0, y: height - ballRadius), size: CGSize(width: width, height: ballRadius)))
    }
    
    func createWall(rect: CGRect) {
        let node = SKShapeNode(rect: rect)
        node.fillColor = SKColor.white
        node.physicsBody = getWallPhysicsbody(rect: rect)
        self.addChild(node)
    }
    
    // Create the physics objetcs to handle wall collisions 
    func getWallPhysicsbody(rect: CGRect) -> SKPhysicsBody {
        let physicsBody = SKPhysicsBody(rectangleOf: rect.size, center: CGPoint(x: rect.midX, y: rect.midY))
        physicsBody.affectedByGravity = false
        physicsBody.isDynamic = false
        physicsBody.collisionBitMask = CollisionTypes.Ball.rawValue
        physicsBody.categoryBitMask = CollisionTypes.Wall.rawValue
        return physicsBody
    }
    
    // Create the racket sprite
    func createRacket() {
        racket =  SKShapeNode(rect: CGRect(origin: CGPoint.zero, size: CGSize(width: ballRadius, height: racketHeight)))
        self.addChild(racket!)
        racket!.fillColor = SKColor.white
        let physicsBody = SKPhysicsBody(rectangleOf: racket!.frame.size, center: CGPoint(x: racket!.frame.midX, y: racket!.frame.midY))
        physicsBody.affectedByGravity = false
        physicsBody.isDynamic = false
        physicsBody.collisionBitMask = CollisionTypes.Ball.rawValue
        physicsBody.categoryBitMask = CollisionTypes.Racket.rawValue
        physicsBody.contactTestBitMask = CollisionTypes.Ball.rawValue
        racket!.physicsBody = physicsBody
    }
    
    // Start a new game
    func startNewGame() {
        score = 0
        scoreLabel.text = "0"
        racket!.position = CGPoint(x: width - ballRadius * 2, y: height / 2)
        
        var counter = 3
        let startLabel = SKLabelNode(text: "Game Over")
        startLabel.position = CGPoint(x: width / 2, y: height / 2)
        startLabel.fontSize = 160
        self.addChild(startLabel)
        
        // Animated countdown
        let fadeIn = SKAction.fadeIn(withDuration: 0.5)
        let fadeOut = SKAction.fadeOut(withDuration: 0.5)
        startLabel.text = "3"
        startLabel.run(SKAction.sequence([fadeIn, fadeOut]), completion: {
            startLabel.text = "2"
            startLabel.run(SKAction.sequence([fadeIn, fadeOut]), completion: {
                startLabel.text = "1"
                startLabel.run(SKAction.sequence([fadeIn, fadeOut]), completion: {
                    startLabel.text = "0"
                    startLabel.run(SKAction.sequence([fadeIn, fadeOut]), completion: {
                        startLabel.removeFromParent()
                        self.gameRunning = true
                        self.ball!.position = CGPoint(x: 30, y: height / 2)
                        self.addChild(self.ball!)
                    })
                })
            })
        })
    }
    
    // Handle touch events to move the racket
    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        for touch in touches {
            var location = touch.location(in: self)
            if location.y > height / 2 {
                direction = Direction.Up
            } else if location.y < height / 2{
                direction = Direction.Down
            }
        }
    }
    
    // Stop racket movement
    override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        direction = Direction.None
    }
    
    // Game loop: 
    // - Game over check: Ball still on screen
    // - Trigger racket movement
    var dt = TimeInterval(0)
    override func update(_ currentTime: TimeInterval) {
        if gameRunning {
            super.update(currentTime)
            checkGameOver()
            if dt > 0 {
                moveRacket(dt: currentTime - dt)
            }
            dt = currentTime
        }
    }
    
    // Move the racket up or down
    func moveRacket(dt: TimeInterval) {
        if direction == Direction.Up && racket!.position.y < height - racketHeight {
            racket!.position.y = racket!.position.y + CGFloat(racketSpeed * dt) 
        } else if direction == Direction.Down && racket!.position.y > 0 {
            racket!.position.y = racket!.position.y - CGFloat(racketSpeed * dt)
        }
    }
    
    // Check if the ball is still on screen
    // Game Over animation
    func checkGameOver() {
        if ball!.position.x > CGFloat(width) {
            gameRunning = false
            ball!.removeFromParent()
            let gameOverLabel = SKLabelNode(text: "Game Over")
            gameOverLabel.position = CGPoint(x: width / 2, y: height / 2)
            gameOverLabel.fontSize = 80
            self.addChild(gameOverLabel)
            
            // Game Over animation
            let rotateAction = SKAction.rotate(byAngle: CGFloat(M_PI), duration: 1)
            let fadeInAction = SKAction.fadeIn(withDuration: 2)
            gameOverLabel.run(SKAction.repeat(rotateAction, count: 2))
            gameOverLabel.run(SKAction.scale(to: 0, duration: 2.5), completion: {
                gameOverLabel.removeFromParent()
                self.startNewGame()
            })
        }
    }
    
    // Detect collisions between ball and racket to increase the score
    func didBegin(_ contact: SKPhysicsContact) {
        if contact.bodyA.categoryBitMask == CollisionTypes.Racket.rawValue || contact.bodyB.categoryBitMask == CollisionTypes.Racket.rawValue {
            score += 1
            scoreLabel.text = String(score)
        }
    }
}

//Initialize the playground and start the scene:
let skView = SKView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: height)))
let scene = gameScene(size: skView.frame.size)
skView.presentScene(scene)

PlaygroundPage.current.liveView =  skView

 

You can also download the sample directly from my GitHub repository.

PongPlayground

My personal conclusion: You can really write advanced Playground Apps completely on your iPad. The editor offers some intelligent solutions to choose the next needed code snippet without typing, but for bigger projects an external keypad improves the coding experience a lot. A possibility to upload the playgrounds as an App to the AppStore is missing. So a kind of Swift Playground Store would be nice for the future. Maybe this will come with the final release of iOS 10.

 

That’s all for today.

If you want to support me, please download my Apps from the Apple AppStore.

AppStore Stefan

Cheers,
Stefan