[SceneKit專題]20-仿水果忍者小遊戲Geometry-Fighter

說明

本系列文章是對<3D Apple Games by Tutorials>一書的學習記錄和體會node

此書對應的代碼地址git

SceneKit系列文章目錄github

更多iOS相關知識查看github上WeekWeekUpProjectswift

WX20171104-172721@2x.png

01-Scenes場景

在Xcode主菜單中選擇File > New > Project.數組

選擇iOS/Application/Game模板,點擊Next app

WX20171104-173654@2x.png

輸入項目名GeometryFighter,選擇Swift語言, SceneKit遊戲技術,Universal設備類型, 去掉單元測試的勾,點擊Next: dom

WX20171104-173711@2x.png

下一步,清理不須要的文件. 刪除art.scnassets文件夾. 清理GameViewController.swift文件中的內容:ide

import UIKit
import SceneKit
class GameViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
  }
  override var shouldAutorotate: Bool {
return true
}
  override var prefersStatusBarHidden: Bool {
return true
} }
複製代碼

而後在viewDidLoad()前面添加:工具

var scnView: SCNView!
複製代碼

再在prefersStatusBarHidden()下方添加:oop

func setupView() {
   scnView = self.view as! SCNView
}
複製代碼

並在Main.storyboard中將view類型設置爲SCNView.

繼續添加屬性:

var scnScene: SCNScene!
複製代碼

setupView()下方接着寫:

func setupScene() {
  scnScene = SCNScene()
  scnView.scene = scnScene
}
複製代碼

viewDidLoad()中調用這些方法:

setupView()
 setupScene()
複製代碼

Resources中找到遊戲圖標,拖放到Assets.xcassets

WX20171104-181911@2x.png

此時運行遊戲,看到的是黑屏.

02-Nodes節點

resources文件夾中拖放GeometryFighter.scnassets到咱們的項目中,選中Copy items if needed, Create Groups還有個人項目GeometryFighter,點擊Finish.

WX20171104-182534@2x.png

在項目中選中素材文件,能夠查看詳情

WX20171104-182553@2x.png

下面添加啓動屏幕.

先點擊Assets.xcassets,拖放GeometryFighter.scnassets/Textures/Logo_Diffuse.pngAppIcon下面.

WX20171104-182901@2x.png

再點擊LaunchScreen.storyboard,選中view,設置背景爲深藍色:

WX20171104-182913@2x.png

從右下的媒體庫中,拖放Logo_Diffuse到view中,設置Content ModeAspect Fit:

WX20171104-183238@2x.png

添加約束:

WX20171104-183402@2x.png

運行一下:

WX20171104-183454@2x.png

添加遊戲中的背景圖片

GameViewController.swiftsetupScene()方法的底部添加:

scnScene.background.contents = "GeometryFighter.scnassets/Textures/
Background_Diffuse.png"
複製代碼

運行一下

WX20171104-183709@2x.png

添加攝像機

打開GameViewController.swift,在scnScene下方添加新屬性:

var cameraNode: SCNNode!
複製代碼

並在setupScene()方法下方添加:

func setupCamera() {
  // 1
  cameraNode = SCNNode()
  // 2
  cameraNode.camera = SCNCamera()
  // 3
  cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
  // 4
  scnScene.rootNode.addChildNode(cameraNode)
}
複製代碼

其中:

  1. 建立一個空節點並賦值到cameraNode.
  2. 建立一個新的SCNCamera對象,並賦值給cameraNodecamera屬性.
  3. 設置攝像機位置(x:0, y:0, z:10).
  4. 添加cameraNode到場景中,做爲場景根節點的一個子節點.

完成後,在viewDidLoad()方法中,setupScene()方法後面調用:

setupCamera()
複製代碼
添加幾何體

添加一個新文件,命名爲setupCamera()

WX20171104-184838@2x.png

打開並更改內容以下:

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))!
} }
複製代碼

代碼含義:

  1. 建立一個新的枚舉名爲ShapeType,用來表示各類不一樣形狀.
  2. 定義一個static方法名爲random(),用來產生隨機的ShapeType.

GameViewController.swift中,setupCamera()方法下面,添加:

func spawnShape() {
  // 1
  var geometry:SCNGeometry
  // 2
  switch ShapeType.random() {
  default:
// 3
    geometry = SCNBox(width: 1.0, height: 1.0, length: 1.0,
      chamferRadius: 0.0)
}
// 4
  let geometryNode = SCNNode(geometry: geometry)
  // 5
  scnScene.rootNode.addChildNode(geometryNode)
}
複製代碼

