本系列文章是對<3D Apple Games by Tutorials>一書的學習記錄和體會node
此書對應的代碼地址git
SceneKit系列文章目錄github
更多iOS相關知識查看github上WeekWeekUpProjectswift
在Xcode主菜單中選擇File > New > Project.數組
選擇iOS/Application/Game模板,點擊Next app
輸入項目名GeometryFighter,選擇Swift語言, SceneKit遊戲技術,Universal設備類型, 去掉單元測試的勾,點擊Next: dom
下一步,清理不須要的文件. 刪除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中
此時運行遊戲,看到的是黑屏.
從resources文件夾中拖放GeometryFighter.scnassets到咱們的項目中,選中Copy items if needed, Create Groups還有個人項目GeometryFighter,點擊Finish.
在項目中選中素材文件,能夠查看詳情
先點擊Assets.xcassets,拖放GeometryFighter.scnassets/Textures/Logo_Diffuse.png到AppIcon下面.
再點擊LaunchScreen.storyboard,選中view,設置背景爲深藍色:
從右下的媒體庫中,拖放Logo_Diffuse到view中,設置Content Mode爲Aspect Fit:
添加約束:
運行一下:
在GameViewController.swift中setupScene()
方法的底部添加:
scnScene.background.contents = "GeometryFighter.scnassets/Textures/
Background_Diffuse.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)
}
複製代碼
其中:
cameraNode
.cameraNode
的camera
屬性.(x:0, y:0, z:10)
.cameraNode
到場景中,做爲場景根節點的一個子節點.完成後,在viewDidLoad()
方法中,setupScene()
方法後面調用:
setupCamera()
複製代碼
添加一個新文件,命名爲setupCamera()
打開並更改內容以下:
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))!
} }
複製代碼
代碼含義:
ShapeType
,用來表示各類不一樣形狀.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)
}
複製代碼
代碼含義:
switch
語句來處理ShapeType.random()
中返回的形狀.暫時咱們只添加一個立方體形狀,其餘的稍後添加.SCNBox
對象並儲存在geometry
中.SCNNode
實例,命名爲geometryNode
.構造器使用geometry
參數來自動建立一個節點並將幾何體附加在上面.還須要在viewDidLoad()
中調用一下,放在setupCamera()
後面:
spawnShape()
複製代碼
運行一下,看到一個白方塊:
由於立方體節點是從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
複製代碼
代碼含義:
showStatistics
會在屏幕底部啓動一個實時的統計面板.allowsCameraControl
能讓你用手勢(單指輕掃,雙指輕掃,雙指捏合,雙擊)控制攝像機的位置.autoenablesDefaultLighting
則建立一個泛光燈來照亮你的場景.運行一下,看起來好多了!
拖放GameUtils文件夾到咱們的項目中,點擊Finish:
打開GameViewController.swift,在spawnShape()
中的建立geometryNode
代碼以後添加一行:
geometryNode.physicsBody =
SCNPhysicsBody(type: .dynamic, shape: nil)
複製代碼
shape傳nil,會自動根據顯示的形狀建立一個物理形體.
運行一下,會看到隨機產生的幾何體,自動掉落下去了,這是由於SceneKit的場景會自動打開重力:
在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)
複製代碼
代碼含義:
applyForce(direction: at: asImpulse:)
方法將力應用到geometryNode
的物理形體上.運行一下,物體憑空出現後,受到力的做用被拋向空中,飛翔以後,最終受到重力影響下落.
如今物體是在屏幕中間憑空出現,效果很很差,咱們只須要修改攝像機的位置就能夠改善.在setupCamera()
中更改位置:
cameraNode.position = SCNVector3(x: 0, y: 5, z: 10)
複製代碼
下面,還能夠給幾何體添加一些隨機顏色.在spawnShape()
方法中添加一行,在建立geometry
以後中, 建立geometryNode
以前:
geometry.materials.first?.diffuse.contents = UIColor.random()
複製代碼
運行一下,物體就有了漂亮的顏色:
在GameViewController.swift中,添加SCNSceneRendererDelegate
協議,並實現協議方法:
// 1
extension GameViewController: SCNSceneRendererDelegate {
// 2
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// 3
spawnShape()
} }
複製代碼
在此以前,還要先成爲視圖的代理.在setupView()
方法的末尾添加一行:
scnView.delegate = self
複製代碼
此時,已經能夠刪除viewDidLoad()
中對spawnShape()
的調用了.運行一下:
能夠發現,建立的太多了,場面幾乎失控了.咱們須要控制一下建立幾何體的時間間隔.
在cameraNode
下方添加一個新屬性:
var spawnTime: TimeInterval = 0
複製代碼
而後替換renderer(_:updateAtTime:)
方法中的內容:
// 1
if time > spawnTime {
spawnShape()
// 2
spawnTime = time + TimeInterval(Float.random(min: 0.2, max: 1.5))
}
複製代碼
代碼含義:
time
(當前系統的時間),若是大於spawnTime
就產生一個新的形狀,不然,什麼也不作.spawnTime
來決定下一次建立的時機.下一次建立時間應該是在當前時間上增長一個隨機量.運行一下.
spawnShape()
方法一直不停地建立新的節點並添加到場景中,可是卻沒有移除,僅僅是掉落出視線而已.雖然SceneKit有些優化能讓場景繼續運行下去不卡頓,但咱們仍然須要將不要的節點移除掉.
在spawnShape()
下方,添加幾行:
func cleanScene() {
// 1
for node in scnScene.rootNode.childNodes {
// 2
if node.presentation.position.y < -2 {
// 3
node.removeFromParentNode()
}
} }
複製代碼
代碼含義:
position
來表示它的真實位置,此時的position
反應的是動畫開始前的位置.SceneKit在動畫期間保存了對象的副本,並用副原本執行動畫.要想獲得動畫進行過程當中的實際位置,須要使用presentationNode
屬性.在renderer(_: updatedAtTime:)
方法中調用cleanScene()
方法:
cleanScene()
複製代碼
還有一個問題須要處理.默認狀況下,SceneKit在沒有動畫時會進入"暫停"狀態.咱們能夠啓用SCNView
實例的playing
屬性來阻止它.
在setupView()
的最後,添加下面的代碼:
scnView.isPlaying = true
複製代碼
運行一下,旋轉看看物體下落到哪裏消失的.
建立一個新分組
命名爲Particles,右擊分組選擇New File,選擇iOS/Resource/SceneKit Particle System模板,點擊Next繼續:
接下來,在Particle system template中選擇Fire類型,點擊Next.保存爲Tail.scnp並點擊Create.而後你會看到這樣的場景:
注:Xcode 11 中,粒子系統建立方式有變化,在.scn 場景右上角的「+」號中。
在右側配置粒子系統的屬性以下:
配置完成後的最終效果以下,若是你看到的不同,試着旋轉一下攝像機:
在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
}
複製代碼
代碼含義:
createTrail(_: geometry:)
接收color
和geometry
參數來建立粒子系統.進入spawnShape()
中,找到設置材質顏色的代碼,用常量保存起來:
let color = UIColor.random()
geometry.materials.first?.diffuse.contents = color
複製代碼
下一步,在spawnShape()
中,在添加力到geometryNode
的物理形體上以後,添加下面的代碼:
let trailEmitter = createTrail(color: color, geometry: geometry)
geometryNode.addParticleSystem(trailEmitter)
複製代碼
運行一下:
給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()
複製代碼
運行一下,屏幕上方就出現了擡頭顯示面板:
在咱們處理觸摸事件以前,咱們須要標識出每一個物體.最簡單的方法就是給他們起個名字.
在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)
}
}
複製代碼
代碼含義:
scnView
的座標.hitTest(_: options:)
返回一個SCNHitTestResult
對象數組,表明着從用戶觸摸點發出的射線碰到的全部物體.最後一步,須要禁用攝像機控制:
scnView.allowsCameraControl = false
複製代碼
運行一下,用手指觸摸就會毀滅!
再建立一個粒子效果,命名爲Explode.scnp.嘗試着本身配置一下,讓它看起來像這樣:
能夠用下面的圖片做爲參考:
能夠在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)
}
複製代碼
代碼含義:
createExplosion(_: position: rotation:)
接收三個參數:geometry
定義了粒子效果的形狀,position
和rotation
幫助放置爆炸效果到場景中.geometry
做爲emitterShape
,這樣粒子就能夠從形狀的表面發射出來.addParticleSystem(_: wtihTransform)
將爆炸效果添加到場景中.在handleTouchFor(_:)
中添加兩次下面的代碼-"good"分支一次,"bad"分支一次.添加在移除節點以前:
createExplosion(geometry: node.geometry!, position: node.presentation.position,rotation: node.presentation.rotation)
複製代碼
這裏,咱們又使用了
presentation
,由於物理效果模擬正在移動節點.
運行一下,點擊爆炸!
這個效果能夠在projects/ challenge/GeometryFighter文件夾中找到.
爲了讓遊戲更好玩,還能夠添加不少彩蛋效果,好比:
這些效果均可以在projects/juiced/GeometryFighter文件夾中找到最終完成品.打開嘗試一下吧.