寫在前面:這個(gè)系列文章是轉(zhuǎn)載過來的,簡(jiǎn)書里之前也有人轉(zhuǎn)載了驰吓,不過沒有進(jìn)行重新編排哪亿,圖文等格式并不適用于簡(jiǎn)書,我參照原文樣式重新排版了一次菱魔。
- 原文地址:How To Build A SpriteKit Game In Swift 3 (Part 1)
- 原文作者:Marc Vandehey
- 譯文出自:掘金翻譯計(jì)劃
- 譯文地址:如何在 Swift 3 中用 SpriteKit 框架編寫游戲 (Part 1)
- 譯者:Gocy
- 校對(duì)者:Tuccuay, DeepMissea
你有沒有想過要如何開始創(chuàng)作一款基于 SpriteKit 的游戲?開發(fā)一款基于真實(shí)物理規(guī)則的游戲是不是讓你望而生畏吟孙?隨著 SpriteKit 的出現(xiàn)澜倦,在 iOS 上開發(fā)游戲已經(jīng)變得空前的簡(jiǎn)單了。
本系列將分為三個(gè)部分杰妓,帶你探索 SpriteKit 的基礎(chǔ)知識(shí)藻治。我們會(huì)接觸到物理引擎( SKPhysics
)、碰撞巷挥、紋理管理桩卵、互動(dòng)、音效、音樂雏节、按鈕以及場(chǎng)景( SKScene
) 胜嗓。這些看上去艱深晦澀的東西其實(shí)非常容易掌握。趕緊跟著我們一起開始編寫 RainCat 吧钩乍。
RainCat辞州,第一課
我們將要實(shí)現(xiàn)的這個(gè)游戲有一個(gè)簡(jiǎn)單的前提:我們想喂飽一只饑腸轆轆的貓,但它現(xiàn)在正孤身地站在雨中寥粹。不巧地是变过,RainCat 并不喜歡下雨天,而它被淋濕之后就會(huì)覺得很難過涝涤。為了讓它能在大吃的時(shí)候不被雨水淋到媚狰,我們必須要替它撐把傘。想先體驗(yàn)一下我們的目標(biāo)成果的話阔拳,看看 完整項(xiàng)目 吧崭孤。項(xiàng)目中會(huì)有一些文章里不會(huì)涉及到的進(jìn)階內(nèi)容,但你可以稍后在 GitHub 上面看到這些內(nèi)容衫生。本系列的目標(biāo)是讓你深刻地理解做一個(gè)簡(jiǎn)單地游戲需要投入些什么裳瘪。你可以隨時(shí)與我們聯(lián)系,并把這些代碼作為將來其它項(xiàng)目的參考罪针。我將會(huì)持續(xù)更新代碼庫彭羹,添加一些有趣的新功能并對(duì)一些部分進(jìn)行重構(gòu)。
入門
接下來有幾件事需要你跟著完成泪酱。為了讓你輕松起步派殷,我準(zhǔn)備好了一個(gè)基礎(chǔ)工程。這個(gè)工程把 Xcode 8 在創(chuàng)建新的 SpriteKit 工程時(shí)聯(lián)帶生成的冗余代碼都刪的一干二凈了墓阀。
- 從 這里 下載 RainCat 游戲工程的基礎(chǔ)代碼毡惜。
- 安裝 Xcode 8。
- 找一臺(tái)測(cè)試機(jī)器斯撮!在本例中经伙,你應(yīng)該找一臺(tái) iPad ,這樣可以避免做復(fù)雜的屏幕適配勿锅。模擬器也是可以的帕膜,但是操作上會(huì)有延遲,而且比在真實(shí)設(shè)備上的幀數(shù)低不少溢十。
查看工程代碼
我已經(jīng)幫你起了個(gè)好頭了垮刹,創(chuàng)建好了 RainCat 工程,還做了一些初始化的工作张弛。打開這個(gè) Xcode 工程』牡洌現(xiàn)在酪劫,項(xiàng)目看起來還非常的簡(jiǎn)單基礎(chǔ)。我們先梳理一下現(xiàn)在的情況:我們創(chuàng)建了一個(gè)工程寺董,指定運(yùn)行系統(tǒng)為 iOS 10覆糟,運(yùn)行設(shè)備為 iPad ,并且只支持設(shè)備的水平方向螃征。如果我們要在較舊的設(shè)備上進(jìn)行測(cè)試搪桂,我們也可以把系統(tǒng)版本設(shè)定為更早的版本,Swift 3 至多支持到 iOS 8 盯滚。當(dāng)然踢械,讓你的應(yīng)用支持起碼比最新版本要早一個(gè)版本的系統(tǒng)也是一個(gè)很好的實(shí)踐。不過需要注意:本教程內(nèi)容僅針對(duì) iOS 10 魄藕,如果你要支持更早的版本的話内列,可能會(huì)出現(xiàn)一些問題。
決定利用 Swift 3 來實(shí)現(xiàn)這個(gè)游戲的原因: iOS 開發(fā)者社區(qū)非常積極地參與到了 Swift 3 的發(fā)布過程中背率,帶來了許多編碼風(fēng)格上的變化和全方位的升級(jí)话瞧。由于新版本的 iOS 系統(tǒng)在 Apple 用戶群體中覆蓋速率快、面積廣寝姿,我們認(rèn)為交排,使用最新發(fā)布的 Swift 版本來編寫這篇教程是最合適的。
在 GameViewController.swift
中有一個(gè)標(biāo)準(zhǔn)的 UIViewController 子類 饵筑,我們修改了一些初始化 GameScene.swift
中的 SKScene 的代碼埃篓。在做這些改動(dòng)之前,我們會(huì)通過一個(gè) SpriteKit 場(chǎng)景編輯器文件( SpriteKit scene editor (SKS) file )來讀取 GameScene
類根资。在本教程中架专,我們將直接讀取這個(gè)場(chǎng)景,而不是使用更復(fù)雜的 SKS 文件玄帕。如果你想更深入地了解 SKS 文件的相關(guān)知識(shí)部脚, Ray Wenderlich 有一篇 極佳的文章 。
獲取資源文件
在我們寫代碼之前裤纹,要先獲取項(xiàng)目中會(huì)用到的資源委刘。今天我們會(huì)用到雨傘和雨滴。你可以在 GitHub 上找到這些 紋理 鹰椒。將它們添加到 Xcode 左部面板的 Assets.xcassets
文件夾中锡移。當(dāng)你點(diǎn)擊 Assets.xcassets
文件,你會(huì)見到一個(gè)帶有 AppIcon
占位符的空白界面吹零。在 Finder 中選中所有(解壓的資源文件)罩抗,并把它們都拖到 AppIcon
占位符的下面拉庵。如果你正確進(jìn)行了上述操作灿椅,你的 “Assets” 文件看起來應(yīng)該是這樣:
雖然你不能從白色的背景上分辨出白色的傘尖,但我保證,它是在那兒的茫蛹。
是時(shí)候動(dòng)手編碼了
現(xiàn)在我們已經(jīng)做足了各項(xiàng)準(zhǔn)備工作操刀,我們可以開始動(dòng)手開發(fā)游戲啦。
我們首先要做出個(gè)地面婴洼,好騰出地方來遛貓和喂貓骨坑。由于背景和地面都非常的簡(jiǎn)單,我們可以把這些精靈( sprite )放到一個(gè)自定義的背景結(jié)點(diǎn)( node )中柬采。在 Xcode 左部面板的 “Sprites” 文件夾下欢唾,創(chuàng)建名為 BackgroundNode.swift
的 Swift 源文件,并添加以下代碼:
import SpriteKit
public class BackgroundNode: SKNode {
public func setup(size: CGSize) {
let yPos: CGFloat = size.height * 0.10
let startPoint = CGPoint(x: 0, y: yPos)
let endPoint = CGPoint(x: size.width, y: yPos)
physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint)
physicsBody?.restitution = 0.3
}
}
上面的代碼引用了 SpriteKit 框架粉捻。這是 Apple 官方的用于開發(fā)游戲的資源庫礁遣。在我們接下來新建的大部分源文件中,我們都會(huì)用到它肩刃。我們創(chuàng)建的這個(gè)對(duì)象是一個(gè) SKNode 實(shí)例祟霍,我們會(huì)把它作為背景元素的容器。目前盈包,我們僅僅是在調(diào)用 setup(size:)
方法的時(shí)候?yàn)槠涮砑恿艘粋€(gè) SKPhysicsBody 實(shí)例沸呐。這個(gè)物理實(shí)體( physics body )會(huì)告訴我們的場(chǎng)景( scene ),其定義的區(qū)域(目前只有一條線)呢燥,能夠和其它的物理實(shí)體和 物理世界( physics world ) 進(jìn)行交互崭添。我們還改變了 restitution
的值。這個(gè)屬性決定了地面的彈性疮茄。想讓這個(gè)對(duì)象為我們所用滥朱,我們需要把它加入 GameScene
中。切換到 GameScene.swift
文件中力试,在靠近頂部徙邻,一串 TimeInterval
變量的下面,添加如下代碼:
private let backgroundNode = BackgroundNode()
然后畸裳,在 sceneDidLoad()
方法中缰犁,我們可以初始化背景,并將其加入場(chǎng)景中:
backgroundNode.setup(size: size)
addChild(backgroundNode)
現(xiàn)在怖糊,如果我們運(yùn)行程序帅容,我們將會(huì)看到如圖的游戲場(chǎng)景:
我們的略微空曠的場(chǎng)景。
如果你沒看見那條線伍伤,那說明你在將結(jié)點(diǎn)( node )加入場(chǎng)景時(shí)出現(xiàn)了錯(cuò)誤并徘,要么就是場(chǎng)景現(xiàn)在不顯示物理實(shí)體。要控制這些選項(xiàng)的開關(guān)扰魂,只需要在 GameViewController.swift
中修改下列選項(xiàng)即可:
if let view = self.view as! SKView? {
view.presentScene(sceneNode)
view.ignoresSiblingOrder = true
view.showsPhysics = true
view.showsFPS = true
view.showsNodeCount = true
}
現(xiàn)在麦乞,確保 showsPhysics
屬性被設(shè)為 true
蕴茴。這有助于我們調(diào)試物理實(shí)體。盡管眼下并沒有什么值得特別關(guān)注的地方姐直,但這個(gè)背景將會(huì)充當(dāng)雨滴下落反彈時(shí)的地面倦淀,也會(huì)作為貓咪行走時(shí)的邊界。
接下來声畏,我們來添加一些雨水撞叽。
如果我們?cè)诎延甑渭尤雸?chǎng)景之前思考一下,就會(huì)明白在這兒我們需要一個(gè)可復(fù)用的方法來原子性地添加雨滴插龄。雨滴元素將由一個(gè) SKSpriteNode
和另外一個(gè)物理實(shí)體構(gòu)成愿棋。你可以用一張圖片或是一塊紋理來實(shí)例化一個(gè) SKSpriteNode
對(duì)象。明白了這點(diǎn)均牢,并且想到我們應(yīng)該會(huì)添加許多的雨滴初斑,我們就知道自己應(yīng)該做一些復(fù)用了。有了這個(gè)想法膨处,我們就可以復(fù)用紋理见秤,而不必每次創(chuàng)建雨滴元素時(shí)都創(chuàng)建新的紋理了。
在 GameScene.swift
文件的頂部真椿,實(shí)例化 backgroundNode
的前面鹃答,加入下面這行代碼:
let raindropTexture = SKTexture(imageNamed: "rain_drop")
現(xiàn)在我們就可以在創(chuàng)建雨滴時(shí)進(jìn)行復(fù)用,而不需要在每次都浪費(fèi)內(nèi)存來生成一份新的紋理了突硝。
接著测摔,在 GameScene.swift
的底部,加入下述代碼解恰,以便我們方便的創(chuàng)建雨滴:
private func spawnRaindrop() {
let raindrop = SKSpriteNode(texture: raindropTexture)
raindrop.physicsBody = SKPhysicsBody(texture: raindropTexture, size: raindrop.size)
raindrop.position = CGPoint(x: size.width / 2, y: size.height / 2)
addChild(raindrop)
}
該方法被調(diào)用時(shí)锋八,會(huì)利用我們剛剛創(chuàng)建的 raindropTexture
來生成一個(gè)新的雨滴結(jié)點(diǎn)。然后护盈,我們通過紋理的形狀創(chuàng)建 SKPhysicsBody
挟纱,將結(jié)點(diǎn)位置設(shè)置為場(chǎng)景中央,并最終將其加入場(chǎng)景中腐宋。由于我們?yōu)橛甑谓Y(jié)點(diǎn)添加了 SKPhysicsBody
紊服,它將會(huì)自動(dòng)地受到默認(rèn)的重力作用并滴落至地面。為了測(cè)試這段代碼胸竞,我們可以在 touchesBegan(_ touches:, with event:)
中調(diào)用這個(gè)方法欺嗤,并看到如圖的效果:
讓雨水來的更猛烈些吧
只要我們不斷地點(diǎn)擊屏幕,雨滴就會(huì)源源不斷地出現(xiàn)卫枝。這僅僅是出于測(cè)試的目的煎饼;畢竟最終我們想要控制的是雨傘,而不是雨水落下的速率校赤。玩夠了之后吆玖,我們就該把代碼從 touchesBegan(_ touches:, with event:)
中刪除淤袜,并將其綁定到我們的 update
循環(huán)中了。我們有一個(gè)名為 update(_ currentTime:)
的方法衰伯,我們希望在這個(gè)方法中進(jìn)行降雨操作。方法中已經(jīng)有一些基礎(chǔ)代碼了积蔚;目前意鲸,我們僅僅是測(cè)量時(shí)間差,但一會(huì)兒尽爆,我們將用它來更新其它的精靈元素怎顾。在這個(gè)方法的底部,更新 self.lastUpdateTime
變量之前漱贱,添加如下代碼:
// Update the spawn timer
currentRainDropSpawnTime += dt
if currentRainDropSpawnTime > rainDropSpawnRate {
currentRainDropSpawnTime = 0
spawnRaindrop()
}
上述代碼在每次累加的時(shí)間差大于 rainDropSpawnRate
的時(shí)候槐雾,就會(huì)新建一個(gè)雨滴。rainDropSpawnRate
目前是 0.5 秒幅狮;也就是說募强,每過半秒鐘就會(huì)有新的雨滴被創(chuàng)建并落至地面。運(yùn)行程序來測(cè)試一下吧〕缟悖現(xiàn)在你不需要點(diǎn)擊屏幕擎值,而是每過半秒就有一滴新的雨滴被創(chuàng)建并下落,就像之前一樣逐抑。
但這還不夠好鸠儿。我們可不想所有雨滴都出現(xiàn)在同一個(gè)地方,更別說都從屏幕中間開始往下落了厕氨。我們可以更新 spawnRaindrop()
方法來隨機(jī)化每個(gè)新雨滴的 x
坐標(biāo)进每,并將它們放到屏幕頂部。
找到 spawnRaindrop()
方法中的這行代碼:
raindrop.position = CGPoint(x: size.width / 2, y: size.height / 2)
將其替換成如下代碼:
let xPosition = CGFloat(arc4random()).truncatingRemainder(dividingBy: size.width)
let yPosition = size.height + raindrop.size.height
raindrop.position = CGPoint(x: xPosition, y: yPosition)
在創(chuàng)建雨滴之后命斧,我們利用 arc4Random()
來隨機(jī)化 x
坐標(biāo)田晚,并通過調(diào)用 truncatingRemainder
來確保坐標(biāo)在屏幕范圍內(nèi)。現(xiàn)在運(yùn)行程序国葬,你應(yīng)該可以看到這樣的效果:
這雨可以下好幾天肉瓦!
我們可以嘗試不同的雨滴生成速率,雨滴生成的快慢將會(huì)根據(jù)我們?cè)O(shè)置的值變化胃惜。將 rainDropSpawnRate
設(shè)置為 0
泞莉,你將會(huì)看到漫天的雨滴。但如果你真的這么做了船殉,你就會(huì)發(fā)現(xiàn)一個(gè)嚴(yán)重的問題鲫趁。我們相當(dāng)于創(chuàng)建了無數(shù)個(gè)對(duì)象,并且永遠(yuǎn)沒有清除它們的機(jī)制利虫,我們的幀率最終會(huì)掉到四幀左右挨厚,并且很快就會(huì)超出內(nèi)存限制堡僻。
監(jiān)測(cè)碰撞
我們目前只需要考慮兩種碰撞。雨滴之間的碰撞以及雨滴和地面的碰撞疫剃。我們需要監(jiān)測(cè)雨滴碰撞到其它實(shí)體時(shí)的情況钉疫,并判斷是否要移除雨滴。我們將引入另一個(gè)物理實(shí)體來充當(dāng)全局邊界( world frame )巢价。任何觸碰到邊界的對(duì)象都會(huì)被銷毀牲阁,內(nèi)存壓力也將得到緩解。我們還需要區(qū)分不同的物理實(shí)體壤躲。幸運(yùn)的是城菊,SKPhysicsBody
有一個(gè)名為 categoryBitMask
的屬性。這個(gè)屬性將幫助我們區(qū)分互相發(fā)生接觸的對(duì)象碉克。
要完成上述工作凌唬,我們將在 Xcode 左部面板的 “Support” 文件夾下新創(chuàng)建一個(gè) Constants.swift
源文件。這個(gè) “Constants” 文件將統(tǒng)一管理我們?cè)谡麄€(gè)工程中會(huì)用到的硬編碼值( hardcode value )漏麦。我們并不會(huì)用到許多這種類型的變量客税,但把它們放在同一個(gè)地方管理是一個(gè)好習(xí)慣,這樣我們就不需要在工程中到處尋找它們了撕贞。創(chuàng)建完文件后霎挟,在里面添加如下的代碼:
let WorldCategory : UInt32 = 0x1 << 1
let RainDropCategory : UInt32 = 0x1 << 2
let FloorCategory : UInt32 = 0x1 << 3
上述的代碼運(yùn)用了 移位運(yùn)算符 來為不同物理實(shí)體的 categoryBitMasks
設(shè)置不同的唯一值。0x1 << 1
是十六進(jìn)制的 2 麻掸,0x1 << 2
是十六進(jìn)制的 4 酥夭,0x1 << 3
是十六進(jìn)制的 8 ,后續(xù)的值依此類推脊奋,為前一個(gè)值的兩倍熬北。在設(shè)置這些特定的類別( category )之后,回到 BackgroundNode.swift
文件中诚隙,將我們的物理實(shí)體更新為剛創(chuàng)建的 FloorCategory
讶隐。接著,我們還要將地面物理實(shí)體設(shè)置為可觸碰的久又。為了達(dá)到這個(gè)目的巫延,將 RainDropCategory
添加到地面元素的 contactTestBitMask
中。如此一來地消,當(dāng)我們將這些元素加入 GameScene.swift
中時(shí)炉峰,我們就能在二者(雨滴和地面)接觸時(shí)收到回調(diào)了。BackgroundNode
代碼如下:
import SpriteKit
public class BackgroundNode: SKNode {
public func setup(size: CGSize) {
let yPos: CGFloat = size.height * 0.10
let startPoint = CGPoint(x: 0, y: yPos)
let endPoint = CGPoint(x: size.width, y: yPos)
physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint)
physicsBody?.restitution = 0.3
physicsBody?.categoryBitMask = FloorCategory
physicsBody?.contactTestBitMask = RainDropCategory
}
}
下一步則是為雨滴元素設(shè)置正確的類別脉执,并為其添加可觸碰元素疼阔。回到 GameScene.swift
中,在 spawnRaindrop()
方法中初始化雨滴物理實(shí)體的代碼后面添加:
raindrop.physicsBody?.categoryBitMask = RainDropCategory
raindrop.physicsBody?.contactTestBitMask = FloorCategory | WorldCategory
注意婆廊,此處我們也添加了 WorldCategory
迅细。由于我們此處使用的是 位掩碼( bitmask ) ,我們可以通過 位運(yùn)算( bitwise operation) 來添加任何我們想要的類別淘邻。而對(duì)于本例中的 raindrop
實(shí)例茵典,我們希望監(jiān)聽它與 FloorCategory
以及 WorldCategory
發(fā)生碰撞時(shí)的信息。現(xiàn)在宾舅,我們終于可以在 sceneDidLoad()
方法中加入我們的全局邊界了:
var worldFrame = frame
worldFrame.origin.x -= 100
worldFrame.origin.y -= 100
worldFrame.size.height += 200
worldFrame.size.width += 200
self.physicsBody = SKPhysicsBody(edgeLoopFrom: worldFrame)
self.physicsBody?.categoryBitMask = WorldCategory
在上述代碼中统阿,我們創(chuàng)建了一個(gè)和場(chǎng)景形狀相同的邊界,只不過我們將每個(gè)邊都擴(kuò)張了 100 個(gè)點(diǎn)贴浙。這相當(dāng)于創(chuàng)建了一個(gè)緩沖區(qū),使得元素在離開屏幕后才會(huì)被銷毀署恍。注意我們所使用的 edgeLoopFrom
崎溃,它創(chuàng)建了一個(gè)空白矩形,其邊界可以和其它元素發(fā)生碰撞盯质。
現(xiàn)在袁串,一切用于檢測(cè)碰撞的準(zhǔn)備都已經(jīng)就緒了,我們只需要監(jiān)聽它就可以了呼巷。為我們的游戲場(chǎng)景添加對(duì) SKPhysicsContactDelegate
協(xié)議的支持囱修。在文件的頂部,找到這一行代碼:
class GameScene: SKScene {
把它改成這樣:
class GameScene: SKScene, SKPhysicsContactDelegate {
現(xiàn)在王悍,我們需要監(jiān)聽場(chǎng)景的 physicsWorld 中所發(fā)生的碰撞破镰。在 sceneDidLoad()
中,我們?cè)O(shè)置全局邊界的邏輯下面添加如下代碼:
self.physicsWorld.contactDelegate = self
接著压储,我們需要實(shí)現(xiàn) SKPhysicsContactDelegate
中的一個(gè)方法鲜漩,didBegin(_ contact:)
。每當(dāng)帶有我們預(yù)先設(shè)置的 contactTestBitMasks
的物體碰撞發(fā)生時(shí)集惋,這個(gè)方法就會(huì)被調(diào)用孕似。在 GameScene.swift
的底部,加入如下代碼:
func didBegin(_ contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == RainDropCategory) {
contact.bodyA.node?.physicsBody?.collisionBitMask = 0
contact.bodyA.node?.physicsBody?.categoryBitMask = 0
} else if (contact.bodyB.categoryBitMask == RainDropCategory) {
contact.bodyB.node?.physicsBody?.collisionBitMask = 0
contact.bodyB.node?.physicsBody?.categoryBitMask = 0
}
}
現(xiàn)在刮刑,當(dāng)一滴雨滴和任何其它對(duì)象的邊緣發(fā)生碰撞后喉祭,我們會(huì)將其碰撞掩碼( collision bitmask )清零。這樣做可以避免雨滴在初次碰撞后反復(fù)與其它對(duì)象碰撞雷绢,最終變成像俄羅斯方塊那樣的噩夢(mèng)泛烙!
愉快蹦達(dá)著的小雨滴
如果雨滴的表現(xiàn)沒有像 GIF 圖中所展示的那樣,回頭確認(rèn)所有的 categoryBitMask
和 contactTestBitMasks
都被正確設(shè)置了翘紊。同時(shí)胶惰,你應(yīng)該注意到場(chǎng)景右下角的結(jié)點(diǎn)數(shù)目會(huì)持續(xù)增長。雨滴不會(huì)再堆積在地面上了霞溪,但它們也沒有從場(chǎng)景中移除孵滞。如果我們不做移除工作中捆,內(nèi)存依然會(huì)出現(xiàn)不足的情況。
在 didBegin(_ contact:)
方法中坊饶,我們需要加入銷毀操作來移除這些結(jié)點(diǎn)泄伪。該方法需要被修改成這樣:
func didBegin(_ contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == RainDropCategory) {
contact.bodyA.node?.physicsBody?.collisionBitMask = 0
contact.bodyA.node?.physicsBody?.categoryBitMask = 0
} else if (contact.bodyB.categoryBitMask == RainDropCategory) {
contact.bodyB.node?.physicsBody?.collisionBitMask = 0
contact.bodyB.node?.physicsBody?.categoryBitMask = 0
}
if contact.bodyA.categoryBitMask == WorldCategory {
contact.bodyB.node?.removeFromParent()
contact.bodyB.node?.physicsBody = nil
contact.bodyB.node?.removeAllActions()
} else if contact.bodyB.categoryBitMask == WorldCategory {
contact.bodyA.node?.removeFromParent()
contact.bodyA.node?.physicsBody = nil
contact.bodyA.node?.removeAllActions()
}
}
現(xiàn)在,運(yùn)行程序匿级,我們會(huì)看到結(jié)點(diǎn)計(jì)數(shù)器增長到 6 個(gè)結(jié)點(diǎn)左右之后便會(huì)維持在那個(gè)數(shù)字蟋滴。如果確實(shí)如此,那就證明我們成功的移除了那些離開屏幕的結(jié)點(diǎn)了痘绎。
更新背景結(jié)點(diǎn)
目前為止津函,背景結(jié)點(diǎn)都非常的簡(jiǎn)單。它只是一個(gè) SKPhysicsBody
孤页,也就是一條線尔苦。我們要對(duì)它進(jìn)行升級(jí)來讓我們的應(yīng)用看起來更棒。放在以前行施,我們會(huì)用一個(gè) SKSpriteNode
來實(shí)現(xiàn)這個(gè)需求允坚,但這意味著要為一個(gè)簡(jiǎn)單背景耗費(fèi)一塊巨大的紋理。由于背景僅僅由兩種顏色組成蛾号,我們可以通過創(chuàng)建兩個(gè) SKShapeNode
來達(dá)到天空和地面的效果稠项。
打開 BackgroundNode.swift
并在 setup(size)
方法中,初始化 SKPhysicsBody
的下面添加如下代碼:
let skyNode = SKShapeNode(rect: CGRect(origin: CGPoint(), size: size))
skyNode.fillColor = SKColor(red: 0.38, green: 0.60, blue: 0.65, alpha: 1.0)
skyNode.strokeColor = SKColor.clear
skyNode.zPosition = 0
let groundSize = CGSize(width: size.width, height: size.height * 0.35)
let groundNode = SKShapeNode(rect: CGRect(origin: CGPoint(), size: groundSize))
groundNode.fillColor = SKColor(red: 0.99, green: 0.92, blue: 0.55, alpha: 1.0)
groundNode.strokeColor = SKColor.clear
groundNode.zPosition = 1
addChild(skyNode)
addChild(groundNode)
在上述代碼中鲜结,我們創(chuàng)建了兩個(gè)矩形的 SKShapeNode
實(shí)例展运,但引入 zPosition
導(dǎo)致了一個(gè)新問題。我們將 skyNode
的 zPosition
設(shè)為 0
精刷,而地面結(jié)點(diǎn)設(shè)置為 1
乐疆,如此一來,在渲染時(shí)地面就會(huì)始終在天空之上贬养。如果你現(xiàn)在運(yùn)行程序挤土,你會(huì)發(fā)現(xiàn),雨滴會(huì)被渲染在天空之上误算,但卻在地面之下仰美。這顯然不是我們想要的。讓我們回到 GameScene.swift
中儿礼,更新 spawnRaindrop()
方法中雨滴的 zPosition
咖杂,使之在被渲染在地面之上。在 spawnRaindrop()
方法中蚊夫,設(shè)置雨滴出現(xiàn)位置的下方诉字,加入下列代碼:
raindrop.zPosition = 2
再次運(yùn)行程序,背景應(yīng)該能夠被正常繪制了。
這下就好多了壤圃。
添加交互
現(xiàn)在對(duì)雨滴和背景的設(shè)置都已經(jīng)完成了陵霉,我們可以開始添加交互了。在 “Sprites” 文件夾下添加 UmbrellaSprite.swift
源文件伍绳,并添加下列代碼以生成雨傘的雛形踊挠。
import SpriteKit
public class UmbrellaSprite: SKSpriteNode {
public static func newInstance() -> UmbrellaSprite {
let umbrella = UmbrellaSprite(imageNamed: "umbrella")
return umbrella
}
}
一個(gè)非常基礎(chǔ)的對(duì)象就能滿足創(chuàng)建雨傘的要求了冲杀。目前效床,我們只是使用一個(gè)靜態(tài)方法創(chuàng)建了一個(gè)新的精靈結(jié)點(diǎn)( sprite node ),但別急权谁,一會(huì)我們就會(huì)為其添加一個(gè)自定的物理實(shí)體了剩檀。我們可以像創(chuàng)建雨滴一樣,調(diào)用 init(texture: size:)
方法來用紋理創(chuàng)建一個(gè)物理實(shí)體旺芽。這樣做也是可以的沪猴,但是雨傘的把手就會(huì)被物理實(shí)體所環(huán)繞。如果把手被物理實(shí)體環(huán)繞甥绿,那么貓就可能被掛在傘上字币,這個(gè)游戲也就因此失去了許多樂趣则披。所以共缕,我們會(huì)轉(zhuǎn)而通過在 newInstance()
方法中構(gòu)造一個(gè) CGPath
來初始化 SKPhysicsBody
。將下列代碼添加到 UmbrellaSprite.swift
的 newInstance()
方法中士复,返回雨傘對(duì)象的語句之前图谷。
let path = UIBezierPath()
path.move(to: CGPoint())
path.addLine(to: CGPoint(x: -umbrella.size.width / 2 - 30, y: 0))
path.addLine(to: CGPoint(x: 0, y: umbrella.size.height / 2))
path.addLine(to: CGPoint(x: umbrella.size.width / 2 + 30, y: 0))
umbrella.physicsBody = SKPhysicsBody(polygonFrom: path.cgPath)
umbrella.physicsBody?.isDynamic = false
umbrella.physicsBody?.restitution = 0.9
我們自己創(chuàng)建路徑來初始化雨傘的 SKPhysicsBody
主要有兩個(gè)原因。首先阱洪,就像之前提到的一樣便贵,我們只希望雨傘的頂部能夠與其它對(duì)象碰撞。其次冗荸,這樣我們可以自行調(diào)控雨傘的有效撞擊區(qū)域承璃。
先創(chuàng)建一個(gè) UIBezierPath
并添加點(diǎn)和線繪制好圖形后,再通過它生成 CGPath
是一個(gè)相對(duì)簡(jiǎn)單的方法蚌本。上述代碼中盔粹,我們就創(chuàng)建了一個(gè) UIBezierPath
并將其繪制起點(diǎn)移動(dòng)到精靈的中心點(diǎn)。umbrellaSprite
的中心點(diǎn)是 0,0
的原因是:其 anchorPoint 的值為 0.5,0.5
程癌。接著舷嗡,我們向左側(cè)添加一條線,并向外延伸 30 個(gè)點(diǎn)( points )嵌莉。
本文中關(guān)于“點(diǎn)( point )”的概念的注解:一個(gè)“點(diǎn)”进萄,不要與 CGPoint
或是我們的 anchorPoint
混淆,它是一個(gè)測(cè)量單位。在非 Retina 設(shè)備上中鼠,一個(gè)點(diǎn)等于一個(gè)像素可婶,在 Retina 設(shè)備上則等于兩個(gè)像素,這個(gè)值會(huì)隨著屏幕分辨率的提高而增加兜蠕。更多相關(guān)知識(shí)扰肌,請(qǐng)參閱 Fluid 博客中的 pixels and points 。
隨后熊杨,我們一路畫到精靈的頂部中點(diǎn)位置曙旭,再畫到中部右側(cè),并向外延伸 30 個(gè)點(diǎn)晶府。我們向外延伸一些距離桂躏,是為了在保持精靈外觀的前提下,增大其能遮雨的區(qū)域川陆。當(dāng)我們用這個(gè)多邊形初始化 SKPhysicsBody
時(shí)剂习,路徑將會(huì)自動(dòng)閉合成一個(gè)完整的三角形。接著较沪,將雨傘的物理狀態(tài)設(shè)置為非動(dòng)態(tài)鳞绕,這樣它就不會(huì)受重力影響了。我們繪制的這個(gè)物理實(shí)體看起來是這樣的:
現(xiàn)在尸曼,到 GameScene.swift
中來初始化雨傘對(duì)象并將其加入場(chǎng)景中们何。在文件頂部,類變量的下方控轿,加入下面的代碼:
private let umbrellaNode = UmbrellaSprite.newInstance()
接著冤竹,在 sceneDidLoad()
中,將 backgroundNode
加入場(chǎng)景的下面茬射,加入如下代碼來將雨傘放置在屏幕中央:
umbrellaNode.position = CGPoint(x: frame.midX, y: frame.midY)
umbrellaNode.zPosition = 4
addChild(umbrellaNode)
完成上述操作后鹦蠕,再運(yùn)行程序,你就能看見雨傘了在抛,同時(shí)你還會(huì)發(fā)現(xiàn)雨滴將會(huì)被雨傘彈開钟病!
動(dòng)起來
我們要為雨傘添加手勢(shì)響應(yīng)了。聚焦到 GameScene.swift
中的空方法 touchesBegan(_ touches:, with event:)
和 touchesMoved(_ touches:, with event:)
刚梭。這兩個(gè)方法會(huì)把我們的交互操作傳遞給雨傘對(duì)象肠阱。如果我們?cè)趦蓚€(gè)方法中都直接根據(jù)當(dāng)前的觸摸來更新雨傘的位置,雨傘將會(huì)從屏幕的一個(gè)位置瞬間移動(dòng)到另一位置望浩。
另一個(gè)可行方法是辖所,實(shí)時(shí)設(shè)置 UmbrellaSprite
對(duì)象的終點(diǎn),并且在 update(dt:)
方法被調(diào)用時(shí)磨德,逐步向終點(diǎn)方向移動(dòng)缘回。
而第三個(gè)可選方案則是在 touchesBegan(_ touches:, with event:)
或 touchesMoved(_ touches:, with event:)
中通過設(shè)置一系列 SKAction
來移動(dòng) UmbrellaSprite
吆视,但我不推薦這么做。這樣做會(huì)導(dǎo)致 SKAction
對(duì)象被頻繁創(chuàng)建和銷毀酥宴,使得性能變差啦吧。
我們這里選擇第二個(gè)解決方案。將 UmbrellaSprite
的代碼改成下面這樣:
import SpriteKit
public class UmbrellaSprite: SKSpriteNode {
private var destination: CGPoint!
private let easing: CGFloat = 0.1
public static func newInstance() -> UmbrellaSprite {
let umbrella = UmbrellaSprite(imageNamed: "umbrella")
let path = UIBezierPath()
path.move(to: CGPoint())
path.addLine(to: CGPoint(x: -umbrella.size.width / 2 - 30, y: 0))
path.addLine(to: CGPoint(x: 0, y: umbrella.size.height / 2))
path.addLine(to: CGPoint(x: umbrella.size.width / 2 + 30, y: 0))
umbrella.physicsBody = SKPhysicsBody(polygonFrom: path.cgPath)
umbrella.physicsBody?.isDynamic = false
umbrella.physicsBody?.restitution = 0.9
return umbrella
}
public func updatePosition(point: CGPoint) {
position = point
destination = point
}
public func setDestination(destination: CGPoint) {
self.destination = destination
}
public func update(deltaTime: TimeInterval) {
let distance = sqrt(pow((destination.x - position.x), 2) + pow((destination.y - position.y), 2))
if(distance > 1) {
let directionX = (destination.x - position.x)
let directionY = (destination.y - position.y)
position.x += directionX * easing
position.y += directionY * easing
} else {
position = destination;
}
}
}
這里主要干了這么幾件事拙寡。newInstance()
方法保持不變授滓,但我們?cè)谒纳戏郊尤肓藘蓚€(gè)變量。我們加入了 destination 變量(保存對(duì)象移動(dòng)的終點(diǎn)位置)肆糕;我們加入了 setDestination(destination:)
方法來緩沖雨傘的移動(dòng)般堆;我們還加入了一個(gè) updatePosition(point:)
方法。
updatePosition(point:)
方法將會(huì)在我們進(jìn)行刷新操作之前直接對(duì) position
屬性進(jìn)行賦值(譯者注:此處的意思是诚啃,雨傘的移動(dòng)本應(yīng)是設(shè)置終點(diǎn)后淮摔,在 update(dt:)
方法中逐步移動(dòng),但這個(gè) updatePosition(point:)
方法將直接移動(dòng)雨傘)∈际辏現(xiàn)在我們可以同時(shí)更新 position 和 destination 了和橙。如此一來, umbrellaSprite
對(duì)象就會(huì)被移動(dòng)到相應(yīng)位置造垛,并保持在原地魔招,由于這個(gè)位置就是它的終點(diǎn),它也不會(huì)在設(shè)置位置后立刻移動(dòng)了五辽。
setDestination(destination:)
方法僅更新 destination 屬性的值办斑;我們會(huì)在后續(xù)對(duì)這個(gè)值進(jìn)行一系列運(yùn)算。最終奔脐,我們?cè)?update(dt:)
方法中添加了計(jì)算我們所需要向終點(diǎn)方向移動(dòng)多少距離的邏輯俄周。我們計(jì)算兩點(diǎn)之間的距離吁讨,如果距離大于一個(gè)點(diǎn)髓迎,我們就結(jié)合 easing
屬性來計(jì)算移動(dòng)的距離(譯者注:原文寫的是 easing
function ,但實(shí)際代碼中 easing
只是一個(gè) factor 屬性)建丧。在計(jì)算出對(duì)象需要移動(dòng)的方向和距離后排龄, easing
屬性將每個(gè)坐標(biāo)軸上所需移動(dòng)的距離乘以 10% ,作為實(shí)際移動(dòng)距離翎朱。這樣做的話橄维,雨傘就不會(huì)瞬間到達(dá)新的位置了邢滑,當(dāng)雨傘離目標(biāo)位置較遠(yuǎn)時(shí)护蝶,其移動(dòng)速度會(huì)較快,而當(dāng)它接近終點(diǎn)附近枷遂,它的速度便會(huì)逐漸減低澈灼。如果距離終點(diǎn)距離不足一個(gè)點(diǎn)竞川,我們就直接移動(dòng)到終點(diǎn)店溢。我們這樣做是因?yàn)榫彌_機(jī)制(easing function)的存在會(huì)使終點(diǎn)附近的移動(dòng)非常緩慢。不用反復(fù)地計(jì)算委乌、更新并每次將雨傘移動(dòng)一小段距離床牧,我們只需要簡(jiǎn)單地設(shè)置好終點(diǎn)位置就可以了。
回到 GameScene.swift
中遭贸,將 touchesBegan(_ touches: with event:)
和 touchesMoved(_ touches: with event:)
中的邏輯做如下修改:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchPoint = touches.first?.location(in: self)
if let point = touchPoint {
umbrellaNode.setDestination(destination: point)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchPoint = touches.first?.location(in: self)
if let point = touchPoint {
umbrellaNode.setDestination(destination: point)
}
}
現(xiàn)在戈咳,我們的雨傘就能響應(yīng)觸摸事件了。在每個(gè)方法中壕吹,我們都檢測(cè)觸摸是否有效著蛙。有效的話,我們就將雨傘的終點(diǎn)更新為觸摸的位置耳贬。接下來册踩,把 sceneDidLoad()
中的這行代碼:
umbrella.position = CGPoint(x: frame.midX, y: frame.midY)
修改成:
umbrellaNode.updatePosition(point: CGPoint(x: frame.midX, y: frame.midY))
這樣,雨傘的初始位置和終點(diǎn)就設(shè)置好了效拭。當(dāng)我們運(yùn)行程序暂吉,場(chǎng)景中的雨傘僅會(huì)在我們進(jìn)行手勢(shì)交互時(shí)才會(huì)移動(dòng)。最后缎患,我們要在 update(currentTime:)
中通知雨傘進(jìn)行更新慕的。
在 update(currentTime:)
的底部加入如下代碼:
umbrellaNode.update(deltaTime: dt)
再次運(yùn)行程序,雨傘應(yīng)該能夠正確地跟著點(diǎn)擊和拖動(dòng)手勢(shì)進(jìn)行移動(dòng)了挤渔。
嘿肮街,第一課到此結(jié)束啦!我們接觸到了許多概念判导,并自己動(dòng)手搭建了基礎(chǔ)代碼嫉父,接著又添加了一個(gè)容器結(jié)點(diǎn)來容納背景和地面的 SKPhysicsBody
。我們還成功使新的雨滴定時(shí)出現(xiàn)眼刃,并讓雨傘響應(yīng)我們的手勢(shì)绕辖。你可以在 GitHub上找到 第一課內(nèi)容所涉及的源代碼。
你完成的怎么樣擂红?你的代碼實(shí)現(xiàn)是否和我的示例幾乎一樣仪际?哪里有不同呢?你是否優(yōu)化了示例代碼昵骤?教程中是否有闡述不清晰的地方树碱?請(qǐng)?jiān)谠u(píng)論中寫下你的想法。
感謝你堅(jiān)持完成了第一課变秦。讓我們拭目以待 RainCat 第二課吧成榜!