代碼含義:

  1. 建立一個佔位幾何體,稍後會用到.
  2. 定義一個switch語句來處理ShapeType.random()中返回的形狀.暫時咱們只添加一個立方體形狀,其餘的稍後添加.
  3. 建立一個SCNBox對象並儲存在geometry中.
  4. 建立一個SCNNode實例,命名爲geometryNode.構造器使用geometry參數來自動建立一個節點並將幾何體附加在上面.
  5. 將節點添加到場景的根節點上.

還須要在viewDidLoad()中調用一下,放在setupCamera()後面:

spawnShape()
複製代碼

運行一下,看到一個白方塊:

WX20171104-190431@2x.png

由於立方體節點是從spwnSpape()建立的,會位於場景的(x:0, y:0, z:0).咱們又是從cameraNode節點來觀察場景的,攝像機節點位置是在(x:0, y:0: z:10),因此正好立方體正好出如今屏幕中間.

爲了更方便觀察,咱們能夠打開視圖的內置屬性,給GameViewController.swift中的setupView()方法再添加幾行:

// 1
scnView.showsStatistics = true
// 2
scnView.allowsCameraControl = true
// 3
scnView.autoenablesDefaultLighting = true
複製代碼

代碼含義:

  1. showStatistics會在屏幕底部啓動一個實時的統計面板.
  2. allowsCameraControl能讓你用手勢(單指輕掃,雙指輕掃,雙指捏合,雙擊)控制攝像機的位置.
  3. autoenablesDefaultLighting則建立一個泛光燈來照亮你的場景.

運行一下,看起來好多了!

WX20171104-191707@2x.png

03-Physics物理效果

導入遊戲工具類

拖放GameUtils文件夾到咱們的項目中,點擊Finish:

WX20171105-115859@2x.png

物理效果

打開GameViewController.swift,在spawnShape()中的建立geometryNode代碼以後添加一行:

geometryNode.physicsBody =
  SCNPhysicsBody(type: .dynamic, shape: nil)
複製代碼

shape傳nil,會自動根據顯示的形狀建立一個物理形體.

運行一下,會看到隨機產生的幾何體,自動掉落下去了,這是由於SceneKit的場景會自動打開重力:

WX20171105-120752@2x.png

添加力

spawnShape()中的建立geometryNode代碼以後添加一行:

// 1
let randomX = Float.random(min: -2, max: 2)
let randomY = Float.random(min: 10, max: 18)
// 2
let force = SCNVector3(x: randomX, y: randomY , z: 0)
// 3
let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05)
// 4
geometryNode.physicsBody?.applyForce(force,
  at: position, asImpulse: true)
複製代碼

代碼含義:

  1. 建立兩個隨機的浮點數表明力的x份量和y份量.用到的正是咱們添加進項目中的工具類.
  2. 用這些隨機數來建立一個向量表明這個力.
  3. 建立另外一個向量來表示力施加的位置.這個位置是故意稍微偏離中心一些的,這樣就能讓物體旋轉起來.
  4. 經過調用applyForce(direction: at: asImpulse:)方法將力應用到geometryNode的物理形體上.

運行一下,物體憑空出現後,受到力的做用被拋向空中,飛翔以後,最終受到重力影響下落.

WX20171105-122954@2x.png

添加更多效果

如今物體是在屏幕中間憑空出現,效果很很差,咱們只須要修改攝像機的位置就能夠改善.在setupCamera()中更改位置:

cameraNode.position = SCNVector3(x: 0, y: 5, z: 10)
複製代碼

WX20171105-123807@2x.png

下面,還能夠給幾何體添加一些隨機顏色.在spawnShape()方法中添加一行,在建立geometry以後中, 建立geometryNode以前:

geometry.materials.first?.diffuse.contents = UIColor.random()
複製代碼

運行一下,物體就有了漂亮的顏色:

WX20171105-124113@2x.png

04-Render Loop渲染循環

建立

GameViewController.swift中,添加SCNSceneRendererDelegate協議,並實現協議方法:

// 1
extension GameViewController: SCNSceneRendererDelegate {
  // 2
  func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    // 3
    spawnShape()
} }
複製代碼

在此以前,還要先成爲視圖的代理.在setupView()方法的末尾添加一行:

scnView.delegate = self
複製代碼

此時,已經能夠刪除viewDidLoad()中對spawnShape()的調用了.運行一下:

WX20171105-125235@2x.png

能夠發現,建立的太多了,場面幾乎失控了.咱們須要控制一下建立幾何體的時間間隔.

cameraNode下方添加一個新屬性:

var spawnTime: TimeInterval = 0
複製代碼

而後替換renderer(_:updateAtTime:)方法中的內容:

