// // GameScene.swift // BrickBreaker // // Created by Sally McNichols on 10/1/14. // Copyright (c) 2014 CMU iOS Development StuCo. All rights reserved. // import SpriteKit class GameScene: SKScene, SKPhysicsContactDelegate{ var ball: SKSpriteNode var paddle: SKSpriteNode var isSetup: Bool var ballIsMoving: Bool var numberOfLives: Int var score: Int var scoreLabel: SKLabelNode var livesLabel: SKLabelNode let kScoreHeight: CGFloat = 44.0 func vecMult(a: CGPoint, b: Float) -> CGPoint { return CGPointMake(a.x * CGFloat(b), a.y * CGFloat(b)) } func vecLength(a: CGPoint) -> Float { return sqrtf(Float(a.x * a.x + a.y * a.y)) } // Makes a vector have a length of 1 func vecNormalize(a: CGPoint) -> CGPoint { let length: CGFloat = CGFloat(vecLength(a)) return CGPointMake(a.x / length, a.y / length); } func CGRectGetCenter(rect: CGRect) -> CGPoint { return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); } override init(size: CGSize) { // initialize class variables self.score = 0 self.numberOfLives = 3 self.isSetup = false self.ballIsMoving = false self.ball = SKSpriteNode(imageNamed: "ball") self.paddle = SKSpriteNode(imageNamed:"paddle") self.scoreLabel = SKLabelNode(fontNamed: "HelveticaNeue-Light") self.livesLabel = SKLabelNode(fontNamed: "HelveticaNeue-Light") super.init(size: size) self.backgroundColor = SKColor.blackColor() // set acceleration due to gravity self.physicsWorld.gravity = CGVectorMake(0, 0) // set contact delegate so that collisions will be detected self.physicsWorld.contactDelegate = self // setup the ball sprite and its physics self.ball.physicsBody = SKPhysicsBody(circleOfRadius: self.ball.size.width / 2.0) // set up bit masks that are used in collision detection // category bit mask is like an ID for the sprite self.ball.physicsBody?.categoryBitMask = ballCategory // contact test bit mask is used to indicate what the sprite will contact //self.ball.physicsBody?.contactTestBitMask = blockCategory // use precise collision detection since the ball will be moving fast // this will make sure that collisions with the ball are not missed self.physicsBody?.usesPreciseCollisionDetection = true self.ball.position = CGRectGetCenter(self.frame) self.ball.physicsBody?.friction = 0.0 // set "bounciness" of the ball self.ball.physicsBody?.restitution = 1.0 // set air resistance to zero self.ball.physicsBody?.linearDamping = 0.0 self.ball.physicsBody?.angularDamping = 0.0 self.addChild(self.ball) // setup the paddle sprite and its physics self.paddle.physicsBody = SKPhysicsBody(rectangleOfSize: self.paddle.size) // setup bit masks that are used in collision detection self.paddle.physicsBody?.categoryBitMask = paddleCateogry self.paddle.physicsBody?.contactTestBitMask = ballCategory // disable affected by gravity self.paddle.physicsBody?.affectedByGravity = false // set paddle so that collisions don't make it move self.paddle.physicsBody?.dynamic = false var centerPoint: CGPoint = CGRectGetCenter(self.frame) centerPoint.y = self.paddle.size.height / 2.0 self.paddle.position = centerPoint self.addChild(self.paddle) // create boundary sprite nodes so that the ball can bounce off of the walls let screenRect: CGRect = self.frame let wallSize: CGSize = CGSizeMake(1, CGRectGetHeight(screenRect)) let ceilingSize: CGSize = CGSizeMake(CGRectGetWidth(screenRect), 1) let leftBoundary: SKSpriteNode = SKSpriteNode(color: UIColor.blackColor(), size: wallSize) leftBoundary.physicsBody = SKPhysicsBody(rectangleOfSize: leftBoundary.size) // set the center of the left boundary to be on the left side of the screen leftBoundary.position = CGPointMake(0, CGRectGetMidY(screenRect)) // set left boundary so that collisions don't make it move leftBoundary.physicsBody?.dynamic = false let rightBoundary: SKSpriteNode = SKSpriteNode(color: UIColor.blackColor(), size: wallSize) rightBoundary.physicsBody = SKPhysicsBody(rectangleOfSize: rightBoundary.size) // set the center of the right boundary to be on the right side of the screen rightBoundary.position = CGPointMake(CGRectGetMaxX(screenRect), CGRectGetMidY(screenRect)) // set right boundary so that collisions don't make it move rightBoundary.physicsBody?.dynamic = false let ceilingBoundary: SKSpriteNode = SKSpriteNode(color: UIColor.blackColor(), size: ceilingSize) ceilingBoundary.physicsBody = SKPhysicsBody(rectangleOfSize: ceilingBoundary.size) // set the center of the of the ceiling boundary to the top of the screen ceilingBoundary.position = CGPointMake(CGRectGetMidX(screenRect), CGRectGetMaxY(screenRect) - (self.kScoreHeight / 2.0)) // set ceiling boundary so that collisions don't make it move ceilingBoundary.physicsBody?.dynamic = false // add boundary sprites to scene self.addChild(leftBoundary) self.addChild(rightBoundary) self.addChild(ceilingBoundary) } // what happens whenever the scene is presented by a view (will be the view of // the GameViewController for this game) override func didMoveToView(view: SKView) { // setup the scene for the game if(!self.isSetup) { self.setupSceneWithBlocks() self.setupScoreDisplay() self.isSetup = true } } func setupScoreDisplay() { // setup score label self.scoreLabel.name = "scoreLabel" self.scoreLabel.fontColor = SKColor.whiteColor() self.scoreLabel.text = "Score: \(0)" self.scoreLabel.fontSize = 18.0 // set position to the upper left corner self.scoreLabel.position = CGPointMake(5 + self.scoreLabel.frame.size.width / 2.0, CGRectGetMaxY(self.frame) - self.scoreLabel.frame.size.height) self.addChild(self.scoreLabel) // setup lives label self.livesLabel.name = "livesLabel" self.livesLabel.fontColor = SKColor.whiteColor() self.livesLabel.text = "Lives: \(3)" self.livesLabel.fontSize = 18.0 // align the text label so that the left side of the text is on the node's origin self.livesLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Left // set the position to the upper right corner self.livesLabel.position = CGPointMake(CGRectGetMaxX(self.frame) - self.livesLabel.frame.size.width - 5, CGRectGetMaxY(self.frame) - self.livesLabel.frame.size.height) self.addChild(self.livesLabel) } // Setup the blocks for the game func setupSceneWithBlocks() { // Start in the upper left corner just under the score label var startingX: CGFloat = 0.0 var startingY: CGFloat = kScoreHeight var colors: [SKColor] = [SKColor.redColor(), SKColor.orangeColor(), SKColor.yellowColor(), SKColor.greenColor(), SKColor.blueColor(), SKColor.purpleColor()] let blockHeight: CGFloat = 22 let blockWidth: CGFloat = self.size.width / 12.0 var col: Int = 0 // Create and add blocks to scene. This will create 4 rows and 12 columns of rows. // Outer loop: rows (loop guard might seem weird, but that's because startingY isn't 0 // Inner loop: columns while(startingY < (blockHeight * 6)) { while(startingX < self.size.width) { // create new block let block: SKSpriteNode = SKSpriteNode(texture: SKTexture(imageNamed: "block"), color: colors[col % colors.count], size: CGSizeMake(blockWidth, blockHeight)) block.colorBlendFactor = 0.7 block.position = CGPointMake(startingX + (blockWidth / 2.0), self.size.height - startingY + (blockHeight / 2.0)) // setup the physics for the block block.physicsBody = SKPhysicsBody(rectangleOfSize: block.size) // this bit mask is like the block's identifier that it is a block block.physicsBody?.categoryBitMask = blockCategory // this bit mask indicates which sprites we want to handle contact with block.physicsBody?.contactTestBitMask = ballCategory // we do not want collisions on the block, so set this bit mask to zero block.physicsBody?.affectedByGravity = false block.physicsBody?.dynamic = false // add block to scene self.addChild(block) // increment startingX += blockWidth col++ } //increment and reset counters for next row startingY += blockHeight startingX = 0 col = 0 } } // what happens when user first touches the screen override func touchesBegan(touches: Set, withEvent event: UIEvent?) { var randomXOffset: CGFloat = (CGFloat(random()) / CGFloat(RAND_MAX)) / 1.5 if(arc4random() % 2 == 0) { randomXOffset *= -1.0 } let touch: UITouch = touches.first! as UITouch let location: CGPoint = touch.locationInNode(self) // move the paddle to where user touched screen self.movePaddleToPoint(location) // if ball isn't moving that means game hasn't started // start the game by making the ball move down in random x direction if(!self.ballIsMoving) { // apply an impluse to the ball so that it moves self.ball.physicsBody?.applyImpulse(CGVectorMake(randomXOffset, -2)) self.ballIsMoving = true } } // what happens after user moves finger after initial touch override func touchesMoved(touches: Set, withEvent event: UIEvent?) { let touch: UITouch = touches.first! as UITouch let location: CGPoint = touch.locationInNode(self) self.movePaddleToPoint(location) } // makes the paddle move to a specified point func movePaddleToPoint(point: CGPoint) { let movePaddleAction: SKAction = SKAction.moveTo(CGPointMake(point.x, self.paddle.position.y), duration: 0.1) self.paddle.runAction(movePaddleAction) } // Before each frame change this is called. Checks if the ball has hit the ground // (i.e. player lost a life) and then updates and resets accordingly override func update(currentTime: CFTimeInterval) { if (self.ball.position.y < 0) { // reset the ball's velocity to starting speed and put it back in starting position self.ball.physicsBody!.velocity = CGVectorMake(0, 0) self.ball.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)) self.ballIsMoving = false var centerPoint = CGRectGetCenter(self.frame) centerPoint.y = self.paddle.size.height / 2.0 self.paddle.position = centerPoint // update number of lives value and label self.numberOfLives--; self.livesLabel.text = "Lives: \(self.numberOfLives)" } } // Handle contacts between objects // Note that in the game we use contact detection, not collision. // This is because we just want to know when 2 sprites overlap. It's not like // we want to use physics to determine what happens when sprites collide. We let the // physics world take card of that. We just need to know when the sprites overlap so // we can handle what happens in code. func didBeginContact(contact: SKPhysicsContact) { // the bodies involved in the contact var firstBody: SKPhysicsBody var secondBody: SKPhysicsBody // Set firstBody and secondBody so that firstBody will be paddle and secondBody // will be ball if those are the contacting nodes. Otherwise, when the ball hits // a block make the firstBody ball and the second body block if(contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) { firstBody = contact.bodyB secondBody = contact.bodyA } else { firstBody = contact.bodyA secondBody = contact.bodyB } // when contact is between paddle and ball if((firstBody.categoryBitMask & paddleCateogry) != 0 && (secondBody.categoryBitMask & ballCategory) != 0) { let ballVector: CGVector = self.ball.physicsBody!.velocity let normalPoint = vecNormalize(CGPointMake(ballVector.dx, ballVector.dy)) // scale vector so ball moves faster and faster over time let scaledPoint = vecMult(normalPoint, b: 0.04) self.ball.physicsBody?.applyImpulse(CGVectorMake(scaledPoint.x, scaledPoint.y)) } // when contact is between ball and block else if(((firstBody.categoryBitMask & ballCategory) != 0) && (secondBody.categoryBitMask & blockCategory) != 0) { handleContact(firstBody.node as! SKSpriteNode, block: secondBody.node as! SKSpriteNode) } } // What happens when the ball hits a block func handleContact(ball: SKSpriteNode, block: SKSpriteNode) { // remove the block that the ball hit and then increase score block.removeFromParent() self.score++ self.scoreLabel.text = "Score: \(self.score)" } // add this initializer to make Xcode happy required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }