iOS9 Day-by-Day :: Day 12 :: Gameplay Kit – Behaviors and Goals

原文地址
这是一个系列文章,查看更多请移步目录页

在上一篇我们学习了利用 GameplayKit的 pathfinding API 来计算位于场景中的两点之间的路径,并避开指定的障碍物的算法。

在这一篇中,让我们来实现一种不同的在场景中移动的效果。GameplayKit 介绍了 Behaviours(行为) 和 Goals(目标) 的概念.他们提供了一种方式,让你能够依赖约束和目标把节点的放置在场景中某个特定位置。让我们先看一下视频,然后再来详细看一下。

上面的例子中(我们马上要创建它),你可以看到一个黄色的盒子代表一个用户。黄色的盒子随着用户点击场景中的任意一处来移动。特别基本的东西,对吧。有趣的是导弹部分,它能够寻找到player,并且总是试图通过player节点的中心。

这不需要任何的物理或者自定义代码来完成,这完全有一个行为可寻址目标来控制。

现在,让我们通过 Demo了解一下 behaviours 和 goals 是怎么工作的。

Creating a Behavior and Goal Example

使用默认的 SpriteKit 模版创建项目,打开 GameScene.swift

setup1.png

首先,我们定义一个实例

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 游戏比较陌生,如有翻译错误,还望大家在评论中指出。***

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,368评论 25 707
  • This article is a record of my journey to learn Game Deve...
    蔡子聪阅读 3,746评论 0 9
  • 随着工作节奏的加快,如何更快更好的完成每天的工作、生活等内容就成了一个大问题。所以我们无时无刻不提出要保持高效的口...
    卧寅阅读 436评论 2 0
  • 路由系统(URL配置)它就是URL与要为该URL调用的视图函数之间的映射表,也就是说不同的URL对应不同的处理函数...
    戴维得阅读 664评论 0 0