在上一篇我们学习了利用 GameplayKit的 pathfinding API 来计算位于场景中的两点之间的路径,并避开指定的障碍物的算法。
在这一篇中,让我们来实现一种不同的在场景中移动的效果。GameplayKit 介绍了 Behaviours(行为) 和 Goals(目标) 的概念.他们提供了一种方式,让你能够依赖约束和目标把节点的放置在场景中某个特定位置。让我们先看一下视频,然后再来详细看一下。
上面的例子中(我们马上要创建它),你可以看到一个黄色的盒子代表一个用户。黄色的盒子随着用户点击场景中的任意一处来移动。特别基本的东西,对吧。有趣的是导弹部分,它能够寻找到player,并且总是试图通过player节点的中心。
这不需要任何的物理或者自定义代码来完成,这完全有一个行为可寻址目标来控制。
现在,让我们通过 Demo了解一下 behaviours 和 goals 是怎么工作的。
Creating a Behavior and Goal Example
使用默认的 SpriteKit 模版创建项目,打开 GameScene.swift
首先,我们定义一个实例
let player:Player = Player()
var missile:Missile?
GKEntity 是一个通用的实体,可以给它添加组件和方法。在我们的例子中,我们有两个实例,一个代表 player,一个代表导弹。我们马上来看一下它的细节实现。
我们还需要创建一个组件系统的数组。这个组件系统是指符合同样类型的组件的一个集合。我们可以在需要时候的时候,再定义它(lazy var),因为我们仅需初始化它一次。我们有一个组件作为靶子(可以用来追踪player的位置,并添加冒烟的效果),另一个作为导弹。我们定义的顺序,会成为一会儿运动的顺序。所以我们先返回targeting 然后是 rendering. 因为我们希望根据目标的变化,来追踪显示他们的。
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的一个子类, 呈现为一个根据速度定位的本地坐标系统。
class Player: NodeEntity, GKAgentDelegate {
let agent:GKAgent2D = GKAgent2D()
在本例中,代理其实是无言的。如果不是用户手动干预,它不会做任何事情,也不会对位置进行任何变化。我们需要一个代理,因为靶子组件必须有一个代理。
override init() {
super.init()
在初始化中,我们添加一个 RenderComponent 和一个PlayerNode. 我们不详细讲 PlayerNode 了,因为非常枯燥。这里我们仅简单画一个黄色的方盒。
let renderComponent = RenderComponent(entity: self)
renderComponent.node.addChild(PlayerNode())
addComponent(renderComponent)
我们把代理设为自己,通过把代理添加到实体上去。
agent.delegate = self
addComponent(agent)
}
我们还需要去生命 GKAgentDelegate 的代理方法。这样,当代理更新后,Node 的位置会自动更新,同时,当用户手动更新了位置后,代理也会通过计算更新位置。
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 略有不同。我们添加一个目标代理,让导弹去追踪。
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 轴的数值。
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行代码就可以。
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的子类,有一个单独的方法。
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方法。
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
我们已经在初始化是创建了 player,所以我们添加player.node到场景中。
self.addChild(player.node)
对于missile, 我们也必须要在这里创建好。
missile = Missile(withTargetAgent: player.agent)
然后我们为 missile 添加setupEmitters方法,让烟雾可以根据目标移动并扩散,而非只是动一下。
missile!.setupEmitters(withTargetScene: self)
self.addChild(missile!.node)
最后,所有的entities创建好后,我们添加它的components到我们的组件系统中。
for componentSystem in self.componentSystems {
componentSystem.addComponentWithEntity(player)
componentSystem.addComponentWithEntity(missile!)
}
现在在update.currentTime方法中,为组件的更新时间数组,添加增量时间。这会使的重新计算时间并进行渲染。
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 游戏比较陌生,如有翻译错误,还望大家在评论中指出。***