Common/ViewController.swift
| /* | 
| Copyright (C) 2016 Apple Inc. All Rights Reserved. | 
| See LICENSE.txt for this sample’s licensing information | 
| Abstract: | 
| The main ViewController used to host the scene and | 
| configure gameplay. | 
| */ | 
| import GameKit | 
| import AVFoundation | 
| #if os(iOS) || os(tvOS) | 
| typealias BaseViewController = UIViewController | 
| #elseif os(OSX) | 
| typealias BaseViewController = NSViewController | 
| #endif | 
| class ViewController: BaseViewController, SCNSceneRendererDelegate { | 
| // MARK: Types | 
|     struct Assets { | 
| static let basePath = "badger.scnassets/" | 
| private static let soundsPath = basePath + "sounds/" | 
|         static func sound(named name: String) -> SCNAudioSource { | 
|             guard let source = SCNAudioSource(named: soundsPath + name) else { | 
|                 fatalError("Failed to load audio source \(name).") | 
| } | 
| return source | 
| } | 
|         static func animation(named name: String) -> CAAnimation { | 
| return CAAnimation.animation(withSceneName: basePath + name) | 
| } | 
|         static func scene(named name: String) -> SCNScene { | 
|             guard let scene = SCNScene(named: basePath + name) else { | 
|                 fatalError("Failed to load scene \(name).") | 
| } | 
| return scene | 
| } | 
| } | 
|     struct Trigger { | 
| let position: float3 | 
| let action: (ViewController) -> () | 
| } | 
|     private enum CollectableState: UInt { | 
| case notCollected = 0 | 
| case beingCollected = 2 | 
| } | 
|     private enum GameState: UInt { | 
| case notStarted = 0 | 
| case started = 1 | 
| } | 
| // MARK: Configuration Properties | 
| /// Determines if the level uses local sun. | 
| let isUsingLocalSun = true | 
| /// Determines if audio should be enabled. | 
| let isSoundEnabled = true | 
| let speedFactor: Float = 1.5 | 
| // MARK: Scene Properties | 
| @IBOutlet var sceneView: View! | 
| let scene = Assets.scene(named: "scene.scn") | 
| // MARK: Animation Properties | 
| let character: SCNNode | 
| let idleAnimationOwner: SCNNode | 
| let cartAnimationName: String | 
| /** | 
| These animations will be played when the user performs an action | 
| and will temporarily disable the "idle" animation. | 
| */ | 
| let jumpAnimation = Assets.animation(named: "animation-jump.scn") | 
| let squatAnimation = Assets.animation(named: "animation-squat.scn") | 
| let leanLeftAnimation = Assets.animation(named: "animation-lean-left.scn") | 
| let leanRightAnimation = Assets.animation(named: "animation-lean-right.scn") | 
| let slapAnimation = Assets.animation(named: "animation-slap.scn") | 
| let leftHand: SCNNode | 
| let rightHand: SCNNode | 
| var sunTargetRelativeToCamera: SCNVector3 | 
| var sunDirection: SCNVector3 | 
| var sun: SCNNode | 
| // Sparkles effect | 
| var sparkles: SCNParticleSystem | 
| var stars: SCNParticleSystem | 
| var leftWheelEmitter: SCNNode | 
| var rightWheelEmitter: SCNNode | 
| var headEmitter: SCNNode | 
| var wheels: SCNNode | 
| // Collect particles | 
| var collectParticleSystem: SCNParticleSystem | 
| var collectBigParticleSystem: SCNParticleSystem | 
| // State | 
| var squatCounter = 0 | 
| var isOverWood = false | 
| // MARK: Sound Properties | 
| var railSoundSpeed: UInt = 0 | 
| let hitSound = Assets.sound(named: "hit.mp3") | 
| let railHighSpeedSound = Assets.sound(named: "rail_highspeed_loop.mp3") | 
| let railMediumSpeedSound = Assets.sound(named: "rail_normalspeed_loop.mp3") | 
| let railLowSpeedSound = Assets.sound(named: "rail_slowspeed_loop.mp3") | 
| let railWoodSound = Assets.sound(named: "rail_wood_loop.mp3") | 
| let railSqueakSound = Assets.sound(named: "cart_turn_squeak.mp3") | 
| let cartHide = Assets.sound(named: "cart_hide.mp3") | 
| let cartJump = Assets.sound(named: "cart_jump.mp3") | 
| let cartTurnLeft = Assets.sound(named: "cart_turn_left.mp3") | 
| let cartTurnRight = Assets.sound(named: "cart_turn_right.mp3") | 
| let cartBoost = Assets.sound(named: "cart_boost.mp3") | 
| // MARK: Collectable Properties | 
| let collectables: SCNNode | 
| let speedItems: SCNNode | 
| let collectSound = Assets.sound(named: "collect1.mp3") | 
| let collectSound2 = Assets.sound(named: "collect2.mp3") | 
| // MARK: Triggers | 
| /// Triggers are configured in `configureScene()`. | 
| var triggers = [Trigger]() | 
| var activeTriggerIndex = -1 | 
| // MARK: Game controls | 
| var controllerDPad: GCControllerDirectionPad? | 
| /// Game state | 
| private var gameState: GameState = .notStarted | 
| // MARK: View Controller Initialization | 
| #if os(iOS) || os(tvOS) | 
|     override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { | 
|         fatalError("init(coder:) has not been implemented") | 
| } | 
| #elseif os(OSX) | 
|     override init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { | 
|         fatalError("init(coder:) has not been implemented") | 
| } | 
| #endif | 
|     required init?(coder: NSCoder) { | 
| // Retrieve the character and its animations. | 
| // The character node "Bob_root" initially is a placeholder. | 
| // We will load the models from one of the animation scenes and add them to the empty node. | 
| character = scene.rootNode.childNode(withName: "Bob_root", recursively: true)! | 
| let idleScene = Assets.scene(named: "animation-idle.scn") | 
| let characterHierarchy = idleScene.rootNode.childNode(withName: "Bob_root", recursively: true)! | 
|         for node in characterHierarchy.childNodes { | 
| character.addChildNode(node) | 
| } | 
| idleAnimationOwner = character.childNode(withName: "Dummy_kart_root", recursively: true)! | 
| // The animation for the cart is always running. The name of the animation is retrieved | 
| // so that we can change its speed as the cart accelerates or decelerates. | 
| cartAnimationName = scene.rootNode.animationKeys.first! | 
| // Play character idle animation. | 
| let idleAnimation = Assets.animation(named: "animation-start-idle.scn") | 
| idleAnimation.repeatCount = Float.infinity | 
| character.addAnimation(idleAnimation, forKey: "start") | 
| // Load sparkles. | 
| let sparkleScene = Assets.scene(named: "sparkles.scn") | 
| let sparkleNode = sparkleScene.rootNode.childNode(withName: "sparkles", recursively: true)! | 
| sparkles = sparkleNode.particleSystems![0] | 
| sparkles.loops = false | 
| let starsNode = sparkleScene.rootNode.childNode(withName: "slap", recursively: true)! | 
| stars = starsNode.particleSystems![0] | 
| stars.loops = false | 
| // Collect particles. | 
| collectParticleSystem = SCNParticleSystem(named: "collect.scnp", inDirectory: "badger.scnassets")! | 
| collectParticleSystem.loops = false | 
| collectBigParticleSystem = SCNParticleSystem(named: "collect-big.scnp", inDirectory: "badger.scnassets")! | 
| collectBigParticleSystem.loops = false | 
| leftHand = character.childNode(withName: "Bip001_L_Finger0Nub", recursively: true)! | 
| rightHand = character.childNode(withName: "Bip001_R_Finger0Nub", recursively: true)! | 
| leftWheelEmitter = character.childNode(withName: "Dummy_rightWheel_sparks", recursively: true)! | 
| rightWheelEmitter = character.childNode(withName: "Dummy_leftWheel_sparks", recursively: true)! | 
| wheels = character.childNode(withName: "wheels_front", recursively: true)! | 
| headEmitter = SCNNode() | 
| headEmitter.position = SCNVector3Make(0, 1, 0) | 
| character.addChildNode(headEmitter) | 
| let wheelAnimation = CABasicAnimation(keyPath: "eulerAngles.x") | 
| wheelAnimation.byValue = 10.0 | 
| wheelAnimation.duration = 1.0 | 
| wheelAnimation.repeatCount = Float.infinity | 
| wheelAnimation.isCumulative = true | 
| wheels.addAnimation(wheelAnimation, forKey: "wheel"); | 
| // Make sure the slap animation plays right away (no fading) | 
| slapAnimation.fadeInDuration = 0.0 | 
| /// Similarly collectables are grouped under a common parent node. | 
| /// In addition, load a sound file that will be played when the user collects an item. | 
| collectables = scene.rootNode.childNode(withName: "Collectables", recursively: false)! | 
| speedItems = scene.rootNode.childNode(withName: "SpeedItems", recursively: false)! | 
| // Load sounds. | 
| collectSound.volume = 5.0 | 
| collectSound2.volume = 5.0 | 
| // Configure sounds. | 
| let sounds = [ | 
| railSqueakSound, collectSound, collectSound2, | 
| hitSound, railHighSpeedSound, railMediumSpeedSound, | 
| railLowSpeedSound, railWoodSound, railSqueakSound, | 
| cartHide, cartJump, cartTurnLeft, | 
| cartTurnRight | 
| ] | 
|         for sound in sounds { | 
| sound.isPositional = false | 
| sound.load() | 
| } | 
| railSqueakSound.loops = true | 
| // Configure the scene to use a local sun. | 
|         if isUsingLocalSun { | 
| sun = scene.rootNode.childNode(withName: "Direct001", recursively: false)! | 
| sun.light?.shadowMapSize = CGSize(width: 2048, height: 2048) | 
| sun.light?.orthographicScale = 10 | 
| sunTargetRelativeToCamera = SCNVector3(x:0, y:0, z:-10) | 
| sun.position = SCNVector3Zero | 
| sunDirection = sun.convertPosition(SCNVector3(x:0, y:0, z:-1), to: nil) | 
| } | 
|         else { | 
| sun = SCNNode() | 
| sunTargetRelativeToCamera = SCNVector3Zero | 
| sunDirection = SCNVector3Zero | 
| } | 
| super.init(coder: coder) | 
| } | 
|     func configureScene() { | 
| // Add sparkles. | 
|         let leftEvent1 = SCNAnimationEvent(keyTime: 0.15) { [unowned self] _ in | 
| self.leftWheelEmitter.addParticleSystem(self.sparkles) | 
| } | 
|         let leftEvent2 = SCNAnimationEvent(keyTime: 0.9) { [unowned self] _ in | 
| self.rightWheelEmitter.addParticleSystem(self.sparkles) | 
| } | 
|         let rightEvent1 = SCNAnimationEvent(keyTime: 0.9) { [unowned self] _ in | 
| self.leftWheelEmitter.addParticleSystem(self.sparkles) | 
| } | 
| leanLeftAnimation.animationEvents = [leftEvent1, leftEvent2] | 
| leanRightAnimation.animationEvents = [rightEvent1] | 
| sceneView.antialiasingMode = .none | 
| // Configure triggers and collectables | 
|         /// Special nodes ("triggers") are placed in the scene under a common parent node. | 
| /// Their names indicate what event should occur as they are hit by the cart. | 
| let triggerGroup = scene.rootNode.childNode(withName: "triggers", recursively: false)! | 
|         triggers = triggerGroup.childNodes.flatMap { node in | 
| let triggerName = node.name! as NSString | 
| let triggerPosition = float3(node.position) | 
|             if triggerName.hasPrefix("Trigger_speed") { | 
| let speedValueOffset = "Trigger_speedX_".characters.count | 
| var speedValue = triggerName.substring(from: speedValueOffset) | 
| speedValue = speedValue.replacingOccurrences(of: "_", with: ".") | 
|                 guard let speed = Float(speedValue) else { | 
|                     print("Failed to parse speed value \(speedValue).") | 
| return nil | 
| } | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.trigger(characterSpeed: speed) | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_obstacle") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.triggerCollision() | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_reverb") && triggerName.hasSuffix("start") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.startReverb() | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_reverb") && triggerName.hasSuffix("stop") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.stopReverb() | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_turn_start") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.startTurn() | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_turn_stop") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.stopTurn() | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_wood_start") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.startWood() | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_wood_stop") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.stopWood() | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_highSpeed") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.changeSpeedSound(speed: 3) | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_normalSpeed") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.changeSpeedSound(speed: 2) | 
| }) | 
| } | 
|             if triggerName.hasPrefix("Trigger_slowSpeed") { | 
|                 return Trigger(position: triggerPosition, action: { controller in | 
| controller.changeSpeedSound(speed: 1) | 
| }) | 
| } | 
| return nil | 
| } | 
| } | 
| // MARK: UIViewController | 
|     override func viewDidLoad() { | 
| super.viewDidLoad() | 
| // Configure scene post init. | 
| configureScene() | 
| /// Set the scene and make sure all shaders and textures are pre-loaded. | 
| sceneView.scene = scene | 
| // At every round regenerate collectables. | 
| let cartAnimation = scene.rootNode.animation(forKey: cartAnimationName)! | 
|         cartAnimation.animationEvents = [SCNAnimationEvent(keyTime: 0.9, block: { [unowned self] _ in | 
| self.respawnCollectables() | 
| })] | 
| scene.rootNode.addAnimation(cartAnimation, forKey: cartAnimationName) | 
| sceneView.prepare(scene, shouldAbortBlock: nil) | 
| sceneView.delegate = self | 
| sceneView.pointOfView = sceneView.scene?.rootNode.childNode(withName: "camera_depart", recursively: true) | 
| // Play wind sound at launch. | 
| let sound = Assets.sound(named: "wind.m4a") | 
| sound.loops = true | 
| sound.isPositional = false | 
| sound.shouldStream = true | 
| sound.volume = 8.0 | 
| sceneView.scene?.rootNode.addAudioPlayer(SCNAudioPlayer(source: sound)) | 
| #if os(iOS) | 
| sceneView.contentScaleFactor = 1.3 | 
| #elseif os(tvOS) | 
| sceneView.contentScaleFactor = 1.0 | 
| #else | 
| sceneView.layer?.contentsScale = 1.0 | 
| #endif | 
| // Start at speed 0. | 
| characterSpeed = 0.0 | 
| setupGameControllers() | 
| } | 
| // MARK: Render loop | 
| /// At each frame, verify if an event should occur | 
|     func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { | 
| activateTriggers() | 
| collectItems() | 
| // Update sun position | 
|         if isUsingLocalSun { | 
| let target = (renderer.pointOfView?.presentation.convertPosition(sunTargetRelativeToCamera, to: nil))! | 
| sun.position = SCNVector3(float3(target) - float3(sunDirection) * 10.0) | 
| } | 
| } | 
| // MARK: Sound effects | 
|     func startReverb() { | 
| } | 
|     func stopReverb() { | 
| } | 
|     func startTurn() { | 
|         guard isSoundEnabled else { return } | 
| let player = SCNAudioPlayer(source:railSqueakSound) | 
| leftWheelEmitter.addAudioPlayer(player) | 
| } | 
|     func stopTurn() { | 
|         guard isSoundEnabled else { return } | 
| leftWheelEmitter.removeAllAudioPlayers() | 
| } | 
|     func startWood() { | 
| isOverWood = true | 
| updateCartSound() | 
| } | 
|     func stopWood() { | 
| isOverWood = false | 
| updateCartSound() | 
| } | 
|     func trigger(characterSpeed speed: Float) { | 
| SCNTransaction.begin() | 
| SCNTransaction.animationDuration = 2.0 | 
| characterSpeed = speed | 
| SCNTransaction.commit() | 
| } | 
|     func triggerCollision() { | 
|         guard squatCounter <= 0 else { return } | 
| // Play sound and animate. | 
| character.runAction(.playAudio(hitSound, waitForCompletion: false)) | 
| character.addAnimation(slapAnimation, forKey: nil) | 
| // Add stars. | 
| let emitter = character.childNode(withName: "Bip001_Head", recursively: true) | 
| emitter?.addParticleSystem(stars) | 
| } | 
|     private func activateTriggers() { | 
| let characterPosition = float3(character.presentation.convertPosition(SCNVector3Zero, to: nil)) | 
| var index = 0 | 
| var didTrigger = false | 
|         for trigger in triggers { | 
|             if length_squared(characterPosition - trigger.position) < 0.05 { | 
|                 if activeTriggerIndex != index { | 
| activeTriggerIndex = index | 
| trigger.action(self) | 
| } | 
| didTrigger = true | 
| break | 
| } | 
| index = index + 1 | 
| } | 
|         if didTrigger == false { | 
| activeTriggerIndex = -1 | 
| } | 
| } | 
| // MARK: Collectables | 
|     private func respawnCollectables() { | 
|         for collectable in collectables.childNodes { | 
| collectable.categoryBitMask = 0 | 
| collectable.scale = SCNVector3(x:1, y:1, z:1) | 
| } | 
|         for collectable in speedItems.childNodes { | 
| collectable.categoryBitMask = 0 | 
| collectable.scale = SCNVector3(x:1, y:1, z:1) | 
| } | 
| } | 
|     private func collectItems() { | 
| let leftHandPosition = float3(leftHand.presentation.convertPosition(SCNVector3Zero, to: nil)) | 
| let rightHandPosition = float3(rightHand.presentation.convertPosition(SCNVector3Zero, to: nil)) | 
|         for collectable in collectables.childNodes { | 
|             guard collectable.categoryBitMask != Int(CollectableState.beingCollected.rawValue) else { continue } | 
| let collectablePosition = float3(collectable.position) | 
|             if length_squared(leftHandPosition - collectablePosition) < 0.05 || length_squared(rightHandPosition - collectablePosition) < 0.05 { | 
| collectable.categoryBitMask = Int(CollectableState.beingCollected.rawValue) | 
| SCNTransaction.begin() | 
| SCNTransaction.animationDuration = 0.25 | 
| collectable.scale = SCNVector3Zero | 
| #if os(iOS) || os(tvOS) | 
| scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) | 
| #else | 
| scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) | 
| #endif | 
|                 if let name = collectable.name, name.hasPrefix("big") { | 
| headEmitter.addParticleSystem(collectBigParticleSystem) | 
| sceneView.didCollectBigItem() | 
| collectable.runAction(.playAudio(collectSound2, waitForCompletion: false)) | 
| } | 
|                 else { | 
| sceneView.didCollectItem() | 
| collectable.runAction(.playAudio(collectSound, waitForCompletion: false)) | 
| } | 
| SCNTransaction.commit() | 
| break | 
| } | 
| } | 
|         for collectable in speedItems.childNodes { | 
|             guard collectable.categoryBitMask != Int(CollectableState.beingCollected.rawValue) else { continue } | 
| let collectablePosition = float3(collectable.position) | 
|             if length_squared(rightHandPosition - collectablePosition) < 0.05 { | 
| collectable.categoryBitMask = Int(CollectableState.beingCollected.rawValue) | 
| SCNTransaction.begin() | 
| SCNTransaction.animationDuration = 0.25 | 
| collectable.scale = SCNVector3Zero | 
| collectable.runAction(.playAudio(collectSound2, waitForCompletion: false)) | 
| #if os(iOS) || os(tvOS) | 
| scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) | 
| #else | 
| scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) | 
| #endif | 
| SCNTransaction.commit() | 
| // Speed boost! | 
| SCNTransaction.begin() | 
| SCNTransaction.animationDuration = 1.0 | 
| let pov = sceneView.pointOfView! | 
| pov.camera?.xFov = 100.0 | 
| #if !os(tvOS) | 
| pov.camera?.motionBlurIntensity = 1.0 | 
| #endif | 
|                 let adjustCamera = SCNAction.run { _ in | 
| SCNTransaction.begin() | 
| SCNTransaction.animationDuration = 1.0 | 
| pov.camera?.xFov = 70 | 
| pov.camera?.motionBlurIntensity = 0.0 | 
| SCNTransaction.commit() | 
| } | 
| pov.runAction(.sequence([.wait(duration: 2.0), adjustCamera])) | 
| character.runAction(.playAudio(cartBoost, waitForCompletion: false)) | 
| SCNTransaction.commit() | 
| break | 
| } | 
| } | 
| } | 
| // MARK: Controlling the Character | 
|     func changeSpeedSound(speed: UInt) { | 
| railSoundSpeed = speed | 
| updateCartSound() | 
| } | 
|     func updateCartSound() { | 
|         guard isSoundEnabled else { return } | 
| wheels.removeAllAudioPlayers() | 
|         switch railSoundSpeed { | 
| case _ where isOverWood: | 
| wheels.addAudioPlayer(SCNAudioPlayer(source:railWoodSound)) | 
| case 1: | 
| wheels.addAudioPlayer(SCNAudioPlayer(source:railLowSpeedSound)) | 
| case 3: | 
| wheels.addAudioPlayer(SCNAudioPlayer(source:railHighSpeedSound)) | 
| case let speed where speed > 0: | 
| wheels.addAudioPlayer(SCNAudioPlayer(source:railMediumSpeedSound)) | 
| default: break | 
| } | 
| } | 
|     func updateSpeed () { | 
| let speed = boostSpeedFactor * characterSpeed | 
| let effectiveSpeed = CGFloat(speedFactor * speed) | 
| scene.rootNode.setAnimationSpeed(effectiveSpeed, forKey: cartAnimationName) | 
| wheels.setAnimationSpeed(effectiveSpeed, forKey: "wheel") | 
| idleAnimationOwner.setAnimationSpeed(effectiveSpeed, forKey: "bob_idle-1") | 
| // Update sound. | 
| updateCartSound() | 
| } | 
|     private var boostSpeedFactor: Float = 1.0 { | 
|         didSet { | 
| updateSpeed() | 
| } | 
| } | 
|     var characterSpeed: Float = 1.0 { | 
|         didSet { | 
| updateSpeed() | 
| } | 
| } | 
|     func squat() { | 
| SCNTransaction.begin() | 
|         SCNTransaction.completionBlock = { | 
| self.squatCounter -= 1 | 
| } | 
| squatCounter += 1 | 
| character.addAnimation(squatAnimation, forKey: nil) | 
| character.runAction(.playAudio(cartHide, waitForCompletion: false)) | 
| SCNTransaction.commit() | 
| } | 
|     func jump() { | 
| character.addAnimation(jumpAnimation, forKey: nil) | 
| character.runAction(.playAudio(cartJump, waitForCompletion: false)) | 
| } | 
|     func leanLeft() { | 
| character.addAnimation(leanLeftAnimation, forKey: nil) | 
| character.runAction(.playAudio(cartTurnLeft, waitForCompletion: false)) | 
| } | 
|     func leanRight() { | 
| character.addAnimation(leanRightAnimation, forKey: nil) | 
| character.runAction(.playAudio(cartTurnRight, waitForCompletion: false)) | 
| } | 
|     func startMusic() { | 
|         guard isSoundEnabled else { return } | 
| let musicIntroSource = Assets.sound(named: "music_intro.mp3") | 
| let musicLoopSource = Assets.sound(named: "music_loop.mp3") | 
| musicLoopSource.loops = true | 
| musicIntroSource.isPositional = false | 
| musicLoopSource.isPositional = false | 
| // `shouldStream` must be false to wait for completion. | 
| musicIntroSource.shouldStream = false | 
| musicLoopSource.shouldStream = true | 
|         sceneView.scene?.rootNode.runAction(.playAudio(musicIntroSource, waitForCompletion: true)) { [unowned self] in | 
| self.sceneView.scene?.rootNode.addAudioPlayer(SCNAudioPlayer(source:musicLoopSource)) | 
| } | 
| } | 
|     func startGameIfNeeded() -> Bool { | 
|         guard gameState == .notStarted else { return false } | 
| sceneView.setup2DOverlay() | 
| // Stop wind. | 
| sceneView.scene?.rootNode.removeAllAudioPlayers() | 
| // Play some music. | 
| startMusic() | 
| gameState = .started | 
| SCNTransaction.begin() | 
| SCNTransaction.animationDuration = 2.0 | 
|         SCNTransaction.completionBlock = { | 
| self.jump() | 
| } | 
| let idleAnimation = Assets.animation(named: "animation-start.scn") | 
| character.addAnimation(idleAnimation, forKey: nil) | 
| character.removeAnimation(forKey: "start", fadeOutDuration: 0.3) | 
| sceneView.pointOfView = sceneView.scene?.rootNode.childNode(withName: "Camera", recursively: true) | 
| SCNTransaction.commit() | 
| SCNTransaction.begin() | 
| SCNTransaction.animationDuration = 5.0 | 
| characterSpeed = 1.0 | 
| railSoundSpeed = 1 | 
| SCNTransaction.commit() | 
| return true | 
| } | 
| } | 
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13