SceneKit框架详细解析(七) —— 基于SceneKit的简单游戏示例的实现(六)

版本记录

版本号 时间
V1.0 2018.10.20 星期六

前言

SceneKit使用高级场景描述创建3D游戏并将3D内容添加到应用程序。 轻松添加动画,物理模拟,粒子效果和逼真的基于物理性的渲染。接下来这几篇我们就详细的解析一下这个框架。感兴趣的看下面几篇文章。
1. SceneKit框架详细解析(一) —— 基本概览(一)
2. SceneKit框架详细解析(二) —— 基于SceneKit的简单游戏示例的实现(一)
3. SceneKit框架详细解析(三) —— 基于SceneKit的简单游戏示例的实现(二)
4. SceneKit框架详细解析(四) —— 基于SceneKit的简单游戏示例的实现(三)
5. SceneKit框架详细解析(五) —— 基于SceneKit的简单游戏示例的实现(四)
6. SceneKit框架详细解析(六) —— 基于SceneKit的简单游戏示例的实现(五)

源码

1. Swift

首先看一下工程文档结构

SceneKit框架详细解析(七) —— 基于SceneKit的简单游戏示例的实现(六)_第1张图片

看一下sb中的内容

SceneKit框架详细解析(七) —— 基于SceneKit的简单游戏示例的实现(六)_第2张图片

下面就是直接看代码了

1. GameViewController.swift
import UIKit
import SceneKit

class GameViewController: UIViewController {
  
