WatchPuzzle WatchKit Extension/InterfaceController.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
WatchOS WKInterfaceController implementation of the game. |
*/ |
import WatchKit |
import Foundation |
import simd |
import SceneKit |
import SpriteKit |
class InterfaceController: WKInterfaceController { |
// MARK: Types |
/// A struct containing all the `SCNNode`s used in the game. |
struct GameNodes { |
let object: SCNNode |
let objectMaterial: SCNMaterial |
let confetti: SCNNode |
let camera: SCNCamera |
let countdownLabel: SKLabelNode |
let congratulationsLabel: SKLabelNode |
/// Queries the root node for the expected nodes. |
init?(sceneRoot: SCNNode) { |
guard let object = sceneRoot.childNode(withName: "teapot", recursively:true), let objectMaterial = object.geometry?.firstMaterial else { return nil } |
guard let confetti = sceneRoot.childNode(withName: "particles", recursively: true) else { return nil } |
guard let camera = sceneRoot.childNode(withName: "camera", recursively: true)!.camera else { return nil } |
self.object = object |
self.objectMaterial = objectMaterial |
self.confetti = confetti |
self.camera = camera |
countdownLabel = SKLabelNode() |
countdownLabel.horizontalAlignmentMode = .center |
congratulationsLabel = SKLabelNode(text: "You Win!") |
congratulationsLabel.fontColor = InterfaceController.GameColors.defaultFont |
congratulationsLabel.fontSize = 45; |
} |
} |
/// Defines the colors used in the game. |
struct GameColors { |
static let defaultFont = UIColor(red:31.0/255, green:226.0/255.0, blue:63.0/255.0, alpha:1.0) |
static let warning = UIColor.orange |
static let danger = UIColor.red |
} |
// MARK: Properties |
@IBOutlet var sceneInterface: WKInterfaceSCNScene! |
var gameNodes: GameNodes? |
var gameStarted = false |
var initialObject3DRotation = SCNMatrix4Identity |
var initialSphereLocation = float3() |
var countdown = 0 |
weak var textUpdateTimer: Timer? |
weak var particleRemovalTimer: Timer? |
// MARK: WKInterfaceController |
override func awake(withContext context: Any?) { |
super.awake(withContext: context) |
setupGame() |
} |
override func willActivate() { |
// Start the game if not already started. |
if !gameStarted { |
startGame() |
} |
super.willActivate() |
} |
// MARK: IB Actions |
@IBAction func handleTap(sender: AnyObject) { |
if let tapGesture = sender as? WKTapGestureRecognizer { |
if tapGesture.numberOfTapsRequired == 1 && !gameStarted { |
// Restart the game on single tap only if presenting congratulation screen. |
startGame() |
} |
} |
} |
// MARK: Gesture reconginzer handling |
/** |
Handle rotation of the 3D object by computing rotations of a virtual |
trackball using the pan gesture touch locations. |
On state ended, end the game if the object has the right orientation. |
*/ |
@IBAction func handlePan(panGesture: WKPanGestureRecognizer) { |
guard let gameNodes = gameNodes, gameStarted else { return } |
let location = panGesture.locationInObject() |
let bounds = panGesture.objectBounds() |
// Compute the projection of the interface point to the virtual trackball. |
let sphereLocation = sphereProjection(forInterfaceLocation: location, inBounds: bounds) |
switch panGesture.state { |
case .began: |
// Record initial states. |
initialSphereLocation = sphereLocation |
initialObject3DRotation = gameNodes.object.transform |
case .cancelled, .ended, .changed: |
// Compute the rotation and apply to the object. |
let currentRotation = rotationFromPoint(initialSphereLocation, to: sphereLocation) |
gameNodes.object.transform = SCNMatrix4Mult(initialObject3DRotation, currentRotation) |
default: |
debugPrint("Unhandled gesture state: \(panGesture.state)") |
} |
// End the game if the object has the initial orientation. |
if panGesture.state == .ended { |
endGameOnCorrectOrientation() |
} |
} |
// MARK: Game flow |
/// Setup overlays and lookup scene objects. |
func setupGame() { |
guard let sceneRoot = sceneInterface.scene?.rootNode, let gameNodes = GameNodes(sceneRoot: sceneRoot) else { fatalError("Unable to load game nodes") } |
self.gameNodes = gameNodes |
gameNodes.object.transform = SCNMatrix4Identity |
gameNodes.objectMaterial.transparency = 0.0 |
gameNodes.confetti.isHidden = true |
let skScene = SKScene(size: CGSize(width: contentFrame.size.width, height: contentFrame.size.height)) |
skScene.scaleMode = SKSceneScaleMode.resizeFill |
skScene.addChild(gameNodes.countdownLabel) |
sceneInterface.overlaySKScene = skScene |
} |
/// Start the game. |
func startGame() { |
guard let gameNodes = gameNodes else { fatalError("Nodes not set") } |
let startSequence = SCNAction.sequence([ |
// Wait for 1 second. |
SCNAction.wait(duration: 1.0), |
SCNAction.group([ |
// Fade in. |
SCNAction.fadeIn(duration: 0.3), |
// Start the game. |
SCNAction.run({ [weak self] (node: SCNNode) in |
guard let gameNodes = self?.gameNodes else { return } |
// Compute a random orientation for the object3D. |
let theta = Float(M_PI) * (Float(arc4random()) / 0x100000000) |
let phi = acosf(2.0 * Float(arc4random()) / 0x100000000 - 1) / Float(M_PI) |
var axis = float3() |
axis.x = cosf(theta) * sinf(phi) |
axis.y = sinf(theta) * sinf(phi) |
axis.z = cosf(theta) |
let angle = 2.0 * Float(M_PI) * (Float(arc4random()) / 0x100000000) |
SCNTransaction.begin() |
SCNTransaction.animationDuration = 0.3 |
SCNTransaction.completionBlock = { |
self?.gameStarted = true |
} |
gameNodes.objectMaterial.transparency = 1.0 |
gameNodes.object.transform = SCNMatrix4MakeRotation(angle, axis.x, axis.y, axis.z) |
SCNTransaction.commit() |
}), |
]) |
]) |
gameNodes.object.runAction(startSequence) |
// Load and set the background image. |
let backgroundImage = UIImage(named:"art.scnassets/background.png") |
sceneInterface.scene?.background.contents = backgroundImage |
// Hide particles, set camera projection to orthographic. |
particleRemovalTimer?.invalidate() |
gameNodes.congratulationsLabel.removeFromParent() |
gameNodes.confetti.isHidden = true |
gameNodes.camera.usesOrthographicProjection = true |
// Reset the countdown. |
countdown = 30 |
gameNodes.countdownLabel.text = "\(countdown)" |
gameNodes.countdownLabel.fontColor = InterfaceController.GameColors.defaultFont |
gameNodes.countdownLabel.position = CGPoint(x: contentFrame.size.width / 2, y: contentFrame.size.height - 30) |
textUpdateTimer?.invalidate() |
textUpdateTimer = Timer.scheduledTimer(timeInterval: 1, |
target: self, |
selector: #selector(updateText(timer:)), |
userInfo: nil, |
repeats: true) |
} |
/// Update countdown timer. |
func updateText(timer: Timer) { |
guard let gameNodes = gameNodes else { fatalError("Nodes not set") } |
gameNodes.countdownLabel.text = "\(countdown)" |
sceneInterface.isPlaying = true |
sceneInterface.isPlaying = false |
countdown -= 1 |
if countdown < 0 { |
gameNodes.countdownLabel.fontColor = InterfaceController.GameColors.danger |
textUpdateTimer?.invalidate() |
return |
} |
else if countdown < 10 { |
gameNodes.countdownLabel.fontColor = InterfaceController.GameColors.warning |
} |
} |
/** |
End the game by showing the congratulation screen after fading the object |
to white. |
*/ |
func endGame() { |
guard let gameNodes = gameNodes else { fatalError("Nodes not set") } |
textUpdateTimer?.invalidate() |
SCNTransaction.begin() |
SCNTransaction.animationDuration = 0.5 |
SCNTransaction.completionBlock = { () in |
SCNTransaction.begin() |
SCNTransaction.animationDuration = 0.3 |
SCNTransaction.completionBlock = { [weak self] () in |
self?.showCongratulation() |
gameNodes.objectMaterial.emission.contents = UIColor.black |
self?.gameStarted = false |
} |
SCNTransaction.commit() |
} |
gameNodes.object.transform = SCNMatrix4Identity |
gameNodes.objectMaterial.emission.contents = UIColor.white |
gameNodes.objectMaterial.transparency = 0.0 |
SCNTransaction.commit() |
} |
// MARK: Convenience |
/// Compute the projection of screen points to unit sphere points. |
func sphereProjection(forInterfaceLocation location: CGPoint, inBounds bounds: CGRect) -> float3 { |
let screenLocation = screenProjection(forInterfaceLocation: location, inBounds: bounds) |
return sphereProjection(forScreenLocation: screenLocation) |
} |
/// Compute projection from object interface to virtual screen on the range [-1, 1]. |
func screenProjection(forInterfaceLocation location: CGPoint, inBounds bounds: CGRect) -> CGPoint { |
let w = bounds.size.width |
let h = bounds.size.height |
let aspectRatioCorrection = (h - w) / 2 |
var screenCoord = CGPoint(x: location.x / w * 2.0 - 1.0, |
y: ((h - location.y) - aspectRatioCorrection) / w * 2.0 - 1.0) |
screenCoord.x = min(1.0, max(-1.0, screenCoord.x)) |
screenCoord.y = min(1.0, max(-1.0, screenCoord.y)) |
return screenCoord |
} |
/// Compute projection of virtual screen point to unit sphere. |
func sphereProjection(forScreenLocation location: CGPoint) -> float3 { |
var sphereCoord = float3() |
let squaredLenght = location.x * location.x + location.y * location.y |
if squaredLenght <= 1.0 { |
sphereCoord.x = Float(location.x) |
sphereCoord.y = Float(location.y) |
sphereCoord.z = sqrtf(1.0 - Float(squaredLenght)) |
} else { |
let n = 1.0 / sqrtf(Float(squaredLenght)) |
sphereCoord.x = n * Float(location.x) |
sphereCoord.y = n * Float(location.y) |
sphereCoord.z = 0 |
} |
return sphereCoord |
} |
/// Compute the rotation matrix from one point to another on a unit sphere. |
func rotationFromPoint(_ start: float3, to end: float3) -> SCNMatrix4 { |
let axis = cross(start, end) |
let angle = atan2f(length(axis), dot(start, end)) |
return SCNMatrix4MakeRotation(angle, axis.x, axis.y, axis.z) |
} |
/// End the game if the object has its initial orientation with a 10 degree tolerance. |
func endGameOnCorrectOrientation() { |
guard let gameNodes = gameNodes, gameStarted else { return } |
let transform = SCNMatrix4ToMat4(gameNodes.object.transform) |
let unitX: float4 = [1 , 0, 0, 0] |
let unitY: float4 = [0 , 1, 0, 0] |
let tX: float4 = matrix_multiply(unitX, transform) |
let tY: float4 = matrix_multiply(unitY, transform) |
let toleranceDegree : Float = 10.0 |
let max_cos_angle = cosf(toleranceDegree * Float(M_PI) / 180) |
let cos_angleX = dot(unitX, tX) |
let cos_angleY = dot(unitY, tY) |
if cos_angleX >= max_cos_angle && cos_angleY >= max_cos_angle { |
endGame() |
} |
} |
// Show the congratulation screen. |
func showCongratulation() { |
guard let gameNodes = gameNodes else { fatalError("Nodes not set") } |
gameNodes.camera.usesOrthographicProjection = false |
sceneInterface.scene?.background.contents = UIColor.black |
gameNodes.confetti.isHidden = false |
particleRemovalTimer?.invalidate() |
particleRemovalTimer = Timer.scheduledTimer(timeInterval: 30, |
target: self, |
selector: #selector(removeParticles(timer:)), |
userInfo: nil, |
repeats:false) |
gameNodes.congratulationsLabel.removeFromParent() |
gameNodes.congratulationsLabel.position = CGPoint(x: contentFrame.size.width/2 , y: contentFrame.size.height/2) |
gameNodes.congratulationsLabel.xScale = 0; |
gameNodes.congratulationsLabel.yScale = 0; |
gameNodes.congratulationsLabel.alpha = 0; |
gameNodes.congratulationsLabel.run( |
SKAction.group([ |
SKAction.fadeIn(withDuration:0.25), |
SKAction.sequence([ |
SKAction.scale(to: 0.70, duration:0.25), |
SKAction.scale(to: 0.80, duration:0.2)]), |
]) |
) |
sceneInterface.overlaySKScene?.addChild(gameNodes.congratulationsLabel) |
} |
// Remove the confetti particles. |
func removeParticles(timer: Timer) { |
guard let gameNodes = gameNodes else { fatalError("Nodes not set") } |
gameNodes.confetti.isHidden = true |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-10-27