說明
本系列文章是對<3D Apple Games by Tutorials>一書的學習記錄和體會
更多iOS相關知識檢視github上WeekWeekUpProject
01-Scenes場景
在Xcode主選單中選擇File > New > Project.
選擇iOS/Application/Game模板,點選Next
輸入專案名GeometryFighter,選擇Swift語言, SceneKit遊戲技術,Universal裝置型別, 去掉單元測試的勾,點選Next:
下一步,清理不需要的檔案. 刪除art.scnassets資料夾. 清理GameViewController.swift檔案中的內容:
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()
下方新增:
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中
此時執行遊戲,看到的是黑屏.
02-Nodes節點
從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
. - 建立一個新的SCNCamera物件,並賦值給
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
,用來表示各種不同形狀. - 定義一個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)
}
複製程式碼
程式碼含義:
- 建立一個佔位幾何體,稍後會用到.
- 定義一個
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
則建立一個泛光燈來照亮你的場景.
執行一下,看起來好多了!
03-Physics物理效果
匯入遊戲工具類
拖放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)
複製程式碼
程式碼含義:
- 建立兩個隨機的浮點數代表力的x分量和y分量.用到的正是我們新增進專案中的工具類.
- 用這些隨機數來建立一個向量代表這個力.
- 建立另一個向量來表示力施加的位置.這個位置是故意稍微偏離中心一些的,這樣就能讓物體旋轉起來.
- 通過呼叫
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()
複製程式碼
執行一下,物體就有了漂亮的顏色:
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()
的呼叫了.執行一下:
可以發現,建立的太多了,場面幾乎失控了.我們需要控制一下建立幾何體的時間間隔.
在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
複製程式碼
執行一下,旋轉看看物體下落到哪裡消失的.
05-Particle Systems粒子系統
運動尾跡
建立一個新分組
命名為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)
}
}
複製程式碼
程式碼含義:
- 拿到可用的touch.此處如果玩家用了多根手指就會有多個touch.
- 從螢幕座標轉換到
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
幫助放置爆炸效果到場景中.- 載入Explode.scnp,將其用作發射器.發射器使用
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資料夾中找到最終完成品.開啟嘗試一下吧.