  var scnView: SCNView!
  var scnScene: SCNScene!
  var cameraNode: SCNNode!
  var spawnTime: TimeInterval = 0
  var game = GameHelper.sharedInstance
  var splashNodes: [String: SCNNode] = [:]
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setupView()
    setupScene()
    setupCamera()
    setupHUD()
    setupSplash()
    setupSounds()
  }
  
  override var shouldAutorotate: Bool {
    return true
  }
  
  override var prefersStatusBarHidden: Bool {
    return true
  }
  
  func setupView() {
    scnView = self.view as! SCNView
    //scnView.showsStatistics = true
    //scnView.allowsCameraControl = false
    scnView.autoenablesDefaultLighting = true
    scnView.delegate = self
    scnView.isPlaying = true
  }
  
  func setupScene() {
    scnScene = SCNScene()
    scnView.scene = scnScene
    scnScene.background.contents =
      "GeometryFighter.scnassets/Textures/Background_Diffuse.png"
  }
  
  func setupCamera() {
    cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    cameraNode.position = SCNVector3(x: 0, y: 5, z: 10)
    scnScene.rootNode.addChildNode(cameraNode)
  }
  
  func spawnShape() {
    var geometry: SCNGeometry
    switch ShapeType.random() {
    case .box:
      geometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
    case .sphere:
      geometry = SCNSphere(radius: 0.5)
    case .pyramid:
      geometry = SCNPyramid(width: 1.0, height: 1.0, length: 1.0)
    case .torus:
      geometry = SCNTorus(ringRadius: 0.5, pipeRadius: 0.25)
    case .capsule:
      geometry = SCNCapsule(capRadius: 0.3, height: 2.5)
    case .cylinder:
      geometry = SCNCylinder(radius: 0.3, height: 2.5)
    case .cone:
      geometry = SCNCone(topRadius: 0.25, bottomRadius: 0.5, height: 1.0)
    case .tube:
      geometry = SCNTube(innerRadius: 0.25, outerRadius: 0.5, height: 1.0)
    }
    let color = UIColor.random()
    geometry.materials.first?.diffuse.contents = color
    let geometryNode = SCNNode(geometry: geometry)
    geometryNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
    
    let randomX = Float.random(min: -2, max: 2)
    let randomY = Float.random(min: 10, max: 18)
    let force = SCNVector3(x: randomX, y: randomY , z: 0)
    let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05)
    geometryNode.physicsBody?.applyForce(force, at: position, asImpulse: true)
    
    let trailEmitter = createTrail(color: color, geometry: geometry)
    geometryNode.addParticleSystem(trailEmitter)
    
    if color == UIColor.black {
      geometryNode.name = "BAD"
      game.playSound(scnScene.rootNode, name: "SpawnBad")
    } else {
      geometryNode.name = "GOOD"
      game.playSound(scnScene.rootNode, name: "SpawnGood")
    }
    
    scnScene.rootNode.addChildNode(geometryNode)
  }
  
  func cleanScene() {
    for node in scnScene.rootNode.childNodes {
      if node.presentation.position.y < -2 {
        node.removeFromParentNode()
      }
    }
  }
  
  func createTrail(color: UIColor, geometry: SCNGeometry) -> SCNParticleSystem {
    let trail = SCNParticleSystem(named: "Trail.scnp", inDirectory: nil)!
    trail.particleColor = color
    trail.emitterShape = geometry
    return trail
  }
  
  func setupHUD() {
    game.hudNode.position = SCNVector3(x: 0.0, y: 10.0, z: 0.0)
    scnScene.rootNode.addChildNode(game.hudNode)
  }
  
  func createSplash(name: String, imageFileName: String) -> SCNNode {
    let plane = SCNPlane(width: 5, height: 5)
    let splashNode = SCNNode(geometry: plane)
    splashNode.position = SCNVector3(x: 0, y: 5, z: 0)
    splashNode.name = name
    splashNode.geometry?.materials.first?.diffuse.contents = imageFileName
    scnScene.rootNode.addChildNode(splashNode)
    return splashNode
  }
  
  func showSplash(splashName: String) {
    for (name,node) in splashNodes {
      if name == splashName {
        node.isHidden = false
      } else {
        node.isHidden = true
      }
    }
  }
  
  func setupSplash() {
    splashNodes["TapToPlay"] = createSplash(name: "TAPTOPLAY",
      imageFileName: "GeometryFighter.scnassets/Textures/TapToPlay_Diffuse.png")
    splashNodes["GameOver"] = createSplash(name: "GAMEOVER",
      imageFileName: "GeometryFighter.scnassets/Textures/GameOver_Diffuse.png")
    showSplash(splashName: "TapToPlay")
  }
  
  func setupSounds() {
    game.loadSound("ExplodeGood",
      fileNamed: "GeometryFighter.scnassets/Sounds/ExplodeGood.wav")
    game.loadSound("SpawnGood",
      fileNamed: "GeometryFighter.scnassets/Sounds/SpawnGood.wav")
    game.loadSound("ExplodeBad",
      fileNamed: "GeometryFighter.scnassets/Sounds/ExplodeBad.wav")
    game.loadSound("SpawnBad",
      fileNamed: "GeometryFighter.scnassets/Sounds/SpawnBad.wav")
    game.loadSound("GameOver",
      fileNamed: "GeometryFighter.scnassets/Sounds/GameOver.wav")
  }
  
  override func touchesBegan(_ touches: Set, with event: UIEvent?) {
    
    if game.state == .GameOver {
      return
    }
    
    if game.state == .TapToPlay {
      game.reset()
      game.state = .Playing
      showSplash(splashName: "")
      return
    }
    
    let touch = touches.first
    let location = touch!.location(in: scnView)
    let hitResults = scnView.hitTest(location, options: nil)
    
    if let result = hitResults.first {
      
      if result.node.name == "HUD" ||
        result.node.name == "GAMEOVER" ||
        result.node.name == "TAPTOPLAY" {
          return
      } else if result.node.name == "GOOD" {
        handleGoodCollision()
      } else if result.node.name == "BAD" {
        handleBadCollision()
      }
      
      createExplosion(geometry: result.node.geometry!,
        position: result.node.presentation.position,
        rotation: result.node.presentation.rotation)
      
      result.node.removeFromParentNode()
    }
  }
  
  func handleGoodCollision() {
    game.score += 1
    game.playSound(scnScene.rootNode, name: "ExplodeGood")
  }
  
  func handleBadCollision() {
    game.lives -= 1
    game.playSound(scnScene.rootNode, name: "ExplodeBad")
    game.shakeNode(cameraNode)
    
    if game.lives <= 0 {
      game.saveState()
      showSplash(splashName: "GameOver")
      game.playSound(scnScene.rootNode, name: "GameOver")
      game.state = .GameOver
      scnScene.rootNode.runAction(SCNAction.waitForDurationThenRunBlock(5) { (node:SCNNode!) -> Void in
        self.showSplash(splashName: "TapToPlay")
        self.game.state = .TapToPlay
        })
    }
  }
  
  func createExplosion(geometry: SCNGeometry, position: SCNVector3,
    rotation: SCNVector4) {
      let explosion =
      SCNParticleSystem(named: "Explode.scnp", inDirectory:
        nil)!
      explosion.emitterShape = geometry
      explosion.birthLocation = .surface
      let rotationMatrix =
      SCNMatrix4MakeRotation(rotation.w, rotation.x,
        rotation.y, rotation.z)
      let translationMatrix =
      SCNMatrix4MakeTranslation(position.x, position.y, position.z)
      let transformMatrix =
      SCNMatrix4Mult(rotationMatrix, translationMatrix)
      scnScene.addParticleSystem(explosion, transform: transformMatrix)
  }
}

