Large diffs are not rendered by default.

Oops, something went wrong.
@@ -0,0 +1,74 @@
+//
+// AppDelegate.swift
+// TouchBarSpaceFight
+//
+// Created by Guilherme Rambo on 09/11/16.
+// Copyright © 2016 Guilherme Rambo. All rights reserved.
+//
+
+import Cocoa
+
+@NSApplicationMain
+class AppDelegate: NSObject, NSApplicationDelegate {
+
+ fileprivate var gameViewController: GameViewController!
+
+ fileprivate var screenViewController: ScreenViewController!
+
+ func applicationDidFinishLaunching(_ aNotification: Notification) {
+ screenViewController = NSApp.windows.first!.contentViewController as! ScreenViewController
+
+ NotificationCenter.default.addObserver(forName: .RestartGame, object: nil, queue: nil) { _ in
+ self.gameViewController.reset()
+ }
+ }
+
+ func applicationWillTerminate(_ aNotification: Notification) {
+ // Insert code here to tear down your application
+ }
+
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+
+ fileprivate var window: GameWindow? {
+ return NSApp.mainWindow as? GameWindow
+ }
+
+}
+
+@available(OSX 10.12.1, *)
+extension NSTouchBarItemIdentifier {
+ static let gameViewController = NSTouchBarItemIdentifier("br.com.guilhermerambo.touchasteroids")
+}
+
+@available(OSX 10.12.1, *)
+extension AppDelegate: NSTouchBarDelegate, NSTouchBarProvider {
+
+ var touchBar: NSTouchBar? {
+ let bar = NSTouchBar()
+
+ bar.delegate = self
+ bar.defaultItemIdentifiers = [.gameViewController]
+
+ return bar
+ }
+
+ func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
+ switch identifier {
+ case NSTouchBarItemIdentifier.gameViewController:
+ let item = NSCustomTouchBarItem(identifier: .gameViewController)
+
+ if gameViewController == nil {
+ gameViewController = GameViewController()
+ }
+
+ item.viewController = gameViewController
+ window?.didReceiveEvent = gameViewController.didReceive
+
+ return item
+ default: return nil
+ }
+ }
+
+}
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-8.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-7.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-6.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-5.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-4.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-3.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-2.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "TouchBarSpaceFightIcon@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "bokeh.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "spark.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "enemy.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "enemy@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "enemy@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "life.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "life@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "life@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "rocket.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "rocket@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "rocket@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "shot.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "shot@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "shot@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Large diffs are not rendered by default.

