說明
本系列文章是對<3D Apple Games by Tutorials>一書的學習記錄和體會
更多iOS相關知識檢視github上WeekWeekUpProject
06-SceneKit Editor場景編輯器
建立遊戲
開啟Xcode,建立一個新專案,選擇iOS/Application/Game模板. 遊戲名Breaker,語言選Swift,遊戲技術SceneKit,裝置支援Universal,取消勾選兩個測試選項.
開啟專案,刪除art.scnassets資料夾.並將GameViewController.swift中的內容替換為下面:
import UIKit
import SceneKit
class GameViewController: UIViewController {
var scnView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
// 1
setupScene()
setupNodes()
setupSounds()
}
// 2
func setupScene() {
scnView = self.view as! SCNView
scnView.delegate = self
}
func setupNodes() {
}
func setupSounds() {
}
override var shouldAutorotate: Bool { return true }
override var prefersStatusBarHidden: Bool { return true }
}
// 3
extension GameViewController: SCNSceneRendererDelegate {
func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
}
}
複製程式碼
程式碼含義:
- 在
viewDidLoad()
裡呼叫一些空的佔位方法.稍後,我們會向這些方法裡新增程式碼. - 在建立場景方法裡將
self.view
轉換為SCNView
物件並儲存起來以便訪問,記self
成為渲染迴圈的代理. GameViewController
遵守SCNSceneRendererDelegate
協議,並實現renderer(_: updateAtTime:)
方法.
找到resources/AppIcon資料夾,裡面有各種尺寸的應用圖示.開啟專案的Assets.xcassets並選擇AppIcon.將圖示拖放到裡面去.
![WX20171106-215541@2x.png](https://i.iter01.com/images/86abea670bd6cd59bf88994f4263f8114cdd41fe56fb87f5a27507d3d94a1a96.png)
選中Assets.xcassets,拖放resources/Logo_Diffuse.png到裡面.然後開啟LaunchScreen.storyboard,將背景顏色改為深藍色.在右下角的Media Library中找到Logo_Diffuse,拖放到啟動螢幕裡.設定圖片的Content Mode為Aspect Fit,並新增約束,讓它處在螢幕中間:
![WX20171106-221817@2x.png](https://i.iter01.com/images/2dfdd6a801833bb2305484877232a5a4acc12f5f786097b00fd2e6b47f25ee86.png)
完成後:
![WX20171106-221905@2x.png](https://i.iter01.com/images/4ab5e0aaca6915441f6963b2222b2c408c69cecae4d6c72209396c8ea3436767.png)
下面還需要新增音效.找到resources/Breaker.scnassets資料夾,拖放到時專案中.注意選中Copy items if needed, Create groups及目標專案Breaker.這裡面有子資料夾,Sounds和Textures分別是音訊和紋理圖片.
還需要一些遊戲工具類.拖放resources/GameUtil到專案中.
開啟GameViewController.swift,在scnView
下面新增屬性:
var game = GameHelper.sharedInstance
複製程式碼
載入場景
右擊Breaker.scnassets,建立一個新資料夾命名為Scenes,用來盛放所有場景.
![WX20171106-222712@2x.png](https://i.iter01.com/images/02f35ea984bad6590af6b208fd8690d031ed1edebb5092aeab48c6da8176d464.png)
選中Breaker專案,建立新檔案,選擇iOS/Resource/ SceneKit Scene模板,命名為Game.scn.注意位置選擇在Breaker.scnassets下面的Scenes資料夾下面.
![WX20171106-222942@2x.png](https://i.iter01.com/images/84c05673ce09bd8d3f24a825b8028e5d42df7abe6a9fc89601f43b1768bf4cd0.png)
從右下角的物體物件庫中拖拽一個Box出來,隨便放在場景中:
![WX20171106-225141@2x.png](https://i.iter01.com/images/6ca3e3da5d7f4ce56d3e3ea874c568a83328be3e7d8e2c873b5d1fd5abf99049.png)
在GameViewController
中新增一個新屬性:
var scnScene: SCNScene!
複製程式碼
接下來,在setupScene()
方法的底部,新增下面程式碼:
scnScene = SCNScene(named: "Breaker.scnassets/Scenes/Game.scn")
scnView.scene = scnScene
複製程式碼
執行一下:
![WX20171106-225545@2x.png](https://i.iter01.com/images/581542afd7845115df07be6538d708c0f81498c92ec27da55b22d48ec6a6e204.png)
測試完成後,就可以刪除立方體了.在左側的場景樹中,按Command-A選擇所有節點,按Delete鍵全部刪除.
![WX20171106-225812@2x.png](https://i.iter01.com/images/814e6946586190c30575018fbeaa1e6868ae3087bff85b29f3bb9ae944814fe1.png)
07-Cameras攝像機
新增攝像機
開啟GameViewController.swift,在setupNodes()
中新增下面一行:
scnScene.rootNode.addChildNode(game.hudNode)
複製程式碼
然後,在renderer(_,updateAtTime)
中新增一行:
game.updateHUD()
複製程式碼
選中Game.scn,以顯示編輯器. 在左下角點選 + 按鈕,建立一個空的節點預設命名為untitled.將其改名為Cameras.
![WX20171108-215639@2x.png](https://i.iter01.com/images/c3aaf2de7ffe96da84195262938370b63409318d40c0ea90135da7147383584c.png)
從右下角的物件庫中拖放兩個Camera節點到場景中.
![WX20171108-215828@2x.png](https://i.iter01.com/images/c069013dc298930ee2995c245400e4abc8fd83eded6dafbfdf6510725897eddf.png)
分別命名為VerticalCamera和HorizontalCamera.稍後會講為什麼需要兩個攝像機.
TL/DR:雙攝像機能讓你更好地處理橫屏與豎屏狀態下的視角.
讓兩個攝像機都成為Cameras的子節點:
![WX20171108-221039@2x.png](https://i.iter01.com/images/67ae99eb9e12d077361b17e6ac5bbc8a45e31f1554e66c2aa202a39308a02eda.png)
選中VerticalCamera,在節點檢查器中設定Position為(x:0, y:22, z:9)
,Euler為 (x:-70, y:0, z:0)
![WX20171108-221410@2x.png](https://i.iter01.com/images/3b66484d4bf8460f25d5cd7013fa46f2fb36cffceea33e59c4306560a9e93204.png)
選中HorizontalCamera,在節點檢查器中設定Position為(x:0, y:8.5, z:15)
,Euler為 (x:-40, y:0, z:0)
![WX20171108-221819@2x.png](https://i.iter01.com/images/953b9667695ab34c7702daa8555aba2ca696a88976a7a4fb3e77a594bb31846a.png)
對比來看,水平攝像機比豎直攝像機離得更近,角度也更小.
![WX20171108-221912@2x.png](https://i.iter01.com/images/2428f4ae08a8297538c749f9a3c7ee3600cfa2f7abfc59e7b9a9a39c2790acfc.png)
在GameViewController.swift中新增兩個屬性:
var horizontalCameraNode: SCNNode!
var verticalCameraNode: SCNNode!
複製程式碼
在setupNodes()
方法的開頭新增下面程式碼:
horizontalCameraNode = scnScene.rootNode.childNode(withName:
"HorizontalCamera", recursively: true)!
verticalCameraNode = scnScene.rootNode.childNode(withName:
"VerticalCamera", recursively: true)!
複製程式碼
因為場景已經載入進來了,所以我們只需要用childNode(withName:recursively:)
方法來找到攝像機節點就可以了.recursively
設定為true
會遞迴遍歷其中的子資料夾.
處理旋轉
設定在旋轉時,螢幕的顯示範圍也在跟著變.與其在兩個方向中找到"sweet-spot",倒不如使用兩個攝像機,每一個都可以最大化利用顯示範圍.
![WX20171108-223028@2x.png](https://i.iter01.com/images/e3138e7be6e5683f116e08fbeb66b3d0f9dda41bb2aa4f2a1206fbb27801a293.png)
為了追蹤裝置方向,需要重寫viewWillTransition(to size:, with coordinator:)
方法:
// 1
override func viewWillTransition(to size: CGSize, with coordinator:
UIViewControllerTransitionCoordinator) {
// 2
let deviceOrientation = UIDevice.current.orientation
switch(deviceOrientation) {
case .portrait:
scnView.pointOfView = verticalCameraNode
default:
scnView.pointOfView = horizontalCameraNode
}
}
複製程式碼
程式碼含義:
- 重寫
viewWillTransition(to:with:)
來執行切換方向的程式碼. - 根據從
UIDevice.current().orientation
中獲取到的deviceOrientation
來切換方向.如果將要切換到.portrait
,則設定視點為verticalCameraNode
.否則,切換視點到horizontalCameraNode
.
執行一下:
![WX20171108-223615@2x.png](https://i.iter01.com/images/13084ddfe6ce82e4b27f7d43e21b66a5bcd6e02030d59a127f02b56736f1e5a3.png)
08-Lights燈光
新增小球
選中Game.scn.在物件庫中,拖放一個Sphere到場景中.
![WX20171108-223931@2x.png](https://i.iter01.com/images/e2fa298dc98fc26c88486796bdb62e4022646de90bff8810dd57027833d25b46.png)
確保球體節點仍處於選中狀態,然後選擇節點檢查器.將Name命名為Ball,將position設定為0,這樣球就在正中間了.
![WX20171108-230307@2x.png](https://i.iter01.com/images/626198d07123a6ce9faa33996ba154edffc98175e84a5431be6827deef58af0d.png)
接著開啟屬性檢查器.將Radius改為0.25, Segment count為17.
![WX20171108-230522@2x.png](https://i.iter01.com/images/c9a4cc41828afe6a11354c4a6ddcfbc6c01408a8956e2ba431d9d3cce8b7e006.png)
兩種球體sphere和geosphere本質上是同樣的.不同的是下面的geodesic核取方塊,決定了渲染引擎如何構建球體.一種是四邊形,一種是三角形.
下一步,選中材料檢查器.將Diffuse改為7F7F7F.將Specular改為White.
![WX20171108-230913@2x.png](https://i.iter01.com/images/44e25458f9bc6ce0a2ac0d1377b81c654b9a0cb443ae26d7514b4cc1ba86847b.png)
繼續向下,找到Setting區域,將Shininess改為0.3.
![WX20171108-231032@2x.png](https://i.iter01.com/images/06097cffa7ed7e3b54522a9567be039ee89ce907535e5ba3ae79081effba33d0.png)
完成後,選中HorizontalCamera,場景看起來是這樣:
![WX20171108-231153@2x.png](https://i.iter01.com/images/008d43537c79d5a3b0537995fc54aef1a995de0cbf6268436179fa1551ce0e01.png)
下面,開啟GameViewController.swift,新增一個屬性:
var ballNode: SCNNode!
複製程式碼
在setupNodes()
末尾新增下面的程式碼:
ballNode = scnScene.rootNode.childNode(withName: "Ball", recursively:true)!
複製程式碼
三點光照
首先,開啟Game.scn,點選 + 建立一個空節點,命名為Lights.它將用來盛放場景中的所有燈光.
![WX20171109-212629@2x.png](https://i.iter01.com/images/73efa168f188766fcf5e393660e72ca88d314a2128f06d236bc8ae11ad2ee640.png)
從物件庫中,拖放一個Omni light到場景中,放到燈光節點下面.
![WX20171109-213018@2x.png](https://i.iter01.com/images/1cddeb49d0e01ee3195e0b20ed7cbff14427db09aad20d0bd2fa60ee55ab7105.png)
選中燈光節點,開啟節點檢查器,重新命名節點為Back.設定Position為 (x:-15, y:-2, z:15)
![WX20171109-213223@2x.png](https://i.iter01.com/images/a43c18d19ea7819d005e1ffd0c779674b42491eb64b4cc5ae3cb747390958020.png)
選擇Attributes Inspector,設定泛光燈屬性.
![WX20171109-213336@2x.png](https://i.iter01.com/images/97a23a1e1a1e38e0fcd4fe01355ce54baf83fe2addf245b89f291de8e796f3b9.png)
再從物件庫中拖放一個Omni light光源到場景中.還是移動到Lights組節點下.
命名新節點為Front,設定Position為 (x:6, y:10, z:15).
![WX20171109-213612@2x.png](https://i.iter01.com/images/33571bf05db04b9cd35c881ab90081839b588b7800586d5725abb751b019a8ed.png)
再從物件庫中拖放一個Ambient light光源到場景中.還是移動到Lights組節點下.
![WX20171109-220913@2x.png](https://i.iter01.com/images/a1dc98e59b9d5967616cbe5424228941f81ccf8f112bfec41af31ffff6e77a7e.png)
命名新節點為Ambient,設定Position為 (x:0, y:0, z:0).
![WX20171109-221045@2x.png](https://i.iter01.com/images/8d0cc1f7a24d63428b61ba17a5f8b06e1572213f2f9d5aa9ffc4575bd14c6537.png)
開啟屬性檢查器:
![WX20171109-221205@2x.png](https://i.iter01.com/images/3c77121f3417b882c985ff3b76131df1be55d6c2ed4355d4fc44f271b9fa39ac.png)
完成後的場景效果:
![WX20171109-221251@2x.png](https://i.iter01.com/images/b78adaf125b9cdf7fb44a592e5f09da3e9ecaa8e19fdcd7f0ae68e9bdd281c6b.png)
執行一下,效果如下:
![WX20171109-221341@2x.png](https://i.iter01.com/images/5dbc97500a5a3f448c6845a94eb1321614b07cee09f54157aecf87470ed917e1.png)
09-Geometric Shapes幾何形狀
建立邊框
選擇Game.scn,點選 + 按鈕新增一個空白節點,命名為Barriers. 這將是用來盛放所有的邊框節點的:
![WX20171109-224809@2x.png](https://i.iter01.com/images/6e42dfd9efff37203668f08ffb8e7ed38a33162ddfc9301d3b7287282b680ad4.png)
從物件庫中,拖放一個Box,在場景樹中,將新的立方體節點拖放到Barriers組節點下面.
![WX20171109-224937@2x.png](https://i.iter01.com/images/3c55754da350c02948ae1362ebe0985d9f1ed0efb909bd7317a6e540a4fd7e04.png)
開啟節點檢查器,命名為Top,設定位置為 (x:0,y:0,z:-10.5).開屬性檢查器,設定Size為width:13, height:2, length:1,設定Chamfer radius為0.3. 開啟
![WX20171111-110146@2x.png](https://i.iter01.com/images/76d5233c694f67322ba3fccd54c9475dd1187e0b35fdf4d0e14ca69b74def55d.png)
![WX20171109-231133@2x.png](https://i.iter01.com/images/376fde4de1524532a27d8a42dff0302cbb023e4b546b3344b003697ffc4a14c8.png)
![WX20171111-105642@2x.png](https://i.iter01.com/images/e46ad94e596a3b2d42ab37a2c95c08ecc2ecbf2920c21d73270b27d0ffc06f08.png)
下面我們通過複製的方式來建立底部的邊框. 複製方法是:按住Option鍵,點選要複製的節點並沿著藍色座標軸拖動:
![WX20171111-110434@2x.png](https://i.iter01.com/images/3445f9eb3bc13008b7d43edc6dfb97f0ee2fd06ff9938a32fe6ef75409153bb2.png)
複製成功後,重新命名為Bottom,將設定為Barriers組的子節點.
![WX20171111-110514@2x.png](https://i.iter01.com/images/886fc539532fcfd4a9bc57d9551dafe4cfee39c3cca209f7cd831d286fe05b52.png)
更改一下位置,Position為 (x:0, y:0, z:10.5).
![WX20171111-113425@2x.png](https://i.iter01.com/images/e0e5ef25a1ce467359195f6967077bb3fdb6d9f2e460b61eb8d4155a05bee09b.png)
最終效果,如圖:
![WX20171111-113510@2x.png](https://i.iter01.com/images/05ddb7d555653b10e3c2f318fbd113c59c721ea2441d822e5b598c5768068f06.png)
還有一個重要的事:注意場景樹的結構,組節點是如何包含頂邊框/底邊框的. 選中新複製出的節點的Attributes Inspector屬性檢查器,在Geometry Sharing區下面,點選Unshare按鈕.
因為建立複本時,複製出的節點仍然會共享原始節點的幾何體(Geometry).這個預設設定是為了減少總的繪製呼叫(draw call)數.
左側邊框的建立
左右兩側的邊框分別由兩根圓柱組成.先在Barriers組下面建立一個Left節點,並放置到合適的位置.裡面的子節點也會跟著發生位置變動.
![WX20171111-115817@2x.png](https://i.iter01.com/images/60a0e2fd9fa82777fad148b34a39fb25275753d5d582006052df9ce9c26e1369.png)
![WX20171111-115849@2x.png](https://i.iter01.com/images/3c1a97628afe29f7d3ea2bfc28ccb8976c54e0f53de3ac7fceec6e2cfded556b.png)
建立左邊框的上半部分 拖放一個Cylinder,重新命名為Top,放置到Barriers/Left下面:
![WX20171111-120053@2x.png](https://i.iter01.com/images/3d04302accd654741997bb565e4558d9bf1aef91b6d76bbd5cedb2ce0f96c459.png)
![WX20171111-120123@2x.png](https://i.iter01.com/images/b275d2de2bc5fa163dabeaab6c5f406e391b4c74bbcfd3c589c7dd21ce527e16.png)
在節點檢查器中,設定Position為 (x:0, y:0.5, z:0),Euler為 (x:90, y:0, z:0).
屬性檢查器中,設定Radius為 0.3,Height 為 22.5.
材料檢查器中,設定Diffuse為Hex Color # 的B3B3B3 ,Specular為White:
![WX20171111-120335@2x.png](https://i.iter01.com/images/76b2de79e3a7b490699c01b2b6da95b22894f7d5a3a5e5ab3b10380447a824fa.png)
![WX20171111-120655@2x.png](https://i.iter01.com/images/93eedfffbbd7f0fa868faff81b2a57ea361fc4f13522dbce522c923986e5f465.png)
![WX20171111-120713@2x.png](https://i.iter01.com/images/12412f4440a03129d73a7ab0109be1168a0f1d306ff9fa8eddd8e1c99a9cff5f.png)
建立左邊框的下半部分 選中Barrier/Left/Top節點,按住Option鍵,沿藍色座標軸,點選拖動.重新命名為Bottom,放在Barriers/Left組下面.在節點檢查器中,設定Position為 (x:0,y:-0.5,z:0):
![WX20171111-125653@2x.png](https://i.iter01.com/images/edaecd006856b0ebf29786424cb0a0edefc021fdff79e647a9b56af57c3608fc.png)
![WX20171111-125915@2x.png](https://i.iter01.com/images/e24e85d73c61c87d16f4af47eb46501e9027fc69fc556d95bd3af8d24a9f3c30.png)
![WX20171111-125939@2x.png](https://i.iter01.com/images/f0e882c21587859bb7e8e3f2b9f237c89135c8f090da909207e5b18a2eb5ddd3.png)
最終效果如圖:
![WX20171111-125954@2x.png](https://i.iter01.com/images/f5cd456a6c7025046b86440168b1cbeaaa6e1b70fd5334eec8a681323dd4f996.png)
建立右側邊框
選中Barriers/Left組,按住Command+Option並沿紅色座標軸點選拖動,這樣就複製了一組節點.重新命名為Right,並設定位置為 (x:6, y:0, z:0)
![WX20171111-130404@2x.png](https://i.iter01.com/images/77b323c0da358a38122f2d370637abe1a7af98d4794be12cd3fb45b5b0a98770.png)
![WX20171111-130443@2x.png](https://i.iter01.com/images/d309b1ad2bcf4ac3107ba8c959a2e645ba9b0c94bdf3f9ccd6d8149834bb4c13.png)
![WX20171111-130454@2x.png](https://i.iter01.com/images/1a3c6127a2cabfb7d8b1d5336d64a23a2ed9ac88ae3b1f9635769337cbc7a7d3.png)
最終效果如圖:
![WX20171111-130609@2x.png](https://i.iter01.com/images/21ecf909eea97cf5c23ad96e8dac41ffdd7f52bea79dbd36ab9ded27cfa2c35d.png)
建立球拍擋板
點選 + 按鈕建立新的節點,命名為Paddle.開啟節點檢查器,設定Position為 (x:0, y:0, z:8).
![WX20171111-132831@2x.png](https://i.iter01.com/images/1fb881a031dbd85e50182a4527377f8fd3c66dc828840a8a1a433fc5192e4f41.png)
![WX20171111-132841@2x.png](https://i.iter01.com/images/a2a9b3f8cf755247263cb5450638d1e65c73e7fc3172c2c08b4192ce11b37397.png)
球拍擋板共有三個部分:左,中,右. 我們先建立中間部分,拖放一個圓柱體,命名為Center,放在Paddle組節點下面.
![WX20171111-133129@2x.png](https://i.iter01.com/images/9e09ba9e336f590a08c4a19df2868715f89d183cd8d66751b9b88b23f8bb2a86.png)
![WX20171111-133141@2x.png](https://i.iter01.com/images/a3d965674331c416041b3d149f7bed4d250f2d123c3edc4ca04bd976c41fb0dc.png)
開啟節點檢查器,設定Position為0,設定Euler為 (x:0, y:0, z:90).
開啟屬性檢查器,設定Radius為0.25, Height為1.5.
開啟材料檢查器,設定Diffuse為Hex Color # 的333333, Specular為White.
![WX20171111-133213@2x.png](https://i.iter01.com/images/38d2842e45c966dac2112d96f6f5a578dd175448d824904a9a6843baf90ef23f.png)
![WX20171111-133225@2x.png](https://i.iter01.com/images/7b2c4db74c4a5e1a43f9c7fe65168b8fb3f2e184c3e94386b8e1a2d7b0beca7a.png)
![WX20171111-133239@2x.png](https://i.iter01.com/images/936d7ef4b1933ceddf7491e3a5e53811630a003c97f8c75b66b64e5bb2baa4fd.png)
建立左側部分
拖放一個圓柱體,命名為Left,放在Paddle組節點下面.
![WX20171111-133904@2x.png](https://i.iter01.com/images/566f4344926a2d7c4a8dc988f6efdd650b005a9e4b735425d38dbae26c568598.png)
設定Position為**(x:-1, y:0, z:0)**, Euler為 (x:0, y:0, z:90).
開啟屬性檢查器,設定Radius為0.25, Height為0.5.
開啟材料檢查器,設定Diffuse為Hex Color # 的666666, Specular為White.
![WX20171111-134208@2x.png](https://i.iter01.com/images/b0a1c02a38e6c6dc83065a9c5b1654576a7d8c71f035dd6e9c5117ac4fed036d.png)
![WX20171111-134218@2x.png](https://i.iter01.com/images/6a5993c70eadfdeefbbb267ed61d6de1d51329a5ec3428c9c01b28d13b791984.png)
![WX20171111-134235@2x.png](https://i.iter01.com/images/a50d890c1d20aaf0f6cbb6fb3d5fd8de1ca9652d52d408383c01324f6db34fc4.png)
複製右側部分 選中Paddle/Left節點,按住Command+Option並沿綠色座標軸點選拖動,這樣就複製了一組節點.重新命名為Right,並設定位置為**(x:1, y:0, z:0)**.還是要注意取消幾何體共享.
![WX20171111-141015@2x.png](https://i.iter01.com/images/b94d85e5c36f399ffc7295d94bab799141461699661bae1e5d1c2c7a2b693832.png)
![WX20171111-141028@2x.png](https://i.iter01.com/images/674024d55354a2bc2d0e02ac06a04997220711fb39c94b3c5252a308dc07e424.png)
繫結球拍擋板,以便操作
開啟GameViewController.swift,新增屬性:
var paddleNode: SCNNode!
複製程式碼
在setupNodes()
方法的末尾,新增繫結球拍的程式碼:
paddleNode =
scnScene.rootNode.childNode(withName: "Paddle", recursively: true)!
複製程式碼
你可以在本章對應程式碼的projects/final/Breaker資料夾下,找到最終的完成版專案.
新增磚塊,挑戰專案
-
首先,建立一個組節點命名為Bricks,用來放置所有的磚塊.
-
設定Bricks節點的位置為 (x:0, y:0, z:-3.0).
-
每個磚塊都是使用一個Box,尺寸為width:1, height:0.5, length: 0.5,Chamfer Radius:0.05.
-
先建立一列各種顏色的磚塊,顏色分別使用white (#FFFFFF), red (#FF0000), yellow (#FFFF00), blue (#0000FF), purple (#8000FF), green (#00FF80):
-
為了方便定位,白色磚塊可以放置在(x: 0, y:0, z:-2.5),綠色磚塊應該在(x:0, y:0, z:0).
-
將磚塊用自己的顏色命名.
-
複製更多列出來.(按住Option和Command)
-
複製時,記得使用材料檢查器下面的Unshare按鈕,以免改變了原始節點的顏色.
-
複製填滿整個區域.
最終效果如圖:
![WX20171111-142642@2x.png](https://i.iter01.com/images/66edb8fb6b44d299f109ffdd9499104110eff9fd580817d3795f664394c07f7a.png)
執行程式
![WX20171111-142655@2x.png](https://i.iter01.com/images/3a73e9872162b4b66650f1af493ff964e677aa10664a885a698e02ff7b10f579.png)
你可以在本章對應程式碼的projects/challenge/Breaker資料夾下,找到最終的完成版專案.
10-Basic Collision Detection碰撞檢測基礎
物理效果
先給小球新增物理效果. 開啟Game.scn並選中Ball.開啟Physics Inspector物理效果檢查器.將Physics Body的Type改為Dynamic. 並按下圖設定各個專案:
![WX20171111-143239@2x.png](https://i.iter01.com/images/85447badaf998b1d3ff0c5145821805dc03735526092969d6067cd9c0e23583a.png)
給邊框新增物理效果 一次性選中左右邊框的四個部分,可以有兩種方法:
- 按住Command在場景樹中點選每個節點.
- 類似於資料夾多選操作,先選中Top節點,按住Shift,點選Right,兩者之間的節點會被全部選中.
保持選中狀態,開啟物理效果檢查器,在Physics Body區域,將Type改為Static,在新展開的設定項裡按下圖設定:
![WX20171111-143930@2x.png](https://i.iter01.com/images/d2c57dd50a53f80f40ba22eece1563e842effe5bd40a93ffdcaaa65331f2802e.png)
點選工具條上的播放按鈕,就可以預覽物理效果:
![WX20171111-144621@2x.png](https://i.iter01.com/images/98b2150856ecea776b0f01d2dec41c360be2f17aded38db26b95de91b3fa4a90.png)
接著給磚塊新增物理效果 全選磚塊節點:
![WX20171111-144805@2x.png](https://i.iter01.com/images/a119cc5ee18b3267af1f259460b1aace5246dc1a33957b0ac03d4ea8899c671a.png)
設定為Static形體,其餘如下圖:
![WX20171111-144821@2x.png](https://i.iter01.com/images/712ee1d2196dcc9e0cd9ba2f8e3724000b23ba7fbe8163705630e69c037f5b68.png)
給球拍擋板新增物理效果 選中球拍三個節點,開啟物理效果檢查器,設定Type為Kinematic,其餘專案設定如下:
![WX20171111-150415@2x.png](https://i.iter01.com/images/d4dfc40934ca3b3e8294f5225611d256884b19b36c13427c563ee8d254eac1d6.png)
![WX20171111-150430@2x.png](https://i.iter01.com/images/77d8d31d78832816e6cf339217a9134f2b8b96d68893ff05b5bf525d1fd858b4.png)
執行一下,小球會瘋狂地到處碰撞,包括與球拍的碰撞:
![WX20171111-151240@2x.png](https://i.iter01.com/images/41afba26b2b576171d65e1d55f905053d54cdbbfc560d0b86907d60de69a9b15.png)
碰撞檢測
碰撞檢測用到的是SCNPhysicsContactDelegate協議. 開啟GameViewController.swift,新增一個新屬性:
var lastContactNode: SCNNode!
複製程式碼
它的作用有兩個:
- 當兩個節點發生互相滑動時,就相當於和同一個節點不停發生碰撞,而我們只關心第一次碰撞.
- 在這個遊戲中,儘管碰撞可能會持續,但小球不能和同一個節點兩次發生接觸事件,直到小球碰到了其它節點.所以我們需要確保只處理一次碰撞.
在GameViewController.swift底部新增類擴充套件:
// 1
extension GameViewController: SCNPhysicsContactDelegate {
// 2
func physicsWorld(_ world: SCNPhysicsWorld,
didBegin contact: SCNPhysicsContact) {
// 3
var contactNode: SCNNode!
if contact.nodeA.name == "Ball" {
contactNode = contact.nodeB
} else {
contactNode = contact.nodeA
}
// 4
if lastContactNode != nil &&
lastContactNode == contactNode {
return
}
lastContactNode = contactNode
}
}
複製程式碼
程式碼含義:
- 擴充套件
GameViewController
類以實現SCNPhysicsContactDelegate
協議,方便組織程式碼. - 實現
physicsWorld(_:didBegin:)
.預設不觸發,需要設定接觸掩碼. - 傳入一個
SCNPhysicsContact
引數,可以判斷並找到哪個是小球. - 防止和同一個節點多次碰撞.
使用位掩碼來檢測接觸事件. 我們已經給遊戲中的不同元素設定了Category bitmask分類掩碼,這個值是二進位制的,各分類如下:
Ball: 1 (Decimal) = 00000001 (Binary)
Barrier: 2 (Decimal) = 00000010 (Binary)
Brick: 4 (Decimal) = 00000100 (Binary)
Paddle: 8 (Decimal) = 00001000 (Binary)
複製程式碼
在GameViewController
頂部定義一個列舉:
enum ColliderType: Int {
case ball = 0b0001
case barrier = 0b0010
case brick = 0b0100
case paddle = 0b1000
}
複製程式碼
在setupNodes()
方法的末尾新增下面程式碼來處理碰撞:
ballNode.physicsBody?.contactTestBitMask =
ColliderType.barrier.rawValue |
ColliderType.brick.rawValue |
ColliderType.paddle.rawValue
複製程式碼
這樣,你就告訴了物理引擎,當小球和分類掩碼為2, 4, 8的節點碰撞時,呼叫physicsWorld(_:didBegin:)
方法通知我. 2,4,8也就是指barrier邊框, brick磚塊和paddle球拍.
在physicsWorld(_:didBegin:)
方法的末尾繼續寫:
// 1
if contactNode.physicsBody?.categoryBitMask ==
ColliderType.barrier.rawValue {
if contactNode.name == "Bottom" {
game.lives -= 1
if game.lives == 0 {
game.saveState()
game.reset()
}
} }
// 2
if contactNode.physicsBody?.categoryBitMask ==
ColliderType.brick.rawValue {
game.score += 1
contactNode.isHidden = true
contactNode.runAction(
SCNAction.waitForDurationThenRunBlock(duration: 120) {
(node:SCNNode!) -> Void in
node.isHidden = false
})
}
// 3
if contactNode.physicsBody?.categoryBitMask ==
ColliderType.paddle.rawValue {
if contactNode.name == "Left" {
ballNode.physicsBody!.velocity.xzAngle -=
(convertToRadians(angle: 20))
}
if contactNode.name == "Right" {
ballNode.physicsBody!.velocity.xzAngle +=
(convertToRadians(angle: 20))
}
}
// 4
ballNode.physicsBody?.velocity.length = 5.0
複製程式碼
程式碼含義:
- 檢查
categoryBitMask
來判斷小球是不是和邊框節點碰撞了.再根據名字判斷,如果是和底部邊框碰撞,則需要扣掉一個生命值. - 檢查並判斷小球是不是和磚塊碰撞了.讓對應磚塊消失120秒,再皇親出現,這樣遊戲就能一直玩下去.
- 判斷小球是不是和球拍碰撞了.如果遇到了中間部分,不改變物理效果,由引擎自動控制反彈.如果是碰到了左邊或右邊,則給小球增加一個
20
度的水平偏轉. - 將小球速度強制限制在5,以防物理引擎出現偏差而失控.
還要記得成為接觸代理.在setupScene()
底部新增一行:
scnScene.physicsWorld.contactDelegate = self
複製程式碼
執行一下,可以打掉磚塊了!
![WX20171111-160202@2x.png](https://i.iter01.com/images/4b889992ce2cfb70bde1ba573418d61bbb3cda21915e8428bba6bab8847ecfdd.png)
觸控控制球拍
給GameViewController
新增兩個屬性:
var touchX: CGFloat = 0
var paddleX: Float = 0
複製程式碼
下一步,給GameViewController
新增下面的方法:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
for touch in touches {
let location = touch.location(in: scnView)
touchX = location.x
paddleX = paddleNode.position.x
}
}
複製程式碼
記錄下觸控的初始位置,球拍的初始位置
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
for touch in touches {
// 1
let location = touch.location(in: scnView)
paddleNode.position.x = paddleX +
(Float(location.x - touchX) * 0.1)
// 2
if paddleNode.position.x > 4.5 {
paddleNode.position.x = 4.5
} else if paddleNode.position.x < -4.5 {
paddleNode.position.x = -4.5
}
}
}
複製程式碼
程式碼含義:
- 當觸控位置移動時,根據相對初始觸控位置的偏移
touchX
來更新球拍的位置. - 限制球拍的移動,確保在邊框之間.
執行一下,可以來回移動球拍了:
![WX20171111-163506@2x.png](https://i.iter01.com/images/70248c5fb97cd251246b2a1465693bfbb8af52e21acc797d4768d23640184ea5.png)
攝像機追蹤
在touchesMoved(_:with:)
方法的底部,新增下面程式碼,讓攝像機水平位置和球拍一致:
verticalCameraNode.position.x = paddleNode.position.x
horizontalCameraNode.position.x = paddleNode.position.x
複製程式碼
在GameViewController
中新增一個新屬性來依舊在地板節點:
var floorNode: SCNNode!
複製程式碼
在setupNodes()
底部新增程式碼:
floorNode =
scnScene.rootNode.childNode(withName: "Floor",
recursively: true)!
verticalCameraNode.constraints =
[SCNLookAtConstraint(target: floorNode)]
horizontalCameraNode.constraints =
[SCNLookAtConstraint(target: floorNode)]
複製程式碼
這段程式碼含義:找到名為Floor的節點,繫結到floorNode
.給場景中的兩個攝像機新增SCNLookAtConstraint
約束,能讓攝像機始終對準目標節點,也就是遊戲區域的中央.
可以執行試玩一下了:
![WX20171111-164815@2x.png](https://i.iter01.com/images/e3d2531f5b4e2fad8deb440a05d10fd5beedfafeb514fddeff376c030a09c355.png)
粒子效果
選中場景Game.scn.從物件庫中拖放一個Particle System粒子系統到場景中,命名為Trail,並放在Ball節點中
![WX20171111-165921@2x.png](https://i.iter01.com/images/8cfac1e40e48488fbfe0e35a29db122baea85e6910c06dca25eb91f43f951a34.png)
![WX20171111-165743@2x.png](https://i.iter01.com/images/696bd50203a96e98129c21f75006d797347e7482f676c3fe310cae3f353b13e9.png)
開啟節點檢查器,設定position為 (x:0, y:0, z:0).
![WX20171111-170628@2x.png](https://i.iter01.com/images/384f2f16a97d106cd4db5529c1d74fbf9cbe871ecfea308d7ca89cdb75449818.png)
開啟屬性檢查器,配置粒子系統的屬性:
![WX20171111-171334@2x.png](https://i.iter01.com/images/3bf042d7cde11f02df1b312f3260c0fd87a2af68f4b4c62b682a44bcf77a37fd.png)
完成後,點選播放按鈕預覽一下:
![WX20171111-171439@2x.png](https://i.iter01.com/images/6d248ec595d46e6ec93dd36bf7c5c9e38364f10f92c5c85c25221f923cbb2355.png)
正式執行一下,可以玩起來了!
![WX20171111-171501@2x.png](https://i.iter01.com/images/0d9a9bd33039e437bc4b63b1141d6f10d8ac8b06efff59a1137a2d7d436dddc6.png)
該部分最終完成的專案,放在程式碼中對應章節的projects/final/Breaker資料夾裡.
新增聲音效果
新增setupSounds()
方法,並新增程式碼:
game.loadSound(name: "Paddle",
fileNamed: "Breaker.scnassets/Sounds/Paddle.wav")
game.loadSound(name: "Block0",
fileNamed: "Breaker.scnassets/Sounds/Block0.wav")
game.loadSound(name: "Block1",
fileNamed: "Breaker.scnassets/Sounds/Block1.wav")
game.loadSound(name: "Block2",
fileNamed: "Breaker.scnassets/Sounds/Block2.wav")
game.loadSound(name: "Barrier",
fileNamed: "Breaker.scnassets/Sounds/Barrier.wav")
複製程式碼
可以在碰撞的時候,播放對應的音效:
- 使用
game.playSound(node: scnScene.rootNode, name: "SoundToPlay")
來播放已載入好的音效. - 給Block新增音效時使用隨機值,用
random() % 3
來產生0~2的隨機數.
最終完成的專案,放在程式碼中對應章節的projects/challenge/Breaker資料夾裡.