概述
詳細
上周一, 相信很多人和我一樣, 全程觀看了WWDC2017的開發者大會, 其中雖然亮點平平但也能些許的看出蘋果未來的戰略, 雖然已經從先驅者變成跟隨者, 但強者恆強的道理是亘古不變的真理, 而且在生態鏈的建設上也是無人能出其右, 雖然在消費者眼中最為關注的是HomePod和iPad Pro10.5, 而在開發者眼中為之眼前一亮的則是ARKit和Core ML.
一、了解SceneKit
Core ML 剛發布的時候還以為是終於能用Swift進行模型的訓練了, 終於不用學習縮進地獄的Python了, 然而這僅僅是一個類似適配器一樣的東西, 把通過機器學習訓練完成后的模型通過.py轉換器轉換成Core ML的格式並進行集成到Apple Devices中, 誒, 沒意思...
ARKit 的出現能夠看到蘋果對擴展現實技術未來推動的決心, 的確如會上所說, ARKit憑借眾多的移動設備一躍成為了最大的AR開發平台, 在會上的Demo也做的栩栩如生, 但對於ARKit, 並不是直接就能夠學習的, 需要一些基礎的知識, 比如跨平台的Unity, 可是並沒有做過游戲開發的我, 新學一門語言雖然並非難事, 但要學個大概也並非易事啊!! 還好ARKit也支持SceneKit和SpriteKit, 誒... 親兒子嘛, 所以為了學習之后的ARKit, 先學習下SceneKit打好基礎吧~
SceneKit 基本概念
SCNVector3:
果然三維的向量, 蘋果創建了一個有三個屬性的結構體, 這也是意料之中的事情, 對於向量的理解就是方向加上速度, 還有畢達哥拉斯定理的應用也是非常重要的一部分.
public struct SCNVector3 { public var x: Float public var y: Float public var z: Float public init() public init(x: Float, y: Float, z: Float) }
有了之前學習SpriteKit的經驗, 現在對於游戲世界的概念也變的清晰了起來, SceneKit和SpriteKit的區別簡單的來說就是二維和三維的區別, 現在我們就要對Z軸的概念需要有更深的理解了.
SCNCamera
攝像頭的概念和之前的SKCamera還是有歇息不同的, 好歹也是個三維的攝像頭, 更加真實的體現了拍電影時機位的特點, 360°無死角的拍攝, 比較能夠符合這個概念吧.
var cameraNode: SCNNode! func setupCamera() { cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 0, y: 5, z: 10) scnScene.rootNode.addChildNode(cameraNode) }
Camera和SpriteKit中的概念相同但形式不同, 圖中zNear和zFar就能了解到機位的概念, 代碼中的position是指機位上調5個單位, 向后調10個單位.
SCNGeometry
三維幾何圖形, 這些幾何圖形都是系統框架內自帶的, 當然后期可能會有其他方法進行自定義幾何圖形, 這些幾何圖形, 如果你了解CALayer的子類CAShapeLayer你就能夠了解到, 只不過是二維和三維的區別了.
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) }
其中SCNBox, SCNSphere, SCNPyramid, SCNTorus, SCNCapsule, SCNCylinder, SCNCone, SCNTube對應這上圖中的八個幾何圖形, 各自的初始化方法就是對於長寬高及弧度的屬性設置.
-
geometry.materials.first?.diffuse.contents = color 表示這對幾何圖形的內容進行賦值
-
geometryNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil) 和SpriteKit一樣 創建三維的物理體
-
geometryNode.physicsBody?.applyForce(force, at: position, asImpulse: true) 對物理體申請力及脈沖力
-
geometryNode.addParticleSystem(trailEmitter) 添加粒子系統
SceneKit 渲染周期
所謂的渲染周期, 就是在每一幀系統會做哪些時期, 當然不是很理解也沒關系, 我們可以用生命周期距離, UIKit中當壓入棧的時候, 會出發生命周期, 從開辟到銷毀會經歷好幾個方法, 當然渲染周期也是類似.
SceneKit在每一幀渲染的時候會經歷圖上九個過程:
1.更新:視圖在其代理方法中進行渲染(:updateAtTime :)。 一般寫一些基本的更新邏輯.
2.執行操作和動畫:SceneKit執行所有操作並執行所有連接的動畫到場景圖中的節點。
3.應用動畫:視圖調用其代理的渲染器(:didApplyAnimationsAtTime :)。在這一點上,場景中的所有節點都基於應用的動作和動畫完成了一幀的動畫。
4.模擬物理:SceneKit將物理模擬的一個步驟應用於場景中的所有物理體。
5.完成模擬物理:視圖在其委托上調用渲染器(:didSimulatePhysicsAtTime :)。在這一點上,物理模擬步驟已經完成,您可以添加任何取決於上面應用的物理學的邏輯。
6.評估約束:SceneKit評估和應用約束,這是可以配置的規則,使SceneKit自動調整節點的轉換。
7.將要渲染場景:視圖在其委托上調用渲染器(:willRenderScene:atTime :)。在這一點上,視圖即將呈現場景,所以在這里應該執行最后一分鍾的更改。
8.渲染場景視圖:SceneKit渲染視圖中的場景。
9.渲染場景完成:最后一步是讓視圖調用其代理渲染器(_:didRenderScene:atTime :)。這標志着渲染循環的一個循環的結束;您可以將任何游戲邏輯放在這里,需要在進程重新啟動之前執行。
SceneKit 粒子系統
如果了解過CALayer的子類的話, 就可能會知道有一個粒子的Layer, 相信做過直播項目的同學們都有所了解, 其實技術做久了就會發現好多的東西概念都是相通的, 僅僅是屬性和方法名不同罷了, 所以我認為做技術的朋友, 記住API本身是沒有什么意義的, 畢竟現在誰開發能離開Google和Baidu, 對於技術來講, 設計模式, 數據結構及算法, 才是技術進階的根基, 所以現在我不再強調API了, 而是更加強調概念.
-
出生率:控制粒子的排放率。將其設置為25,指示粒子引擎以每秒25個粒子的速率產生新的粒子。
-
預熱持續時間:在渲染粒子之前模擬運行的秒數。這可以用於在開始時顯示充滿顆粒的屏幕,而不是等待顆粒填充屏幕。將其設置為0,以便從一開始就可以觀察到模擬。
-
位置:相對於形狀,發射器產生其粒子的位置。將其設置為頂點,這意味着粒子將使用幾何頂點作為產生位置。
-
排放空間:發射的顆粒將駐留的空間。將其設置為世界空間,以便將發射的粒子發射到世界空間,而不是對象節點本身的本地空間。
-
方向模式:控制如何產生的粒子行進;您可以將它們全部移動到恆定的方向,讓它們從形狀的表面徑向向外移動,或者簡單地將它們隨機移動。將其設置為Constant,保持所有發射的粒子沿恆定方向移動。
-
方向:指定方向模式不變時使用的初始方向矢量。將此矢量設置為(x:0,y:0,z:0),將方向設置為無。
-
傳播角度:隨機產生的粒子的發射角度。將其設置為0°,從而在以前設置的方向上精確地發射顆粒。
-
初始角度:發射粒子的初始角度。將其設置為0°,因為這與零向矢量無關。
-
形狀:發射粒子的形狀。將形狀設置為“球形”,從而使用球形作為幾何體。
-
形狀半徑:此屬性的存在取決於您使用的形狀; 對於球形發射器,這決定了球體的大小。 將其設置為0.2,它定義了足夠大的球體,滿足您的需要。
-
使用壽命:指定粒子的壽命(以秒為單位)。 將其設置為1,因此單個粒子將只存在一秒鍾。
-
線速度:指定發射粒子的線速度。 將其設置為0,以便粒子不會產生方向或速度。
-
角速度:指定發射的粒子的角速度。 將其設置為0,以便顆粒不會旋轉。
-
加速度:指定施加到發射粒子的力矢量。 將它設置為(x:0,y:-5,z:0) - 它是一個向下的向量 - 一旦產生,就模擬顆粒上的軟重力效應。
-
速度因子:設定粒子模擬速度的乘數。 將其設置為1以正常速度運行模擬。
-
拉伸因子:在其運動方向上延伸顆粒的乘數。 將其設置為0以不展開粒子圖像。
-
圖像:指定要渲染每個粒子的圖像。 選擇CircleParticle.png圖像,給出粒子的主要形狀。
-
顏色:設置指定圖像的色調。 將顏色設置為白色,給出粒子系統的基本顏色為白色。
-
動畫顏色:使粒子在其使用壽命期間變色。 取消選中此項,因為粒子顏色根本不會改變。
-
顏色變化:為粒子顏色添加一點隨機性。 您可以將其設置為(h:0,s:0,b:0,a:0),因為粒子顏色不會改變。
-
大小:指定粒子的大小。 將其設置為0.1,使發射的顆粒尺寸小。
-
初始幀:設置動畫序列的第一個基於零的幀。 第零幀對應於網格中的左上角圖像。 您正在使用單幀圖像,因此將其設置為0。
-
幀率:以秒為單位控制動畫的速率。 將其設置為0,因為這僅適用於使用包含多個幀的圖像時。
-
動畫:指定動畫序列的行為。 重復循環動畫,Clamp僅播放一次,Auto Reverse從開始到結束播放,然后再次播放。 你可以把它放在重復上,因為在使用單幀圖像時並不重要。
-
維度:指定動畫網格中的行數和列數。 由於您使用單幀圖像,請將其設置為(行:1,列:1)。
-
混合:指定在將粒子繪制到場景中時渲染器的混合模式。 將其設置為Alpha,將使用圖像Alpha通道信息進行透明度。
-
方向:控制顆粒的旋轉。 將其設置為Billboard屏幕對齊,這將始終保持平面微粒面向相機視圖,因此您不會注意到顆粒確實是平面圖像。
-
排序:設置粒子的渲染順序。 此屬性與混合模式配合使用,並影響如何應用混合。 將其設置為無,因此粒子系統將不會使用排序。
-
照明:控制SceneKit是否將照明應用於顆粒。 取消選中此項,以便粒子系統忽略場景中的任何指示燈。
-
受重力影響:使場景的重力影響顆粒。 取消選中此項,因為您不希望粒子系統參與物理模擬。
-
受物理場影響:造成場景內的物理場影響粒子。 取消選中此項,因為您不希望物理字段對粒子產生影響。
-
死於沖突:使您的場景中的物理體碰撞並破壞粒子。 取消選中此項,因為您不想在與場景中的節點對象沖突時刪除粒子。
-
物理屬性:在物理模擬過程中控制粒子物理行為的基本物理屬性。 您可以將所有這些保留為默認值,因為粒子系統將不會使用它們。
-
發射持續時間:控制發射器發射新顆粒的時間長度。 將其設置為1,這將激活粒子發射器,總長度為1秒。
-
空閑持續時間:循環粒子系統在指定的發射持續時間內發射粒子,然后在指定的空閑持續時間內空轉,之后循環重復。 將其設置為0,因此粒子系統將僅發射一次。
-
循環:指定粒子系統是否像爆炸一樣發射粒子,或像火山一樣持續發射粒子。 將其設置為循環,以使發射器在再次從場景中移除之前盡可能長的發射。
二、SceneKit 實戰演練
SceneKit 實戰演練
我們今天所要實現的是一個類似水果忍者的游戲, 從底部發射出一些幾何模塊, 點擊賺取分數, 當點到黑色的時候就會被扣除一條命. 根據我們剛剛所學的知識, 我們就能夠實現出這樣一個3D游戲, 就當入門吧!
Step1 場景設置
override func viewDidLoad() { super.viewDidLoad() setupView() //添加View setupScene() //添加場景 setupCamera() //添加攝像頭 setupHUD() //添加文字 setupSplash() //添加圖片 setupSounds() //添加聲音 } 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" }
Step2 逐幀渲染
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() //更新文字表述 } } func cleanScene() { for node in scnScene.rootNode.childNodes { if node.presentation.position.y < -2 { 當幾何圖形到某個位置的時候 刪除節點 node.removeFromParentNode() } } }
Step3 用戶交互
override func touchesBegan(_ touches: Set<UITouch>, 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() //刪除子節點 } }
Step4 交互邏輯
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 }) } }
Step5 爆炸效果
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) }
三、運行效果與文件截圖
1、運行效果:
2、文件截圖:
GeometryFighter文件夾內的截圖:
GeometryFighter.xcodeproj文件夾內的截圖:
注:本文著作權歸作者,由demo大師發表,拒絕轉載,轉載需要作者授權