在上一篇我們學習了利用 GameplayKit的 pathfinding API 來計算位於場景中的兩點之間的路徑,並避開指定的障礙物的演算法。
在這一篇中,讓我們來實現一種不同的在場景中移動的效果。GameplayKit 介紹了 Behaviours(行為) 和 Goals(目標) 的概念.他們提供了一種方式,讓你能夠依賴約束和目標把節點的放置在場景中某個特定位置。讓我們先看一下視訊,然後再來詳細看一下。
上面的例子中(我們馬上要建立它),你可以看到一個黃色的盒子代表一個使用者。黃色的盒子隨著使用者點選場景中的任意一處來移動。特別基本的東西,對吧。有趣的是導彈部分,它能夠尋找到player,並且總是試圖通過player節點的中心。
這不需要任何的物理或者自定義程式碼來完成,這完全有一個行為可定址目標來控制。
現在,讓我們通過 Demo瞭解一下 behaviours 和 goals 是怎麼工作的。
Creating a Behavior and Goal Example
使用預設的 SpriteKit 模版建立專案,開啟 GameScene.swift
首先,我們定義一個例項
1 2 |
let player:Player = Player() var missile:Missile? |
GKEntity 是一個通用的實體,可以給它新增元件和方法。在我們的例子中,我們有兩個例項,一個代表 player,一個代表導彈。我們馬上來看一下它的細節實現。
我們還需要建立一個元件系統的陣列。這個元件系統是指符合同樣型別的元件的一個集合。我們可以在需要時候的時候,再定義它(lazy var),因為我們僅需初始化它一次。我們有一個元件作為靶子(可以用來追蹤player的位置,並新增冒煙的效果),另一個作為導彈。我們定義的順序,會成為一會兒運動的順序。所以我們先返回targeting 然後是 rendering. 因為我們希望根據目標的變化,來追蹤顯示他們的。
1 2 3 4 5 |
lazy var componentSystems:[GKComponentSystem] = { let targetingSystem = GKComponentSystem(componentClass: TargetingComponent.self) let renderSystem = GKComponentSystem(componentClass: RenderComponent.self) return [targetingSystem, renderSystem] }() |
但什麼才是一個 GameKit 元件呢?我們已經討論了在場景中的實體的效果,但沒講具體做了什麼。一個 GKComponent 在特定部分,囊括了資料和邏輯。元件和實體聯絡,一個實體可能對應多個元件。它們為元件提供可重用的行為。它們通過元件模型,來幫助解決大型遊戲中可能出現的複雜而大型的繼承樹問題。
在這個場景中,兩個實體都有渲染元件,導彈實體還有靶子元件。
設定實體
The Player Entity
下面程式碼是 player 類,它是一個簡單的幾成字 NodeEntity的類,擁有唯一一個元件。注意還有一個 GKAgent2D 的屬性.
GKAgent2D 是 GKAgent的一個子類, 呈現為一個根據速度定位的本地座標系統。
1 2 |
class Player: NodeEntity, GKAgentDelegate { let agent:GKAgent2D = GKAgent2D() |
在本例中,代理其實是無言的。如果不是使用者手動干預,它不會做任何事情,也不會對位置進行任何變化。我們需要一個代理,因為靶子元件必須有一個代理。
1 2 |
override init() { super.init() |
在初始化中,我們新增一個 RenderComponent 和一個PlayerNode. 我們不詳細講 PlayerNode 了,因為非常枯燥。這裡我們僅簡單畫一個黃色的方盒。
1 2 3 |
let renderComponent = RenderComponent(entity: self) renderComponent.node.addChild(PlayerNode()) addComponent(renderComponent) |
我們把代理設為自己,通過把代理新增到實體上去。
1 2 3 |
agent.delegate = self addComponent(agent) } |
我們還需要去生命 GKAgentDelegate 的代理方法。這樣,當代理更新後,Node 的位置會自動更新,同時,當使用者手動更新了位置後,代理也會通過計算更新位置。
1 2 3 4 5 6 7 8 9 10 11 12 |
func agentDidUpdate(agent: GKAgent) { if let agent2d = agent as? GKAgent2D { node.position = CGPoint(x: CGFloat(agent2d.position.x), y: CGFloat(agent2d.position.y)) } } func agentWillUpdate(agent: GKAgent) { if let agent2d = agent as? GKAgent2D { agent2d.position = float2(Float(node.position.x), Float(node.position.y)) } } } |
The Missile Entity
missile 實體和 PlayerNode 略有不同。我們新增一個目標代理,讓導彈去追蹤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Missile: NodeEntity, GKAgentDelegate { let missileNode = MissileNode() required init(withTargetAgent targetAgent:GKAgent2D) { super.init() let renderComponent = RenderComponent(entity: self) renderComponent.node.addChild(missileNode) addComponent(renderComponent) let targetingComponent = TargetingComponent(withTargetAgent: targetAgent) targetingComponent.delegate = self addComponent(targetingComponent) } |
你可能注意到這個類中沒有 GKAgent2D,這是因為我們使用了 TargetingComponent 來控制實體在場景中的移動。稍後,我們會討論 TargetingComponent. 現在,我們需要知道,我們已經提供了 targetAgent ,我們啟動代理的方法。
我們需要生命 agentDidUpdate 和 agentWillUpdate兩個代理方法。這和Player類中有什麼不同呢?在這個類中,我們還需要為方法提供 Z 軸的數值。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func agentDidUpdate(agent: GKAgent) { if let agent2d = agent as? GKAgent2D { node.position = CGPoint(x: CGFloat(agent2d.position.x), y: CGFloat(agent2d.position.y)) node.zRotation = CGFloat(agent2d.rotation) } } func agentWillUpdate(agent: GKAgent) { if let agent2d = agent as? GKAgent2D { agent2d.position = float2(Float(node.position.x), Float(node.position.y)) agent2d.rotation = Float(node.zRotation) } } |
The Targeting Component
到目前為止,所有的類相對都是輕便的。你可能都忘了還需要在靶子元件中完成邏輯程式碼
幸運的是, 得益於 GameplayKit,在本例中,我們僅需要寫20行程式碼就可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class TargetingComponent: GKAgent2D { let target:GKAgent2D required init(withTargetAgent targetAgent:GKAgent2D) { target = targetAgent super.init() let seek = GKGoal(toSeekAgent: targetAgent) self.behavior = GKBehavior(goals: [seek], andWeights: [1]) self.maxSpeed = 4000 self.maxAcceleration = 4000 self.mass = 0.4 } } |
這段程式碼簡單的不需解釋。你可以看到他繼承自 GKAgent2D, 建立了一個GKGoal.然後通過這個goal 建立了CKBehavior物件。如果你有多個 goal,例如去追蹤一個目標同時要避開某個目標,你就可以建立多個 GKGoal。 你甚至還可以分別GKGoal 的 weight 屬性,這樣可以設定避開某個 goal 比追逐某個 goal 的權重更重一些。
我們同時也設定了一些其他的屬性:maxSpeed,maxAcceleration 和 mass. 這些屬性需要根據你的實際場景進行設定,這裡設定成這樣對我來說是合適的。剛開始的時候我使用了預設值,然後以為那裡出來毛病。後來發現是預設值太低了,導致移動非常慢,完全看不出效果。
The Missile Node
現在 Missile entity 建立好了,我們需要給它新增一個node,以在場景中顯示。這個node 是SKNode的子類,有一個單獨的方法。
1 2 3 4 5 6 7 8 9 |
func setupEmitters(withTargetScene scene:SKScene) { let smoke = NSKeyedUnarchiver.unarchiveObjectWithFile(NSBundle.mainBundle().pathForResource("MissileSmoke", ofType:"sks")!) as! SKEmitterNode smoke.targetNode = scene self.addChild(smoke) let fire = NSKeyedUnarchiver.unarchiveObjectWithFile(NSBundle.mainBundle().pathForResource("MissileFire", ofType:"sks")!) as! SKEmitterNode fire.targetNode = scene self.addChild(fire) } |
你可以看到setupEmitters 方法建立了兩個 SKEmitter nodes.把 target node 設定為了場景,如果不設定的話,那麼就不會出現跟蹤導彈並冒煙的效果。你可以開啟 MissileFire.sks 和 MissileSmoke.sks 兩個檔案,檢視具體內容,這裡我們不詳細解釋了。
Combining the Parts
現在我們的nodes, entities 和 components都已經建立好了,我們回到 GameScene.swift檔案中,把它們組合起來。 我們需要過載 didMoveToView方法。
1 2 |
override func didMoveToView(view: SKView) { super.didMoveToView(view) |
我們已經在初始化是建立了 player,所以我們新增player.node到場景中。
1 |
self.addChild(player.node) |
對於missile, 我們也必須要在這裡建立好。
1 |
missile = Missile(withTargetAgent: player.agent) |
然後我們為 missile 新增setupEmitters方法,讓煙霧可以根據目標移動並擴散,而非只是動一下。
1 2 |
missile!.setupEmitters(withTargetScene: self) self.addChild(missile!.node) |
最後,所有的entities建立好後,我們新增它的components到我們的元件系統中。
1 2 3 4 |
for componentSystem in self.componentSystems { componentSystem.addComponentWithEntity(player) componentSystem.addComponentWithEntity(missile!) } |
現在在update.currentTime方法中,為元件的更新時間陣列,新增增量時間。這會使的重新計算時間並進行渲染。
1 2 3 4 5 6 7 8 9 10 11 |
override func update(currentTime: NSTimeInterval) { // Calculate the amount of time since `update` was last called. let deltaTime = currentTime - lastUpdateTimeInterval for componentSystem in componentSystems { componentSystem.updateWithDeltaTime(deltaTime) } lastUpdateTimeInterval = currentTime } |
這就是全部我們做的了。現在執行一下游戲,你會看到一個導彈始終跟隨著playe。在這裡我們並沒有新增碰撞和爆炸效果,如果你感興趣可以自己做一下。為什麼不呢?
延伸閱讀
想要了解更多關於 GameplayKit的特性,推薦觀看WWDC 2015的session 608, Introducing GameplayKit. 別忘了,可以在Git中找到本文的示例程式碼。
備註:本文譯者對 iOS 遊戲比較陌生,如有翻譯錯誤,還望大家在評論中指出。