// 1
if time > spawnTime {
  spawnShape()
// 2
  spawnTime = time + TimeInterval(Float.random(min: 0.2, max: 1.5))
}
複製代碼

代碼含義:

  1. 檢查time(當前系統的時間),若是大於spawnTime就產生一個新的形狀,不然,什麼也不作.
  2. 建立一個物體後,更新spawnTime來決定下一次建立的時機.下一次建立時間應該是在當前時間上增長一個隨機量.

運行一下.

WX20171105-130051@2x.png

移除子節點

spawnShape()方法一直不停地建立新的節點並添加到場景中,可是卻沒有移除,僅僅是掉落出視線而已.雖然SceneKit有些優化能讓場景繼續運行下去不卡頓,但咱們仍然須要將不要的節點移除掉.

spawnShape()下方,添加幾行:

func cleanScene() {
  // 1
  for node in scnScene.rootNode.childNodes {
    // 2
    if node.presentation.position.y < -2 {
      // 3
      node.removeFromParentNode()
    }
} }
複製代碼

代碼含義:

  1. 循環遍歷場景的根節點.
  2. 這裏須要注意,由於物理效果模擬此時正在進行中,因此咱們不能簡單取物體的position來表示它的真實位置,此時的position反應的是動畫開始前的位置.SceneKit在動畫期間保存了對象的副本,並用副原本執行動畫.要想獲得動畫進行過程當中的實際位置,須要使用presentationNode屬性.
  3. 讓一個物體消失.

renderer(_: updatedAtTime:)方法中調用cleanScene()方法:

cleanScene()
複製代碼

還有一個問題須要處理.默認狀況下,SceneKit在沒有動畫時會進入"暫停"狀態.咱們能夠啓用SCNView實例的playing屬性來阻止它.

setupView()的最後,添加下面的代碼:

scnView.isPlaying = true
複製代碼

運行一下,旋轉看看物體下落到哪裏消失的.

WX20171105-132510@2x.png

05-Particle Systems粒子系統

運動尾跡

建立一個新分組

WX20171105-133600@2x.png

命名爲Particles,右擊分組選擇New File,選擇iOS/Resource/SceneKit Particle System模板,點擊Next繼續:

WX20171105-133632@2x.png

接下來,在Particle system template中選擇Fire類型,點擊Next.保存爲Tail.scnp並點擊Create.而後你會看到這樣的場景:

WX20171105-134020@2x.png

注:Xcode 11 中,粒子系統建立方式有變化,在.scn 場景右上角的「+」號中。

在右側配置粒子系統的屬性以下:

WX20171105-134200@2x.png

WX20171105-134213@2x.png

WX20171105-134223@2x.png

WX20171105-134235@2x.png

WX20171105-134247@2x.png

WX20171105-134258@2x.png

WX20171105-134309@2x.png

配置完成後的最終效果以下,若是你看到的不同,試着旋轉一下攝像機:

WX20171105-134406@2x.png

GameViewController.swift類中添加下面的代碼:

// 1
func createTrail(color: UIColor, geometry: SCNGeometry) ->
  SCNParticleSystem {
  // 2
  let trail = SCNParticleSystem(named: "Trail.scnp", inDirectory: nil)!
  // 3
  trail.particleColor = color
// 4
  trail.emitterShape = geometry
// 5
  return trail
}
複製代碼

代碼含義:

  1. 定義一個方法createTrail(_: geometry:)接收colorgeometry參數來建立粒子系統.
  2. 從先前建立的文件里加載粒子系統.
  3. 根據傳入的顏色修改粒子的顏色.
  4. 用傳入的幾何體參數來指定發射器的形狀.
  5. 返回新建立的粒子系統.

進入spawnShape()中,找到設置材質顏色的代碼,用常量保存起來:

let color = UIColor.random()
geometry.materials.first?.diffuse.contents = color
複製代碼

下一步,在spawnShape()中,在添加力到geometryNode的物理形體上以後,添加下面的代碼:

let trailEmitter = createTrail(color: color, geometry: geometry)
geometryNode.addParticleSystem(trailEmitter)
複製代碼

運行一下:

WX20171105-141006@2x.png

擡頭顯示面板

GameViewController.swift中添加一個新屬性,放在spawnTime後面:

var game = GameHelper.sharedInstance
複製代碼

GameViewController最底部,createTail()方法後面,添加下面的方法:

func setupHUD() {
  game.hudNode.position = SCNVector3(x: 0.0, y: 10.0, z: 0.0)
  scnScene.rootNode.addChildNode(game.hudNode)
}
複製代碼

其中咱們是從幫助文件庫中調用的game.hudNode.