extension GameViewController: SCNSceneRendererDelegate {
  func renderer(_ renderer: SCNSceneRenderer, updateAtTime time:
    TimeInterval) {
      
      if game.state == .Playing {
        if time > spawnTime {
          spawnShape()
          spawnTime = time + TimeInterval(Float.random(min: 0.2, max: 1.5))
        }
        cleanScene()
      }
      game.updateHUD()
  }
}
2. ShapeType.swift
import Foundation

// 1
enum ShapeType:Int {

  case box = 0
  case sphere
  case pyramid
  case torus
  case capsule
  case cylinder
  case cone
  case tube

  // 2
  static func random() -> ShapeType {
    let maxValue = tube.rawValue
    let rand = arc4random_uniform(UInt32(maxValue+1))
    return ShapeType(rawValue: Int(rand))!
  }
}
3. Double+Extensions.swift
import Foundation

public extension Double {
  public static func random(min: Double, max: Double) -> Double {
    let r64 = Double(arc4random(UInt64.self)) / Double(UInt64.max)
    return (r64 * (max - min)) + min
  }
}
4. Float+Extensions.swift
import Foundation

public extension Float {
  public static func random(min: Float, max: Float) -> Float {
    let r32 = Float(arc4random(UInt32.self)) / Float(UInt32.max)
    return (r32 * (max - min)) + min
  }
}
5. GameHelper.swift
import Foundation
import SceneKit
import SpriteKit

public enum GameStateType {
  case Playing
  case TapToPlay
  case GameOver
}

class GameHelper {
  
  var score:Int
  var highScore:Int
  var lastScore:Int
  var lives:Int
  var state = GameStateType.TapToPlay
  
  var hudNode:SCNNode!
  var labelNode:SKLabelNode!
  
  
  static let sharedInstance = GameHelper()
  
  var sounds:[String:SCNAudioSource] = [:]
  
  private init() {
    score = 0
    lastScore = 0
    highScore = 0
    lives = 3
    let defaults = UserDefaults.standard
    score = defaults.integer(forKey: "lastScore")
    highScore = defaults.integer(forKey: "highScore")
    
    initHUD()
  }
  
  func saveState() {
    
    lastScore = score
    highScore = max(score, highScore)
    let defaults = UserDefaults.standard
    defaults.set(lastScore, forKey: "lastScore")
    defaults.set(highScore, forKey: "highScore")
  }
  
  func getScoreString(_ length:Int) -> String {
    return String(format: "%0\(length)d", score)
  }
  
  func initHUD() {
    
    let skScene = SKScene(size: CGSize(width: 500, height: 100))
    skScene.backgroundColor = UIColor(white: 0.0, alpha: 0.0)
    
    labelNode = SKLabelNode(fontNamed: "Menlo-Bold")
    labelNode.fontSize = 48
    labelNode.position.y = 50
    labelNode.position.x = 250
    
    skScene.addChild(labelNode)
    
    let plane = SCNPlane(width: 5, height: 1)
    let material = SCNMaterial()
    material.lightingModel = SCNMaterial.LightingModel.constant
    material.isDoubleSided = true
    material.diffuse.contents = skScene
    plane.materials = [material]
    
    hudNode = SCNNode(geometry: plane)
    hudNode.name = "HUD"
    hudNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: 3.14159265)
  }
  
  func updateHUD() {
    let scoreFormatted = String(format: "%0\(4)d", score)
    let highScoreFormatted = String(format: "%0\(4)d", highScore)
    labelNode.text = "❤️\(lives)  \(highScoreFormatted) \(scoreFormatted)"
  }
  
  func loadSound(_ name:String, fileNamed:String) {
    if let sound = SCNAudioSource(fileNamed: fileNamed) {
      sound.load()
      sounds[name] = sound
    }
  }
  
  func playSound(_ node:SCNNode, name:String) {
    let sound = sounds[name]
    node.runAction(SCNAction.playAudio(sound!, waitForCompletion: false))
  }
  
  func reset() {
    score = 0
    lives = 3
  }
  
  func shakeNode(_ node:SCNNode) {
    let left = SCNAction.move(by: SCNVector3(x: -0.2, y: 0.0, z: 0.0), duration: 0.05)
    let right = SCNAction.move(by: SCNVector3(x: 0.2, y: 0.0, z: 0.0), duration: 0.05)
    let up = SCNAction.move(by: SCNVector3(x: 0.0, y: 0.2, z: 0.0), duration: 0.05)
    let down = SCNAction.move(by: SCNVector3(x: 0.0, y: -0.2, z: 0.0), duration: 0.05)
    
    node.runAction(SCNAction.sequence([
      left, up, down, right, left, right, down, up, right, down, left, up,
      left, up, down, right, left, right, down, up, right, down, left, up]))
  }
  
  
}
6. Generics.swift
import Foundation

