| @@ -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" | ||
| + } | ||
| +} |
| @@ -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" | ||
| + } | ||
| +} |
| @@ -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" | ||
| + } | ||
| +} |
| @@ -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" | ||
| + } | ||
| +} |
| @@ -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" | ||
| + } | ||
| +} |
| @@ -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" | ||
| + } | ||
| +} |
| @@ -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" | ||
| + } | ||
| +} |
| @@ -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> |