Search code examples
iosswiftsprite-kitgameplay-kit

GKBehavior/GKGoal causing chaos with position


I'm trying to get a GKActor to position SKNodes in a scene. I thought I had this working. However, when I add a GKBehavior to the actor, I get very erratic position changes flicking all over the place and the actors behaviour isn't working.

I'm using an entity-component architecture in my project, which is a little too complicated to show an example here, so I've created a dramatically simplified playground to illustrate the issue I'm seeing.

//: A SpriteKit based Playground

import PlaygroundSupport
import SpriteKit
import GameplayKit

extension CGPoint {
    init(point: vector_float2) {
        self.init(
            x: CGFloat(point.x),
            y: CGFloat(point.y)
        )
    }
}

class GameScene: SKScene, GKAgentDelegate {

    private let playerAgent = GKAgent2D()
    private var player : SKShapeNode!
    private let enemyAgent = GKAgent2D()
    private var enemy : SKShapeNode!
    
    override func didMove(to view: SKView) {
        player = SKShapeNode(circleOfRadius: 40)
        player.fillColor = .green
        addChild(player)

        playerAgent.position.x = .random(in: -(640/2)...640/2)
        playerAgent.position.y = .random(in: -(480/2)...480/2)
        playerAgent.delegate = self

        enemy = SKShapeNode(circleOfRadius: 10)
        enemy.fillColor = .red
        addChild(enemy)

        enemyAgent.position.x = .random(in: -(640/2)...640/2)
        enemyAgent.position.y = .random(in: -(480/2)...480/2)
        enemyAgent.delegate = self


        enemyAgent.behavior = GKBehavior(
            goals: [
                //GKGoal(toSeekAgent: playerAgent)
            ]
        )
    }

    override func update(_ currentTime: TimeInterval) {
        super.update(currentTime)
        playerAgent.update(deltaTime: currentTime)
        enemyAgent.update(deltaTime: currentTime)
    }

    func agentDidUpdate(_ agent: GKAgent) {
        if agent == enemyAgent {
            enemy.position = CGPoint(point: enemyAgent.position)
            print(enemy.position)
        }
        if agent == playerAgent {
            player.position = CGPoint(point: playerAgent.position)
        }
    }
    
    @objc static override var supportsSecureCoding: Bool {
        get {
            return true
        }
    }
}

let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
if let scene = GameScene(fileNamed: "GameScene") {
    scene.scaleMode = .aspectFill
    sceneView.presentScene(scene)
}

PlaygroundSupport.PlaygroundPage.current.liveView = sceneView

This Playground shows the two nodes placed where their agents are positioned. It's working how I expet. However, once the GKGoal is uncommented, the position of the enemyAgent that is printed out wildly fluxuates and doesn't move toward the playerAgent. This is not what I expeted to happen.

Clearly I'm doing somthing wrong but I can't see where i've made a mistake. I'd love some help from someone with more experince working with GameplayKit. Thank You.


Solution

  • I figured out the issue. The GKAgents update function takes a delta time, and the SKScenes update function provides a current time. I didn't spot this mismatch as they are both TimeIntervals and I was passing it directly with out converting it.

    For anyone stumbling across this the way I converted the currentTime to a deltaTime was storing a lastUpdate like this;

        var lastUpdate: TimeInterval = 0
        override func update(_ currentTime: TimeInterval) {
            super.update(currentTime)
            defer { lastUpdate = currentTime }
            guard lastUpdate != 0 else {
                return
            }
            let deltaTime = currentTime - lastUpdate
            playerAgent.update(deltaTime: deltaTime)
            enemyAgent.update(deltaTime: deltaTime)
        }
    

    I hope this helps someone, as it took me far too long to spot this. I think Apple could make this easy mistake more noticeable by printing an error if the delta time is too big or something.