如何在 Swift 3 中用 SpriteKit 框架編寫游戲 (Part 1)

寫在前面:這個(gè)系列文章是轉(zhuǎn)載過來的,簡(jiǎn)書里之前也有人轉(zhuǎn)載了驰吓,不過沒有進(jìn)行重新編排哪亿,圖文等格式并不適用于簡(jiǎn)書,我參照原文樣式重新排版了一次菱魔。

你有沒有想過要如何開始創(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: 第一課

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)景

我們的略微空曠的場(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)所有的 categoryBitMaskcontactTestBitMasks 都被正確設(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è)新問題。我們將 skyNodezPosition 設(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.swiftnewInstance() 方法中士复,返回雨傘對(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í)體看起來是這樣的:

雨傘和雨傘物理實(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 第二課吧成榜!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蹦玫,隨后出現(xiàn)的幾起案子赎婚,更是在濱河造成了極大的恐慌雨饺,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惑淳,死亡現(xiàn)場(chǎng)離奇詭異额港,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)歧焦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門移斩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绢馍,你說我怎么就攤上這事向瓷。” “怎么了舰涌?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵猖任,是天一觀的道長。 經(jīng)常有香客問我瓷耙,道長朱躺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任搁痛,我火速辦了婚禮长搀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鸡典。我一直安慰自己源请,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布彻况。 她就那樣靜靜地躺著谁尸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纽甘。 梳的紋絲不亂的頭發(fā)上良蛮,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音贷腕,去河邊找鬼背镇。 笑死咬展,一個(gè)胖子當(dāng)著我的面吹牛泽裳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播破婆,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼涮总,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了祷舀?” 一聲冷哼從身側(cè)響起瀑梗,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤烹笔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后抛丽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谤职,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年亿鲜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了允蜈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蒿柳,死狀恐怖饶套,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垒探,我是刑警寧澤妓蛮,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站圾叼,受9級(jí)特大地震影響蛤克,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜夷蚊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一咖耘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撬码,春花似錦儿倒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至叫胁,卻和暖如春凰慈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驼鹅。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國打工微谓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人输钩。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓豺型,卻偏偏與公主長得像,于是被迫代替她去往敵國和親买乃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姻氨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容