West1
West1

Reputation: 1928

GameplayKit: Enemy agent overshoots destination point during seek behavior

I'm trying to incorporate GameplayKit into a SpriteKit project.

Specifically, I'm trying to use GameplayKit's seek-and-avoid behavior wherein an "enemy" character chases a moving "player" character while avoiding obstacles.

I have managed to make the enemy seek the player, but the enemy's movement is weird/undesirable: The enemy's speed should adjust so that it comes to a halt at the desired end point, however its speed does not change, which makes it overshoot its target considerably.

Think of a fast-moving train: Once it gets up to speed, it's hard to stop; it wants to just keep on moving. It's the same phenomenon with the enemy character sprite. It moves like a heavy, lumbering object that can't stop quickly enough.

Here's what I'm doing (please assume that properties such as node are defined):

  1. Configure the player character's GameplayKit stuff:
let entity1 = GKEntity()
heroAgent = GKAgent2D()
heroComponent = GKSKNodeComponent(node: node)
heroAgent.delegate = self
node.entity = entity1
heroEntity = entity1
if let comp = heroComponent {
    entity1.addComponent(comp)
    entity1.addComponent(heroAgent)
}
  1. Configure the enemy character's GameplayKit stuff:
let entity = GKEntity()
let agent = GKAgent2D()
let avoid = GKGoal(toAvoid: obstaclesForThisLevel!, maxPredictionTime: 1000.0)
let pursue = GKGoal(toInterceptAgent: heroAgent, maxPredictionTime: 0.0)
nodeComponent = GKSKNodeComponent(node: node)
agent.maxSpeed = 100.0
agent.maxAcceleration = 50.0
agent.position = vector_float2(x: Float(levelData.enemies[i].x), y: Float(levelData.enemies[i].y))
agent.radius = Float(node.size.width*0.5)
agent.behavior = GKBehavior(goals: [avoid, pursue], andWeights: [100.0, 100.0])
agent.delegate = self
node.entity = entity
enemyAgent = agent
if let comp = nodeComponent {
    entity.addComponent(comp)
    entity.addComponent(agent)
}
nodeEntity = entity
  1. In the SKScene's update(_:) method, set the player agent's position and call the enemy character's update(deltaTime:) method:
//Coordinate the hero agent's position with the hero's actual position. Without this, the values in agentDidUpdate() are nan.
heroAgent.position = vector_float2(x: Float(mainCharacter?.position.x ?? 0.0), y: Float(mainCharacter?.position.y ?? 0.0))
nodeComponent?.node.entity?.update(deltaTime: currentTime-lastFrameTime)
  1. Set the enemy character's position in agentDidUpdate(_:):
func agentDidUpdate(_ agent: GKAgent) {
   if let a = agent as? GKAgent2D {
      nodeComponent?.node.position = CGPoint(x: CGFloat(a.position.x), y: CGFloat(a.position.y))
   }
}

Question: Why is the enemy character overshooting its designated target point instead of managing its speed correctly, and how do I fix the issue?

Thank you!

Upvotes: 0

Views: 116

Answers (1)

West1
West1

Reputation: 1928

I decided to only use GameplayKit for the purpose of generating a path around the obstacles (findPath(from:to:) is very helpful), and to simply move the enemy character along that path via SKAction.follow(_:asOffset:orientToPath:speed:) when I want to send the enemy back to its starting position.

So, to accomplish the "seeking" behavior, I'm just doing the following from inside my SKScene's update(_:) method:

let dx = mainCharacter.position.x - enemy.position.x
let dy = mainCharacter.position.y - enemy.position.y
let angle = atan2(dy, dx)
let vx = cos(angle) * enemySpeed
let vy = sin(angle) * enemySpeed

enemy.physicsBody?.velocity.dx = vx
enemy.physicsBody?.velocity.dy = vy

This seeking behavior does not consider obstacles, so the enemy can get kind of stuck behind an obstacle that's located between the enemy and the player. So, it's not ideal, but I think it's going to work for me because of other factors in my game.

To generate a path that does consider obstacles, for use with SKAction, I'm doing the following:

let graph = GKObstacleGraph(obstacles: obstaclesForThisLevel, bufferRadius: 0.0)
let startingPoint = GKGraphNode2D(point: vector_float2(x: Float(enemy.position.x), y: Float(enemy.position.y)))
let endPoint = GKGraphNode2D(point: vector_float2(x: Float(enemy.enemyOriginalPosition.x), y: Float(enemy.enemyOriginalPosition.y)))
graph.connectUsingObstacles(node: startingPoint)
graph.connectUsingObstacles(node: endPoint)
let path = (graph as GKGraph).findPath(from: startingPoint, to: endPoint)

if path.count >= 2 {
    let realPath = GKPath(graphNodes: path, radius: 100.0)
    let myPath: UIBezierPath = UIBezierPath()

     for j in 1..<realPath.numPoints {
          let previousPoint = realPath.float2(at: j-1)
          let nextPoint = realPath.float2(at: j)
          if j == 1 {
              myPath.move(to: CGPoint(x: CGFloat(previousPoint.x), y: CGFloat(previousPoint.y)))
          }
          myPath.addLine(to: CGPoint(x: CGFloat(nextPoint.x), y: CGFloat(nextPoint.y)))
      }
}

And finally:

let followPath = SKAction.follow(myPath.cgPath, asOffset: false, orientToPath: false, speed: enemySpeed)

enemy.run(followPath)

Upvotes: 0

Related Questions