这是GameplayKit简介的第二部分。 如果您还没有完成第一部分 ,那么我建议先阅读该教程,然后再继续。
介绍
在本教程中,我将向您介绍GameplayKit框架的另外两个功能,您可以利用它们:
- 代理人,目标和行为
- 寻找路径
通过利用代理,目标和行为,我们将在本系列第一部分中开始的游戏中构建一些基本的人工智能(AI)。 AI将使我们的红色和黄色敌人点成为目标并向我们的蓝色玩家点移动。 我们还将实现寻路,以扩展此AI来绕过障碍物。
对于本教程,您可以使用本系列第一部分中完成的项目的副本,也可以从GitHub下载源代码的新副本。
1.代理人,目标和行为
在GameplayKit中,代理,目标和行为相互结合使用,以定义不同对象在整个场景中如何相对于彼此移动。 对于单个对象(或SKShapeNode
在我们的游戏),你就开始通过创建一个代理 ,由代表GKAgent
类。 但是,对于像我们这样的2D游戏,我们需要使用具体的GKAgent2D
类。
该GKAgent
类的子类GKComponent
。 这意味着您的游戏需要使用基于实体和组件的结构,正如我在本系列的第一个教程中向您展示的那样。
代理代表对象的位置,大小和速度。 然后,将GKBehaviour
类表示的行为添加到该代理。 最后,创建一组由GKGoal
类表示的目标 ,并将它们添加到行为对象。 目标可用于创建许多不同的游戏元素,例如:
- 走向代理商
- 远离代理商
- 与其他特工紧密联系
- 在特定位置徘徊
行为对象将监视并计算添加到该对象的所有目标,然后将这些数据中继回代理。 让我们看看这在实践中是如何工作的。
打开您的Xcode项目并导航到PlayerNode.swift 。 我们首先需要确保PlayerNode
类符合GKAgentDelegate
协议。
class PlayerNode: SKShapeNode, GKAgentDelegate {
...
接下来,将以下代码块添加到PlayerNode
类。
var agent = GKAgent2D()
// MARK: Agent Delegate
func agentWillUpdate(agent: GKAgent) {
if let agent2D = agent as? GKAgent2D {
agent2D.position = float2(Float(position.x), Float(position.y))
}
}
func agentDidUpdate(agent: GKAgent) {
if let agent2D = agent as? GKAgent2D {
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y))
}
}
首先,向PlayerNode
类添加一个属性,以便始终对当前玩家的代理对象进行引用。 接下来,我们实现GKAgentDelegate
协议的两种方法。 通过实现这些方法,我们确保屏幕上显示的玩家点将始终反映GameplayKit所做的更改。
在GameplayKit查看该代理的行为和目标以确定它应该移动到的位置之前,将调用agentWillUpdate(_:)
方法。 同样,在GameplayKit完成此过程后,直接调用agentDidUpdate(_:)
方法。
我们对这两种方法的实现确保了我们在屏幕上看到的节点能够反映GameplayKit所做的更改,并确保GameplayKit在执行其计算时使用节点的最后位置。
接下来,打开ContactNode.swift并将文件内容替换为以下实现:
import UIKit
import SpriteKit
import GameplayKit
class ContactNode: SKShapeNode, GKAgentDelegate {
var agent = GKAgent2D()
// MARK: Agent Delegate
func agentWillUpdate(agent: GKAgent) {
if let agent2D = agent as? GKAgent2D {
agent2D.position = float2(Float(position.x), Float(position.y))
}
}
func agentDidUpdate(agent: GKAgent) {
if let agent2D = agent as? GKAgent2D {
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y))
}
}
}
通过在ContactNode
类中实现GKAgentDelegate
协议,我们允许游戏中的所有其他点都与GameplayKit和我们的玩家点保持最新。
现在是时候建立行为和目标了。 为了使这项工作有效,我们需要注意三件事:
- 将玩家节点的代理添加到其实体并设置其委托。
- 为我们所有的敌人点配置代理,行为和目标。
- 在正确的时间更新所有这些代理。
首先,打开GameScene.swift,并在didMoveToView(_:)
方法的末尾添加以下两行代码:
playerNode.entity.addComponent(playerNode.agent)
playerNode.agent.delegate = playerNode
通过这两行代码,我们将代理添加为组件,并将代理的委托设置为节点本身。
接下来,用以下实现替换initialSpawn
方法的实现:
func initialSpawn() {
for point in self.spawnPoints {
let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive)
var node: SKShapeNode? = nil
switch respawnFactor {
case 0:
node = PointsNode(circleOfRadius: 25)
node!.physicsBody = SKPhysicsBody(circleOfRadius: 25)
node!.fillColor = UIColor.greenColor()
case 1:
node = RedEnemyNode(circleOfRadius: 75)
node!.physicsBody = SKPhysicsBody(circleOfRadius: 75)
node!.fillColor = UIColor.redColor()
case 2:
node = YellowEnemyNode(circleOfRadius: 50)
node!.physicsBody = SKPhysicsBody(circleOfRadius: 50)
node!.fillColor = UIColor.yellowColor()
default:
break
}
if let entity = node?.valueForKey("entity") as? GKEntity,
let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 {
entity.addComponent(agent)
agent.delegate = node as? ContactNode
agent.position = float2(x: Float(point.x), y: Float(point.y))
agents.append(agent)
let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0)
agent.behavior = behavior
agent.mass = 0.01
agent.maxSpeed = 50
agent.maxAcceleration = 1000
}
node!.position = point
node!.strokeColor = UIColor.clearColor()
node!.physicsBody!.contactTestBitMask = 1
self.addChild(node!)
}
}
我们添加的最重要的代码位于switch
语句之后的if
语句中。 让我们逐行浏览以下代码:
- 我们首先将代理作为组件添加到实体并配置其委托。
- 接下来,我们分配代理的位置,并将代理添加到存储的数组
agents
。 稍后我们将这个属性添加到GameScene
类中。 - 然后,我们用一个
GKGoal
创建一个GKBehavior
对象,以目标当前玩家的代理为目标。 此初始值设定项中的weight
参数用于确定哪些目标应优先于其他目标。 例如,假设您有一个目标是定位特定的业务代表,而目标是远离另一个业务代表,但是您希望目标优先。 在这种情况下,您可以将定位目标的权重设置为1
,将移动目标的权重设置为0.5
。 然后将此行为分配给敌方节点的代理。 - 最后,我们配置代理的
mass
,maxSpeed
和maxAcceleration
属性。 这些影响对象移动和旋转的速度。 随意使用这些值,看看它如何影响敌人点的移动。
接下来,将以下两个属性添加到GameScene
类中:
var agents: [GKAgent2D] = []
var lastUpdateTime: CFTimeInterval = 0.0
agents
数组将用于保留对场景中敌方特工的引用。 lastUpdateTime
属性将用于计算自上次更新场景以来经过的时间。
最后,将GameScene
类的update(_:)
方法的实现替换为以下实现:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
self.camera?.position = playerNode.position
if self.lastUpdateTime == 0 {
lastUpdateTime = currentTime
}
let delta = currentTime - lastUpdateTime
lastUpdateTime = currentTime
playerNode.agent.updateWithDeltaTime(delta)
for agent in agents {
agent.updateWithDeltaTime(delta)
}
}
在update(_:)
方法中,我们计算自上一次场景更新以来经过的时间,然后使用该值更新代理。
生成并运行您的应用,然后开始在现场移动。 您会看到敌人的点将慢慢开始向您移动。
如您所见,虽然敌人的点确实瞄准了当前玩家,但它们并没有绕过白色障碍物,而是试图穿越障碍物。 让我们通过寻路使敌人更聪明。
2.寻路
使用GameplayKit框架,您可以通过将物理物体与GameplayKit类和方法结合起来,为游戏添加复杂的寻路功能。 对于我们的游戏,我们将对其进行设置,以使敌方点将瞄准玩家点,并同时绕过障碍物。
GameplayKit中的寻路始于创建场景图 。 此图是各个位置(也称为节点 )以及这些位置之间的连接的集合。 这些连接定义了特定对象如何从一个位置移动到另一位置。 图形可以通过以下三种方式之一来建模场景中的可用路径:
- 包含障碍物的连续空间:此图形模型允许障碍物从一个位置到另一位置的平滑路径。 对于此模型,将
GKObstacleGraph
类用于图形,将GKPolygonObstacle
类用于障碍物,并将GKGraphNode2D
类用于节点(位置)。 - 一个简单的2D网格:在这种情况下,有效位置只能是具有整数坐标的位置。 当场景具有独特的网格布局并且不需要平滑路径时,此图形模型很有用。 使用此模型时,对象一次只能在一个方向上水平或垂直移动。 对于此模型,将
GKGridGraph
类用于图形,将GKGridGraphNode
类用于节点。 - 位置及其之间的连接的集合:这是最通用的图形模型,建议在对象在不同空间之间移动但对象在该空间中的特定位置对于游戏而言不是必需的情况下使用。 对于此模型,
GKGraph
类用于图形,而GKGraphNode
类用于节点。
因为我们希望游戏中的玩家点能够绕过白色障碍,所以我们将使用GKObstacleGraph
类创建场景图。 首先,将GameScene
类中的spawnPoints
属性替换为以下内容:
let spawnPoints = [
CGPoint(x: 245, y: 3900),
CGPoint(x: 700, y: 3500),
CGPoint(x: 1250, y: 1500),
CGPoint(x: 1200, y: 1950),
CGPoint(x: 1200, y: 2450),
CGPoint(x: 1200, y: 2950),
CGPoint(x: 1200, y: 3400),
CGPoint(x: 2550, y: 2350),
CGPoint(x: 2500, y: 3100),
CGPoint(x: 3000, y: 2400),
CGPoint(x: 2048, y: 2400),
CGPoint(x: 2200, y: 2200)
]
var graph: GKObstacleGraph!
为了本教程的目的, spawnPoints
数组包含一些更改的生成位置。 这是因为当前GameplayKit只能计算彼此相对靠近的对象之间的路径。
由于此游戏中点之间的默认距离较大,因此必须添加几个新的生成点来说明寻路。 请注意,我们还声明了GKObstacleGraph
类型的graph
属性,以保留对将创建的图形的引用。
接下来,在didMoveToView(_:)
方法的开头添加以下两行代码:
let obstacles = SKNode.obstaclesFromNodePhysicsBodies(self.children)
graph = GKObstacleGraph(obstacles: obstacles, bufferRadius: 0.0)
在第一行中,我们从场景中的物理物体创建了一系列障碍。 然后,我们使用这些障碍创建图形对象。 此初始化程序中的bufferRadius
参数可用于强制对象不在这些障碍物的一定距离内。 这些行需要在didMoveToView(_:)
方法的开始处添加,因为在调用initialSpawn
方法之前需要我们创建的图形。
最后,用以下实现替换initialSpawn
方法:
func initialSpawn() {
let endNode = GKGraphNode2D(point: float2(x: 2048.0, y: 2048.0))
self.graph.connectNodeUsingObstacles(endNode)
for point in self.spawnPoints {
let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive)
var node: SKShapeNode? = nil
switch respawnFactor {
case 0:
node = PointsNode(circleOfRadius: 25)
node!.physicsBody = SKPhysicsBody(circleOfRadius: 25)
node!.fillColor = UIColor.greenColor()
case 1:
node = RedEnemyNode(circleOfRadius: 75)
node!.physicsBody = SKPhysicsBody(circleOfRadius: 75)
node!.fillColor = UIColor.redColor()
case 2:
node = YellowEnemyNode(circleOfRadius: 50)
node!.physicsBody = SKPhysicsBody(circleOfRadius: 50)
node!.fillColor = UIColor.yellowColor()
default:
break
}
if let entity = node?.valueForKey("entity") as? GKEntity,
let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 {
entity.addComponent(agent)
agent.delegate = node as? ContactNode
agent.position = float2(x: Float(point.x), y: Float(point.y))
agents.append(agent)
/*let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0)
agent.behavior = behavior*/
/*** BEGIN PATHFINDING ***/
let startNode = GKGraphNode2D(point: agent.position)
self.graph.connectNodeUsingObstacles(startNode)
let pathNodes = self.graph.findPathFromNode(startNode, toNode: endNode) as! [GKGraphNode2D]
if !pathNodes.isEmpty {
let path = GKPath(graphNodes: pathNodes, radius: 1.0)
let followPath = GKGoal(toFollowPath: path, maxPredictionTime: 1.0, forward: true)
let stayOnPath = GKGoal(toStayOnPath: path, maxPredictionTime: 1.0)
let behavior = GKBehavior(goals: [followPath, stayOnPath])
agent.behavior = behavior
}
self.graph.removeNodes([startNode])
/*** END PATHFINDING ***/
agent.mass = 0.01
agent.maxSpeed = 50
agent.maxAcceleration = 1000
}
node!.position = point
node!.strokeColor = UIColor.clearColor()
node!.physicsBody!.contactTestBitMask = 1
self.addChild(node!)
}
self.graph.removeNodes([endNode])
}
我们通过创建具有默认播放器生成坐标的GKGraphNode2D
对象开始该方法。 接下来,我们将此节点连接到图形,以便在查找路径时可以使用它。
大多数initialSpawn
方法保持不变。 我添加了一些注释,以向您展示代码的寻路部分在第一个if
语句中的位置。 让我们逐步看一下这段代码:
- 我们创建另一个
GKGraphNode2D
实例,并将其连接到图形。 - 我们通过调用图上的
findPathFromNode(_:toNode:)
方法来创建一系列组成路径的节点。 - 如果成功创建了一系列路径节点,那么我们将根据它们创建路径。
radius
参数的工作方式与之前的bufferRadius
参数相似,并且定义了对象可以从创建的路径移开多少。 - 我们创建了两个
GKGoal
对象,一个用于跟踪路径,另一个用于保持路径。maxPredictionTime
参数允许目标尽可能早地计算出是否有任何事情会干扰对象跟随/停留在该特定路径上。 - 最后,我们根据这两个目标创建新行为并将其分配给代理。
您还将注意到,完成处理后,我们将从图中删除创建的节点。 这样做是一个很好的实践,因为它可以确保您创建的节点以后不会干扰任何其他寻路计算。
上一次构建并运行您的应用程序时,您会看到两个点非常靠近您,并开始向您移动。 如果它们都以绿色圆点生成,则可能必须多次运行游戏。
重要!
在本教程中,我们使用了GameplayKit的寻路功能,以使敌方点能够将玩家点瞄准障碍物周围。 请注意,这只是一个实际的寻路示例。
对于实际的生产游戏,最好通过将本教程前面的玩家定位目标与使用init(toAvoidObstacles:maxPredictionTime:)
便捷方法创建的避障目标相结合来实现此功能,有关更多信息,请init(toAvoidObstacles:maxPredictionTime:)
在GKGoal
类参考中 。
结论
在本教程中,我向您展示了如何在具有实体组件结构的游戏中利用代理,目标和行为。 虽然我们在本教程中仅创建了三个目标,但是还有更多可用的目标,您可以在GKGoal
类参考中阅读更多内容。
我还向您展示了如何通过创建图形,一组障碍以及遵循这些路径的目标来在游戏中实现一些高级的路径查找。
如您所见,GameplayKit框架为您提供了大量功能。 在本系列的第三部分(也是最后一部分)中,我将向您介绍GameplayKit的随机值生成器,以及如何创建自己的规则系统以在游戏中引入一些模糊逻辑。
与往常一样,请确保在下面留下您的评论和反馈。
翻译自: https://code.tutsplus.com/tutorials/an-introduction-to-gameplaykit-part-2--cms-24528