ARKitTestDemo/ARTest/ViewController.swift

220 lines
6.4 KiB
Swift

//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
import SceneKit
import ARKit
private enum GameState {
case placing
case shootting
}
final class ViewController: UIViewController, ARSCNViewDelegate {
// MARK: - Constants
private static let logoMaxCount = 3
private static let cameraToLogoSpace: Float = -1
// MARK: - IBOutlets
@IBOutlet private weak var sceneView: ARSCNView!
@IBOutlet private weak var timeLabel: UILabel!
@IBOutlet private weak var scoreLabel: UILabel!
@IBOutlet private weak var prizelImageView: UIImageView!
// MARK: - Properties
fileprivate var state: GameState = .placing {
didSet {
prizelImageView.isHidden = state == .placing
}
}
fileprivate var logoCount = 0 {
didSet {
DispatchQueue.main.async {
self.scoreLabel.text = "\(self.logoCount)"
}
}
}
fileprivate var gameSeconds = 0
fileprivate var gameTimer: Timer?
// MARK: - ViewController life cycle
override func viewDidLoad() {
super.viewDidLoad()
sceneView.scene.physicsWorld.contactDelegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapAction))
sceneView.addGestureRecognizer(tapGestureRecognizer)
logoCount = 0
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARSessionConfiguration.isSupported ? ARWorldTrackingSessionConfiguration()
: ARSessionConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
stopGame()
}
// MARK: - Actions
@objc private func tapAction() {
switch state {
case .placing:
addLogo()
case .shootting:
shoot()
}
}
private func addLogo() {
guard let currentFrame = sceneView.session.currentFrame else {
return
}
let logo = Logo()
sceneView.scene.rootNode.addChildNode(logo)
var translation = matrix_identity_float4x4
translation.columns.3.z = ViewController.cameraToLogoSpace
logo.simdTransform = matrix_multiply(currentFrame.camera.transform, translation)
logoCount += 1
if logoCount == ViewController.logoMaxCount {
startGame()
}
}
private func shoot() {
let arBullet = ARBullet()
let (direction, position) = cameraVector
arBullet.position = position
let bulletDirection = direction
arBullet.physicsBody?.applyForce(bulletDirection, asImpulse: true)
sceneView.scene.rootNode.addChildNode(arBullet)
}
}
// MARK: - Game logic
extension ViewController {
fileprivate func startGame() {
state = .shootting
gameTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] _ in
self?.gameSeconds += 1
DispatchQueue.main.async {
self?.configureTimeLabel()
}
})
}
fileprivate func stopGame() {
state = .placing
gameTimer?.invalidate()
gameTimer = nil
gameSeconds = 0
logoCount = 0
configureTimeLabel()
}
fileprivate func configureTimeLabel() {
timeLabel.isHidden = self.gameSeconds == 0
let seconds = self.gameSeconds % 60
let minutes = (self.gameSeconds / 60) % 60
timeLabel.text = String(format: "%02d:%02d", minutes, seconds)
}
}
// MARK: - Utils
extension ViewController {
fileprivate var cameraVector: (SCNVector3, SCNVector3) { // (direction, position)
if let frame = self.sceneView.session.currentFrame {
let mat = SCNMatrix4FromMat4(frame.camera.transform) // 4x4 transform matrix describing camera in world space
let dir = SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33) // orientation of camera in world space
let pos = SCNVector3(mat.m41, mat.m42, mat.m43) // location of camera in world space
return (dir, pos)
}
return (SCNVector3(0, 0, 0), SCNVector3(0, 0, 0))
}
}
// MARK: - SCNPhysicsContactDelegate
extension ViewController: SCNPhysicsContactDelegate {
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
guard let nodeABitMask = contact.nodeA.physicsBody?.categoryBitMask,
let nodeBBitMask = contact.nodeB.physicsBody?.categoryBitMask,
nodeABitMask & nodeBBitMask == CollisionCategory.logos.rawValue & CollisionCategory.arBullets.rawValue else {
return
}
contact.nodeB.removeFromParentNode()
logoCount -= 1
if logoCount == 0 {
DispatchQueue.main.async {
self.stopGame()
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
contact.nodeA.removeFromParentNode()
})
}
}