public func arc4random  (_ type: T.Type) -> T {
  var r: T = 0
  arc4random_buf(&r, Int(MemoryLayout.size))
  return r
}
7. Int+Extensions.swift
import Foundation

public extension Int {
  public static func random(min: Int , max: Int) -> Int {
    return Int(arc4random_uniform(UInt32(max - min + 1))) + min
  }
}
8. SCNAction+Extensions.swift
import SceneKit
import Foundation

extension SCNAction {
  
  class func waitForDurationThenRemoveFromParent(_ duration:TimeInterval) -> SCNAction {
    let wait = SCNAction.wait(duration: duration)
    let remove = SCNAction.removeFromParentNode()
    return SCNAction.sequence([wait,remove])
  }
  
  class func waitForDurationThenRunBlock(_ duration:TimeInterval, block: @escaping ((SCNNode!) -> Void) ) -> SCNAction {
    let wait = SCNAction.wait(duration: duration)
    let runBlock = SCNAction.run { (node) -> Void in
      block(node)
    }
    return SCNAction.sequence([wait,runBlock])
  }
  
  class func rotateByXForever(_ x:CGFloat, y:CGFloat, z:CGFloat, duration:TimeInterval) -> SCNAction {
    let rotate = SCNAction.rotateBy(x: x, y: y, z: z, duration: duration)
    return SCNAction.repeatForever(rotate)
  }
  
}
9. UIColor+Extensions.swift
import UIKit
import SceneKit

let UIColorList:[UIColor] = [
  UIColor.black,
  UIColor.white,
  UIColor.red,
  UIColor.lime,
  UIColor.blue,
  UIColor.yellow,
  UIColor.cyan,
  UIColor.silver,
  UIColor.gray,
  UIColor.maroon,
  UIColor.olive,
  UIColor.brown,
  UIColor.green,
  UIColor.lightGray,
  UIColor.magenta,
  UIColor.orange,
  UIColor.purple,
  UIColor.teal
]

extension UIColor {
  
  public static func random() -> UIColor {
    let maxValue = UIColorList.count
    let rand = Int(arc4random_uniform(UInt32(maxValue)))
    return UIColorList[rand]
  }
  
  public static var lime: UIColor {
    return UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)
  }
  
  public static var silver: UIColor {
    return UIColor(red: 192/255, green: 192/255, blue: 192/255, alpha: 1.0)
  }
  
  public static var maroon: UIColor {
    return UIColor(red: 0.5, green: 0.0, blue: 0.0, alpha: 1.0)
  }
  
  public static var olive: UIColor {
    return UIColor(red: 0.5, green: 0.5, blue: 0.0, alpha: 1.0)
  }
  
  public static var teal: UIColor {
    return UIColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
  }
  
  public static var navy: UIColor {
    return UIColor(red: 0.0, green: 0.0, blue: 128, alpha: 1.0)
  }
}

下面看一下实现效果

后记

本篇主要讲述了基于SceneKit的简单游戏示例的实现,感兴趣的给个赞或者关注~~~

SceneKit框架详细解析(七) —— 基于SceneKit的简单游戏示例的实现(六)_第3张图片

你可能感兴趣的:(SceneKit框架详细解析(七) —— 基于SceneKit的简单游戏示例的实现(六))