Oops, something went wrong.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,115 @@
+//
+// GameModels.swift
+// TouchBarSpaceFight
+//
+// Created by Guilherme Rambo on 09/11/16.
+// Copyright © 2016 Guilherme Rambo. All rights reserved.
+//
+
+import Cocoa
+
+struct GameModels {
+
+ struct Score {
+ var numberOfHits = 0
+ var escapedEnemies = 0
+ var destroyedEnemies = 0
+ var wastedShots = 0
+ var lives = 3
+ }
+
+ struct GameState {
+ var score = Score()
+
+ var enemySpawnInterval: CGFloat = 1.0
+ var maxEnemySpeed: CGFloat = 2
+
+ var lastShotTime: TimeInterval = 0
+ var maxShotSpeed: CGFloat = 2
+ var maxShotRate: TimeInterval = 0.15
+ var maxShotsPerEnemy = 2
+
+ var maxDifficultyIncreaseCount = 10
+ var difficultyIncreaseCount = 0
+ var difficultyIncreaseRate: TimeInterval = 15.0
+ var enemySpeedIncreaseRate: CGFloat = 0.15
+ var enemySpawnIntervalIncreaseRate: CGFloat = 0.15
+
+ var playerSpeed = CGVector(dx: 3, dy: 1.5)
+
+ var isHit = false
+ var hitTime = TimeInterval(0)
+ var hitDuration = TimeInterval(3.0)
+
+ var lifeSpawnRate = TimeInterval(15)
+ var maxLifeSpeed: CGFloat = 4
+ }
+
+ struct PhysicsCategory {
+ static let none: UInt32 = 0
+ static let all: UInt32 = UInt32.max
+ static let enemy: UInt32 = 0b1
+ static let shot: UInt32 = 0b10
+ static let player: UInt32 = 0b100
+ static let life: UInt32 = 0b1000
+ }
+
+ enum Scene: String {
+ case menu = "Menu"
+ case main = "Game"
+ case gameOver = "GameOver"
+ }
+
+ enum Sprite: String {
+ case player
+ case enemy
+ case shot
+ case life
+ }
+
+ enum Key: Int, CustomDebugStringConvertible {
+ case arrowUp = 126
+ case arrowDown = 125
+ case arrowLeft = 123
+ case arrowRight = 124
+ case space = 49
+
+ var debugDescription: String {
+ switch self {
+ case .arrowUp: return "UP ARROW"
+ case .arrowDown: return "DOWN ARROW"
+ case .arrowLeft: return "LEFT ARROW"
+ case .arrowRight: return "RIGHT ARROW"
+ case .space: return "SPACE"
+ }
+ }
+ }
+
+ enum KeyEvent: CustomDebugStringConvertible {
+ case down(Key)
+ case up(Key)
+
+ var debugDescription: String {
+ switch self {
+ case .down(let key):
+ return "\(key) - PRESSED"
+ case .up(let key):
+ return "\(key) - RELEASED"
+ }
+ }
+ }
+
+ enum Sound: String {
+ case enemykilled
+ case gameover
+ case life
+ case music
+ case playerhit
+ case shot
+ }
+
+}
+
+protocol EventHandler: class {
+ func key(event: GameModels.KeyEvent)
+}
@@ -0,0 +1,372 @@
+//
+// GameScene.swift
+// TouchBarSpaceFight
+//
+// Created by Guilherme Rambo on 09/11/16.
+// Copyright © 2016 Guilherme Rambo. All rights reserved.
+//
+
+import Cocoa
+import SpriteKit
+
+extension Notification.Name {
+ static let GameOver = Notification.Name(rawValue: "TouchBarSpaceFightGameOver")
+}
+
+protocol GameSceneDelegate: class {
+ var state: GameModels.GameState { get set }
+}
+
+extension SKScene {
+
+ func childNode<N: SKNode>(for sprite: GameModels.Sprite) -> N {
+ return self.childNode(withName: sprite.rawValue) as! N
+ }
+
+}
+
+extension CGFloat {
+
+ static func random() -> CGFloat {
+ return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
+ }
+
+ static func random(min: CGFloat, max: CGFloat) -> CGFloat {
+ return random() * (max - min) + min
+ }
+
+}
+
+extension GameModels.Sound {
+
+ var instance: SKAudioNode {
+ return SKAudioNode(fileNamed: rawValue)
+ }
+
+}
+
+class GameScene: SKScene, EventHandler, SKPhysicsContactDelegate {
+
+ weak var gameDelegate: GameSceneDelegate!
+
+ private var playerDirection: CGVector = .zero
+
+ private var isShooting = false
+
+ private lazy var player: SKSpriteNode = {
+ let p: SKSpriteNode = self.childNode(for: .player)
+
+ p.physicsBody = SKPhysicsBody(rectangleOf: p.size)
+ p.physicsBody?.isDynamic = true
+ p.physicsBody?.categoryBitMask = GameModels.PhysicsCategory.player
+ p.physicsBody?.contactTestBitMask = GameModels.PhysicsCategory.enemy
+ p.physicsBody?.collisionBitMask = GameModels.PhysicsCategory.none
+
+ return p
+ }()
+
+ private lazy var enemyPrototype: SKSpriteNode = {
+ return self.childNode(for: .enemy)
+ }()
+
+ private lazy var lifePrototype: SKSpriteNode = {
+ return self.childNode(for: .life)
+ }()
+
+ private lazy var shotPrototype: SKSpriteNode = {
+ return self.childNode(for: .shot)
+ }()
+
+ private var numberOfActiveEnemies: Int {
+ return children.filter({ $0.name?.contains("enemy") ?? false }).count
+ }
+
+ private var numberOfActiveShots: Int {
+ return children.filter({ $0.name?.contains("shot") ?? false }).count
+ }
+
+ private var statisticsTimer: Timer!
+
+ override func didMove(to view: SKView) {
+ super.didMove(to: view)
+
+ physicsWorld.gravity = .zero
+ physicsWorld.contactDelegate = self
+
+ run(SKAction.repeatForever(
+ SKAction.sequence([
+ SKAction.run(spawnEnemy),
+ SKAction.wait(forDuration: 1.0)
+ ])
+ ))
+
+ statisticsTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateGameStatistics(_:)), userInfo: nil, repeats: true)
+
+ let musicNode = GameModels.Sound.music.instance
+ musicNode.autoplayLooped = true
+ addChild(musicNode)
+ }
+
+ private var gameStartTime: TimeInterval!
+ private var gameTime: TimeInterval = 0
+
+ override func update(_ currentTime: TimeInterval) {
+ super.update(currentTime)
+
+ if gameStartTime == nil {
+ gameStartTime = currentTime
+ }
+
+ gameTime = currentTime - gameStartTime
+
+ // move player according to keyboard commands
+ if playerDirection.dy < 0 && player.position.y > 0 {
+ player.position.y -= gameDelegate.state.playerSpeed.dy
+ } else if playerDirection.dy > 0 && player.position.y < size.height - player.size.height {
+ player.position.y += gameDelegate.state.playerSpeed.dy
+ }
+
+ if playerDirection.dx < 0 && player.position.x > 10 {
+ player.position.x -= gameDelegate.state.playerSpeed.dx
+ } else if playerDirection.dx > 0 && player.position.x < size.width - 10 {
+ player.position.x += gameDelegate.state.playerSpeed.dx
+ }
+
+ // keep track of player's hit status
+ if gameDelegate.state.isHit {
+ player.alpha = 0.5
+
+ if gameTime - gameDelegate.state.hitTime >= gameDelegate.state.hitDuration {
+ gameDelegate.state.isHit = false
+ gameDelegate.state.hitTime = 0
+ // reset player hit state
+ player.alpha = 1
+ }
+ }
+
+ // shoot if needed
+ if isShooting {
+ if numberOfActiveShots < numberOfActiveEnemies * gameDelegate.state.maxShotsPerEnemy {
+ spawnShot()
+ }
+ }
+ }
+
+ private var statisticsTime: TimeInterval = 0.0
+
+ @objc private func updateGameStatistics(_ sender: Any?) {
+ statisticsTime += 1.0
+
+ // update difficulty level if needed
+ if abs(statisticsTime.truncatingRemainder(dividingBy: gameDelegate.state.difficultyIncreaseRate)) == 0 {
+ // time to increase difficulty
+ if gameDelegate.state.difficultyIncreaseCount < gameDelegate.state.maxDifficultyIncreaseCount {
+ gameDelegate.state.enemySpawnInterval += gameDelegate.state.enemySpawnIntervalIncreaseRate
+ gameDelegate.state.maxEnemySpeed += gameDelegate.state.enemySpeedIncreaseRate
+
+ gameDelegate.state.difficultyIncreaseCount += 1
+ }
+ }
+
+ // maybe spawn a new life for the player
+ if abs(statisticsTime.truncatingRemainder(dividingBy: gameDelegate.state.lifeSpawnRate)) == 0 {
+ if arc4random_uniform(1000) % 3 == 0 {
+ spawnLife()
+ }
+ }
+ }
+
+ func key(event: GameModels.KeyEvent) {
+ switch event {
+ case .down(let key):
+ switch key {
+ case .arrowDown:
+ playerDirection.dy = -1
+ case .arrowUp:
+ playerDirection.dy = 1
+ case .arrowLeft:
+ playerDirection.dx = -1
+ case .arrowRight:
+ playerDirection.dx = 1
+ case .space:
+ isShooting = true
+ }
+ case .up(let key):
+ switch key {
+ case .arrowDown, .arrowUp:
+ playerDirection.dy = 0
+ case .arrowLeft, .arrowRight:
+ playerDirection.dx = 0
+ case .space:
+ isShooting = false
+ }
+ }
+ }
+
+ private func newEnemy() -> SKSpriteNode {
+ let enemy = enemyPrototype.copy() as! SKSpriteNode
+ enemy.name = "enemy-" + UUID().uuidString
+
+ enemy.physicsBody = SKPhysicsBody(rectangleOf: enemy.size)
+ enemy.physicsBody?.isDynamic = true
+ enemy.physicsBody?.categoryBitMask = GameModels.PhysicsCategory.enemy
+ enemy.physicsBody?.contactTestBitMask = GameModels.PhysicsCategory.shot
+ enemy.physicsBody?.collisionBitMask = GameModels.PhysicsCategory.none
+
+ return enemy
+ }
+
+ private func spawnEnemy() {
+ let enemy = newEnemy()
+
+ let y = CGFloat.random(min: 0, max: size.height - enemy.size.height)
+ enemy.position = CGPoint(x: size.width, y: y)
+
+ addChild(enemy)
+
+ let duration = CGFloat.random(min: 1.0, max: gameDelegate.state.maxEnemySpeed)
+
+ let moveAction = SKAction.move(to: CGPoint(x: -enemy.size.width, y: y), duration: TimeInterval(duration))
+ let moveActionCompute = SKAction.run { [weak self] in
+ self?.gameDelegate.state.score.escapedEnemies += 1
+ }
+ let moveActionCompletion = SKAction.removeFromParent()
+ enemy.run(SKAction.sequence([moveAction, moveActionCompute, moveActionCompletion]))
+ }
+
+ private func newShot() -> SKSpriteNode {
+ let shot = shotPrototype.copy() as! SKSpriteNode
+ shot.name = "shot-" + UUID().uuidString
+
+ shot.physicsBody = SKPhysicsBody(rectangleOf: shot.size)
+ shot.physicsBody?.isDynamic = true
+ shot.physicsBody?.categoryBitMask = GameModels.PhysicsCategory.shot
+ shot.physicsBody?.contactTestBitMask = GameModels.PhysicsCategory.enemy
+ shot.physicsBody?.collisionBitMask = GameModels.PhysicsCategory.none
+
+ return shot
+ }
+
+ private func spawnShot() {
+ if gameDelegate.state.lastShotTime != 0 {
+ if gameTime - gameDelegate.state.lastShotTime < gameDelegate.state.maxShotRate {
+ return
+ } else {
+ gameDelegate.state.lastShotTime = 0
+ }
+ }
+
+ let shot = newShot()
+ shot.position = CGPoint(x: player.position.x + player.size.width, y: player.position.y + player.size.height / 2)
+
+ addChild(shot)
+
+ let duration = CGFloat.random(min: 1.0, max: gameDelegate.state.maxShotSpeed)
+
+ let moveAction = SKAction.move(to: CGPoint(x: size.width, y: shot.position.y), duration: TimeInterval(duration))
+ let moveActionCompute = SKAction.run { [weak self] in
+ self?.gameDelegate.state.score.wastedShots += 1
+ }
+ let moveActionCompletion = SKAction.removeFromParent()
+ shot.run(SKAction.sequence([moveAction, moveActionCompute, moveActionCompletion]))
+
+ gameDelegate.state.lastShotTime = gameTime
+
+ run(SKAction.playSoundFileNamed(GameModels.Sound.shot.rawValue, waitForCompletion: false))
+ }
+
+ private func newLife() -> SKSpriteNode {
+ let life = lifePrototype.copy() as! SKSpriteNode
+ life.name = "life-" + UUID().uuidString
+
+ life.physicsBody = SKPhysicsBody(rectangleOf: life.size)
+ life.physicsBody?.isDynamic = true
+ life.physicsBody?.categoryBitMask = GameModels.PhysicsCategory.life
+ life.physicsBody?.contactTestBitMask = GameModels.PhysicsCategory.player
+ life.physicsBody?.collisionBitMask = GameModels.PhysicsCategory.none
+
+ return life
+ }
+
+ private func spawnLife() {
+ let life = newLife()
+
+ let y = CGFloat.random(min: 0, max: size.height - life.size.height)
+ life.position = CGPoint(x: size.width, y: y)
+
+ addChild(life)
+
+ let duration = CGFloat.random(min: 1.0, max: gameDelegate.state.maxLifeSpeed)
+
+ let moveAction = SKAction.move(to: CGPoint(x: -life.size.width, y: y), duration: TimeInterval(duration))
+ let moveActionCompletion = SKAction.removeFromParent()
+ life.run(SKAction.sequence([moveAction, moveActionCompletion]))
+ }
+
+ // MARK: - Collisions
+
+ func didBegin(_ contact: SKPhysicsContact) {
+ var firstBody: SKPhysicsBody
+ var secondBody: SKPhysicsBody
+
+ if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
+ firstBody = contact.bodyA
+ secondBody = contact.bodyB
+ } else {
+ firstBody = contact.bodyB
+ secondBody = contact.bodyA
+ }
+
+ // enemy hit by player shot
+ if (secondBody.categoryBitMask & GameModels.PhysicsCategory.shot) != 0
+ && (firstBody.categoryBitMask & GameModels.PhysicsCategory.enemy) != 0 {
+
+ if let enemy = firstBody.node as? SKSpriteNode, let explosion = SKScene(fileNamed: "Explosion") {
+ explosion.position = enemy.position
+ explosion.size = enemy.size
+
+ addChild(explosion)
+
+ let wait = SKAction.wait(forDuration: 0.3)
+ let remove = SKAction.removeFromParent()
+ explosion.run(SKAction.sequence([wait, remove]))
+ }
+
+ firstBody.node?.removeFromParent()
+ secondBody.node?.removeFromParent()
+ gameDelegate.state.score.destroyedEnemies += 1
+
+ run(SKAction.playSoundFileNamed(GameModels.Sound.enemykilled.rawValue, waitForCompletion: false))
+ }
+
+ // player hit by enemy
+ if !gameDelegate.state.isHit {
+ if (firstBody.categoryBitMask & GameModels.PhysicsCategory.enemy) != 0
+ && (secondBody.categoryBitMask & GameModels.PhysicsCategory.player) != 0 {
+ gameDelegate.state.isHit = true
+ gameDelegate.state.hitTime = gameTime
+ gameDelegate.state.score.lives -= 1
+ gameDelegate.state.score.numberOfHits += 1
+ run(SKAction.playSoundFileNamed(GameModels.Sound.playerhit.rawValue, waitForCompletion: false))
+
+ if gameDelegate.state.score.lives <= 0 {
+ let overAction = SKAction.run {
+ NotificationCenter.default.post(name: .GameOver, object: nil)
+ }
+ let overSoundAction = SKAction.playSoundFileNamed(GameModels.Sound.gameover.rawValue, waitForCompletion: false)
+
+ run(SKAction.sequence([overSoundAction, overAction]))
+ }
+ }
+ }
+
+ // player hit by life
+ if (secondBody.categoryBitMask & GameModels.PhysicsCategory.life) != 0
+ && (firstBody.categoryBitMask & GameModels.PhysicsCategory.player) != 0 {
+ gameDelegate.state.score.lives += 1
+ secondBody.node?.removeFromParent()
+ run(SKAction.playSoundFileNamed(GameModels.Sound.life.rawValue, waitForCompletion: false))
+ }
+ }
+
+}
@@ -0,0 +1,114 @@
+//
+// GameViewController.swift
+// TouchBarSpaceFight
+//
+// Created by Guilherme Rambo on 09/11/16.
+// Copyright © 2016 Guilherme Rambo. All rights reserved.
+//
+
+import Cocoa
+import SpriteKit
+
+extension Notification.Name {
+ static let GameStateDidChange = Notification.Name(rawValue: "TouchBarSpaceFightStateDidChange")
+}
+
+extension GameModels.Scene {
+
+ var instance: SKScene? {
+ switch self {
+ case .main: return GameScene(fileNamed: rawValue)
+ default: return nil
+ }
+ }
+
+}
+
+class GameViewController: NSViewController, GameSceneDelegate {
+
+ var state: GameModels.GameState = GameModels.GameState() {
+ didSet {
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: .GameStateDidChange, object: self.state)
+ }
+ }
+ }
+
+ private lazy var gameView: SKView = {
+ let v = SKView(frame: self.view.bounds)
+ v.autoresizingMask = [.viewWidthSizable, .viewHeightSizable]
+
+ return v
+ }()
+
+ init() {
+ super.init(nibName: nil, bundle: nil)!
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ view = NSView()
+ view.addSubview(gameView)
+ }
+
+ private var wasPausedWhenAppResignedActive = false
+
+ override func viewDidLayout() {
+ super.viewDidLayout()
+
+ gameView.frame = view.bounds
+
+ NotificationCenter.default.addObserver(forName: .GameOver, object: nil, queue: nil) { [weak self] _ in
+ self?.gameView.isPaused = true
+ }
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ NotificationCenter.default.addObserver(forName: .DidPauseGame, object: nil, queue: nil) { [weak self] _ in
+ self?.gameView.isPaused = true
+ }
+ NotificationCenter.default.addObserver(forName: .DidContinueGame, object: nil, queue: nil) { [weak self] _ in
+ self?.gameView.isPaused = false
+ }
+ }
+
+ override func viewDidAppear() {
+ super.viewDidAppear()
+
+ if gameView.scene == nil {
+ showGameScene()
+ }
+ }
+
+ func showGameScene() {
+ let scene = GameModels.Scene.main.instance as! GameScene
+ scene.gameDelegate = self
+ gameView.presentScene(scene)
+ }
+
+ func didReceive(event: NSEvent) {
+ guard let eventHandler = gameView.scene as? EventHandler else { return }
+
+ guard let key = GameModels.Key(rawValue: Int(event.keyCode)) else { return }
+
+ switch event.type {
+ case .keyUp:
+ eventHandler.key(event: .up(key))
+ case .keyDown:
+ eventHandler.key(event: .down(key))
+ default: break
+ }
+ }
+
+ func reset() {
+ gameView.presentScene(nil)
+ state = GameModels.GameState()
+ showGameScene()
+ }
+
+}
@@ -0,0 +1,23 @@
+//
+// GameWindow.swift
+// TouchBarSpaceFight
+//
+// Created by Guilherme Rambo on 09/11/16.
+// Copyright © 2016 Guilherme Rambo. All rights reserved.
+//
+
+import Cocoa
+
+class GameWindow: NSWindow {
+
+ var didReceiveEvent: ((NSEvent) -> Void)?
+
+ override func keyDown(with event: NSEvent) {
+ didReceiveEvent?(event)
+ }
+
+ override func keyUp(with event: NSEvent) {
+ didReceiveEvent?(event)
+ }
+
+}
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2016 Guilherme Rambo. All rights reserved.</string>
+ <key>NSMainStoryboardFile</key>
+ <string>Main</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
@@ -0,0 +1,81 @@
+//
+// ScreenViewController.swift
+// TouchBarSpaceFight
+//
+// Created by Guilherme Rambo on 09/11/16.
+// Copyright © 2016 Guilherme Rambo. All rights reserved.
+//
+
+import Cocoa
+
+extension Notification.Name {
+ static let DidPauseGame = Notification.Name(rawValue: "TouchBarSpaceFightDidPause")
+ static let DidContinueGame = Notification.Name(rawValue: "TouchBarSpaceFightDidContinue")
+ static let RestartGame = Notification.Name(rawValue: "TouchBarSpaceFightRestartGame")
+}
+
+class ScreenViewController: NSViewController {
+
+ var isGameOver = false {
+ didSet {
+ if isGameOver != oldValue {
+ if isGameOver {
+ pauseButton.title = "Game Over (Click to Try Again)"
+ } else {
+ pauseButton.title = "Pause"
+ }
+ }
+ }
+ }
+
+ var isPaused = false {
+ didSet {
+ if isPaused != oldValue {
+ if isPaused {
+ pauseButton.title = "Continue"
+ NotificationCenter.default.post(name: .DidPauseGame, object: nil)
+ } else {
+ pauseButton.title = "Pause"
+ NotificationCenter.default.post(name: .DidContinueGame, object: nil)
+ }
+ }
+ }
+ }
+
+ @IBOutlet weak var livesLabel: NSTextField!
+ @IBOutlet weak var hitsLabel: NSTextField!
+ @IBOutlet weak var destroyedLabel: NSTextField!
+ @IBOutlet weak var lostLabel: NSTextField!
+ @IBOutlet weak var shotsWastedLabel: NSTextField!
+ @IBOutlet weak var pauseButton: NSButton!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ NotificationCenter.default.addObserver(forName: .GameStateDidChange, object: nil, queue: nil) { [weak self] note in
+ guard let state = note.object as? GameModels.GameState else { return }
+ self?.stateDidChange(to: state)
+ }
+ NotificationCenter.default.addObserver(forName: .GameOver, object: nil, queue: nil) { [weak self] _ in
+ self?.isGameOver = true
+ }
+ }
+
+ func stateDidChange(to state: GameModels.GameState) {
+ livesLabel.stringValue = "\(state.score.lives)"
+ hitsLabel.stringValue = "\(state.score.numberOfHits)"
+ destroyedLabel.stringValue = "\(state.score.destroyedEnemies)"
+ lostLabel.stringValue = "\(state.score.escapedEnemies)"
+ shotsWastedLabel.stringValue = "\(state.score.wastedShots)"
+ }
+
+ @IBAction func pause(_ sender: Any) {
+ if isGameOver {
+ NotificationCenter.default.post(name: .RestartGame, object: nil)
+ isGameOver = false
+ } else {
+ isPaused = !isPaused
+ }
+ }
+
+}
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.app-sandbox</key>
+ <true/>
+</dict>
+</plist>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Diff not rendered.
Diff not rendered.