黑森林的樣貌正如其名,不管白天或黑夜,一但走進了森林裡就伸手不見五指...
長老說:「少年,你確定要去嗎?」
山姆堅定地說:「是的。」
「因為,這是身為探險家的必經之路啊!」
山姆背對著長老,揮了揮手,陽光灑落在他的帽簷上,特別耀眼。
首先,我們先創建一個遊戲模板的專案
選擇 Game 模板
給專案一個很酷的名稱
選擇好要創建的位置,點擊 Create 就創建完成囉!
清除 GameScene.sks 中的 helloLabel
刪除 Action.sks
GameScene.swift
把不需要的程式碼刪除,只留下這些:
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
}
}
view.showsFPS = false
view.showsNodeCount = false
我們的目標是要做出可以讓角色在迷宮內移動的地圖,因此採用格子的方式來拼出迷宮,每個格子可以代表牆壁或是路,牆壁會放上不同種類的牆壁貼圖,而路則可以讓角色移動。
首先先決定要繪製的迷宮格子尺寸,我們將繪製:寬:17 格、高:23 格的地圖。
先把地圖的樣子寫出來吧!
使用陣列,畫出 17 x 23 樣子的地圖,並且用符號表示:
(今天先處理畫出牆壁的部分,其他後續的單元會說明)
除了 w (牆壁) 以外,都可以讓角色行走
而選擇使用一維陣列,主要是想讓程式碼看起來跟迷宮比較相近、簡潔
let map = [ "wwwwwwwwwwwwwwwww",
" .....w*......w",
"www.www.w.ww.ww.w",
" w.www....w.ww.w",
"www.....ww.w....w",
"w*...ww.ww...ww.w",
"w.ww.ww....w....w",
"w..w.ww.ww.w.wwww",
"ww... .w ",
"w..w.www www.w ",
"w.ww.w w.wwww",
"w+...wwwwwww. ",
"wwww. .ww.w",
" w.ww.w#.w....w",
" w....ww.w.ww.w",
"wwww.ww....w.ww.w",
" .ww.ww.w....w",
"wwww....w....ww.w",
" w...ww.ww.w..w",
" w.w.........ww",
"wwww.w.www.w.w.ww",
"w*.............*w",
"wwwwwwwwwwwwwwwww",
]
用表格表示可能會清楚些!以下是地圖內所有的格子點
橫的列,未來會定義 gridX 來表示 (範圍0~16)
直的欄,未來會定義 gridY 來表示 (範圍0~22)
透過 gridX 及 gridY 來記錄角色或當前物體的所在位置
-|00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16
-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
00| | | | | | | | | | | | | | | | |
01| | | | | | | | | | | | | | | | |
02| | | | | | | | | | | | | | | | |
03| | | | | | | | | | | | | | | | |
04| | | | | | | | | | | | | | | | |
05| | | | | | | | | | | | | | | | |
06| | | | | | | | | | | | | | | | |
07| | | | | | | | | | | | | | | | |
08| | | | | | | | | | | | | | | | |
09| | | | | | | | | | | | | | | | |
10| | | | | | | | | | | | | | | | |
11| | | | | | | | | | | | | | | | |
12| | | | | | | | | | | | | | | | |
13| | | | | | | | | | | | | | | | |
14| | | | | | | | | | | | | | | | |
15| | | | | | | | | | | | | | | | |
16| | | | | | | | | | | | | | | | |
17| | | | | | | | | | | | | | | | |
18| | | | | | | | | | | | | | | | |
19| | | | | | | | | | | | | | | | |
20| | | | | | | | | | | | | | | | |
21| | | | | | | | | | | | | | | | |
22| | | | | | | | | | | | | | | | |
先將 GameScene 的 anchorPoint 調整至左上角 (0, 1)
有兩種方式:
可以在 GameScene.sks 的屬性中修改
可以在 GameViewController.swift 中加入
...
if let scene = SKScene(fileNamed: "GameScene") {
...
scene.anchorPoint = CGPoint(x: 0, y: 1)
...
}
接著來寫 GameScene 類別的內容。
設定好 gridXCount
及 gridYCount
常數值。
計算格子的長度及寬度 gridWH
為 螢幕寬度/寬17格。
我們預計會加上許多格子 (格子為正方形),因此可以先建立一個父節點 mapNode
,準備將這些格子加進父節點中。請先設定好它的寬跟高,並將 anchorPoint 定在左上角,加到場景裡。
class GameScene: SKScene {
var mapNode: SKSpriteNode?
let gridXCount = 17
let gridYCount = 23
var gridWH = 0
override func didMove(to view: SKView) {
self.backgroundColor = .black
self.gridWH = Int(self.size.width) / gridXCount
self.mapNode = SKSpriteNode(color: .black, size: CGSize(width: CGFloat(self.size.width), height: CGFloat(self.gridWH * gridYCount)))
self.mapNode!.anchorPoint = CGPoint(x: 0, y: 1)
self.addChild(self.mapNode!)
}
}
接著寫繪製地圖的方法 drawMap
:
for 迴圈的第一層先跑Y軸,先取得列,接著將字串用 Array(map[i])
轉換成陣列,再依序取得字元。
我們先對 case "w"
貼上一張樹的貼圖,並設定寬高皆為 gridWH
。
因其 anchorPoint 為 (0.5, 0.5),而父節點的 anchorPoint 為 (0, 1),因此設定 position 的 x 應該為正數,而 y 為負數。除了加/減自己的長度乘以陣列位置的 index,還需加/減回自身長度的一半。
最後加進 mapNode
中。
附註:圖片檔案需拖拉至左側檔案列表的 Assets.xcassets 裡。
func drawMap() {
for i in 0..<gridYCount {
let mapRowArr = Array(map[i])
for j in 0..<gridXCount {
switch mapRowArr[j] {
case "w":
let spriteItem = SKSpriteNode(imageNamed: "tree-green")
spriteItem.anchorPoint = CGPoint(x: 0.5, y: 0.5)
spriteItem.size.width = CGFloat(gridWH)
spriteItem.size.height = CGFloat(gridWH)
spriteItem.position = CGPoint(x: gridWH * j + (gridWH/2), y: -gridWH * i - (gridWH/2))
self.mapNode!.addChild(spriteItem)
default:
break
}
}
}
}
最後呼叫繪製地圖的方法:
override func didMove(to view: SKView) {
...
self.drawMap()
}
運行模擬器後可以看到以下成果:
我們來觀察用不同模擬器開啟的結果
首先,先解決畫面超出螢幕的問題,原因在於手機的尺寸不同
我們印出場景的 size
打開 GameScene.sks,發現場景的尺寸是 750x1334
由於 GameViewController.swift 中有設定 scaleMode
,所以畫面會縮放,而 iPhone 8 剛好比例與 GameScene.sks 設定的比例一樣,所以看起來沒有跑版。而 iPhone 11 比例不一樣,因此跑版了。
scene.scaleMode = .aspectFill
我們將這一行刪除,iPhone 11 畫面就可以正常呈現了。
iOS11 之後,多了 Safe Area 功能,他的範圍能避開可能會被螢幕切掉或擋住的部分
我們來觀察 Main.storyboard View 跟 Safe Area 的差異,Safe Area 比較符合我們想要呈現的方式
試著在 GameViewController.swift 的 viewDidLoad
中取出 Safe Area 的 size、top、bottom
override func viewDidLoad() {
print("size: \(view.safeAreaLayoutGuide.layoutFrame.size)")
print("top: \(view.safeAreaInsets.top)")
print("bottom: \(view.safeAreaInsets.bottom)")
}
在iPhone 11模擬器中印出:
size: (414.0, 896.0)
top: 0.0
bottom: 0.0
發現取出來的數值不是我們預期的結果,原因在於 viewDidLoad
的時間點太早了,還取不到真正的 Safe Area 資訊。覆寫另一個 viewSafeAreaInsetsDidChange
試試看:
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
print("size: \(view.safeAreaLayoutGuide.layoutFrame.size)")
print("top: \(view.safeAreaInsets.top)")
print("bottom: \(view.safeAreaInsets.bottom)")
}
在iPhone 11模擬器中印出:
size: (414.0, 818.0)
top: 44.0
bottom: 34.0
成功取得 Safe Area 的資訊了!
查看官網的說明,這個方法能在 Safe Area 改變時通知 view controller,因此我們必須在這個時間點調整節點的位置。
applySafeArea
方法,把 mapNode
的位置往下校正回來(topSafeArea)class GameScene: SKScene {
var topSafeArea: CGFloat = 0
var bottomSafeArea: CGFloat = 0
...
func applySafeArea() {
if #available(iOS 11.0, *) {
if let view = self.view {
self.topSafeArea = view.safeAreaInsets.top
self.bottomSafeArea = view.safeAreaInsets.bottom
}
}
if let mapNode = self.mapNode {
mapNode.position = CGPoint(x: 0, y: -self.topSafeArea)
}
}
}
viewDidLoad
方法,在這邊我們一樣呈現 GameScene,並把 gameScene 儲存起來。viewSafeAreaInsetsDidChange
方法時,判斷確定取得到 gameScene 後,呼叫 applySafeArea
方法class GameViewController: UIViewController {
var gameScene: GameScene?
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
if let scene = self.gameScene {
scene.applySafeArea()
}
}
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as? SKView {
if let scene = SKScene(fileNamed: "GameScene") {
self.gameScene = scene as? GameScene
...
}
...
}
}
}
請特別注意!不能把原本寫在
viewDidLoad
的程式整個搬進viewSafeAreaInsetsDidChange
。viewDidLoad
在生命週期裡只會呼叫一次,而viewSafeAreaInsetsDidChange
則是當 Safe Area 變動時就會再次呼叫,像是旋轉手機,就會發生改變。若是整個程式都搬進去,當旋轉手機時,遊戲就會重來囉!
成功繪製完迷宮了!
明天來帶大家美化迷宮~
請問一下
override func didMove(to view: SKView) {
self.backgroundColor = .black
self.gridWH = Int(self.size.width) / gridXCount
self.mapNode = SKSpriteNode(color: .black, size: CGSize(width: CGFloat(self.size.width), height: CGFloat(self.cubeWH * CubeYCount)))
self.mapNode!.anchorPoint = CGPoint(x: 0, y: 1)
self.addChild(self.mapNode!)
}
之中的CubeYCount 是指什麼呢
不好意思,因為有調整過命名,文章沒有更新到。
是 gridYCount
,表示地圖格子高有 23 格
self.mapNode = SKSpriteNode(color: .black, size: CGSize(width: CGFloat(self.size.width), height: CGFloat(self.gridWH * gridYCount)))