下一步,咱們須要調用setupHUD().在viewDidLoad()方法的底部添加一行:

setupHUD()
複製代碼

咱們還須要不斷更新顯示的內容.在renderer(_: updateAtTime:)方法底部,調用game.updateHUD():

game.updateHUD()
複製代碼

運行一下,屏幕上方就出現了擡頭顯示面板:

WX20171105-141720@2x.png

觸摸處理

在咱們處理觸摸事件以前,咱們須要標識出每一個物體.最簡單的方法就是給他們起個名字.

spawnShape()中添加下面的代碼,放在添加粒子系統以後:

if color == UIColor.black {
  geometryNode.name = "BAD"
} else {
  geometryNode.name = "GOOD"
}
複製代碼

下一步,在GameViewController中, setupHUD()以後,添加下列方法:

func handleTouchFor(node: SCNNode) {
  if node.name == "GOOD" {
    game.score += 1
    node.removeFromParentNode()
  } else if node.name == "BAD" {
    game.lives -= 1
    node.removeFromParentNode()
  }
}
複製代碼

下一步,在GameViewController中, handleTouchFor(_:)以後,添加下列方法:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
// 1
  let touch = touches.first!
  // 2
  let location = touch.location(in: scnView)
  // 3
  let hitResults = scnView.hitTest(location, options: nil)
  // 4
  if let result = hitResults.first {
// 5
    handleTouchFor(node: result.node)
  }
}
複製代碼

代碼含義:

  1. 拿到可用的touch.此處若是玩家用了多根手指就會有多個touch.
  2. 從屏幕座標轉換到scnView的座標.
  3. hitTest(_: options:)返回一個SCNHitTestResult對象數組,表明着從用戶觸摸點發出的射線碰到的全部物體.
  4. 檢查第一個結果是否可用.
  5. 將第一個碰到的節點傳遞給觸摸處理方法,它能夠計算增長分數或減小生命值.

最後一步,須要禁用攝像機控制:

scnView.allowsCameraControl = false
複製代碼

運行一下,用手指觸摸就會毀滅!

WX20171105-143014@2x.png

爆炸粒子效果

再建立一個粒子效果,命名爲Explode.scnp.嘗試着本身配置一下,讓它看起來像這樣:

WX20171105-143131@2x.png

能夠用下面的圖片做爲參考:

WX20171105-143143@2x.png

能夠在projects/challenge/ GeometryFighter文件夾中找到已經完成的Explode.scnp文件.

接着還須要將這個效果用起來.在GameViewController中, touchesBegan(_: withEvent)方法後面,添加下面的代碼:

// 1
func createExplosion(geometry: SCNGeometry, position: SCNVector3, rotation: SCNVector4) {
  // 2
  let explosion =
    SCNParticleSystem(named: "Explode.scnp", inDirectory:
  nil)!
  explosion.emitterShape = geometry
  explosion.birthLocation = .surface
  // 3
  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)
  // 4
  scnScene.addParticleSystem(explosion, transform: transformMatrix)
}
複製代碼

代碼含義:

  1. createExplosion(_: position: rotation:)接收三個參數:geometry定義了粒子效果的形狀,positionrotation幫助放置爆炸效果到場景中.
  2. 加載Explode.scnp,將其用做發射器.發射器使用geometry做爲emitterShape,這樣粒子就能夠從形狀的表面發射出來.
  3. 建立旋轉矩陣和平移矩陣,相乘獲得複合變換矩陣.
  4. 調用addParticleSystem(_: wtihTransform)將爆炸效果添加到場景中.

handleTouchFor(_:)中添加兩次下面的代碼-"good"分支一次,"bad"分支一次.添加在移除節點以前:

createExplosion(geometry: node.geometry!,  position: node.presentation.position,rotation: node.presentation.rotation)
複製代碼

這裏,咱們又使用了presentation,由於物理效果模擬正在移動節點.

運行一下,點擊爆炸!

WX20171105-144715@2x.png

這個效果能夠在projects/ challenge/GeometryFighter文件夾中找到.

彩蛋

爲了讓遊戲更好玩,還能夠添加不少彩蛋效果,好比:

  • 遊戲狀態管理:好比點擊開始遊戲,暫停/開始,遊戲結束等.
  • 啓動閃屏:根據遊戲狀態提供不一樣的效果.
  • 聲音效果:根據玩家的操做,提供聲音反饋
  • 攝像機抖動:劇烈爆炸會產生劇烈衝擊波,添加攝像機抖動來模擬衝擊波效果.

這些效果均可以在projects/juiced/GeometryFighter文件夾中找到最終完成品.打開嘗試一下吧.

相關文章
相關標籤/搜索