为什么我的 SCNAction 序列间歇性地停止工作?

Why does my SCNAction sequence stop working intermittently?

我正在尝试在场景中移动 SCNNode,限制为 GKGridGraph。沿着 PacMan 的路线思考,但在 3D 中。

我有一个 ControlComponent 来处理我的 SCNNode 的移动。逻辑应该是这样的...

  1. 设置 queuedDirection 属性.
  2. 如果isMoving标志为假,调用move()方法
  3. 使用 GKGridGraph 评估下一步行动

    3.1 如果实体可以使用direction 属性, nextNode = nodeInDirection(direction)

    移动到GKGridGraphNode

    3.2 如果实体可以使用queuedDirection 属性 nextNode = nodeInDirection(queuedDirection)

    移动到GKGridGraphNode

    3.3 如果实体无法移动到任一方向的节点,请将isMoving 标志设置为false 并设置return。

  4. 创建 moveTo 操作
  5. 创建一个 runBlock 调用 move() 方法
  6. moveTorunBlock 操作作为序列应用于 SCNNode

我已将完整的 class 粘贴在下方。但我会先解释我遇到的问题。上述逻辑有效,但只是间歇性的。有时动画几乎立即停止工作,有时它会运行长达一分钟。但在某些时候,出于某种原因,它只是停止工作 - setDirection() 将触发,move() 将触发,SCNNode 将在指定方向移动一次 space 然后 move() 方法刚刚停止被调用。

我不是 100% 相信我目前的方法是正确的,所以我很高兴听到是否有更惯用的 SceneKit/GameplayKit 方法来做到这一点。

这是完整的 class,但我认为重要的是 setDirection()move() 方法。

import GameplayKit
import SceneKit

enum BRDirection {
    case Up, Down, Left, Right, None
}

class ControlComponent: GKComponent {

    var level: BRLevel!
    var direction: BRDirection = .None
    var queuedDirection: BRDirection?
    var isMoving: Bool = false
    var speed: NSTimeInterval = 0.5

    //----------------------------------------------------------------------------------------

    init(level: BRLevel) {
        self.level = level
        super.init()
    }

    //----------------------------------------------------------------------------------------

    func setDirection( nextDirection: BRDirection) {
        self.queuedDirection = nextDirection;
        if !self.isMoving {
            self.move()
        }
    }

    //----------------------------------------------------------------------------------------

    func move() {

        let spriteNode: SCNNode = (self.entity?.componentForClass(NodeComponent.self)!.node)!
        var nextNode = nodeInDirection( direction )

        if let _ = self.queuedDirection {
            let attemptedNextNode = nodeInDirection(self.queuedDirection! )
            if let _ = attemptedNextNode {
                nextNode = attemptedNextNode
                self.direction = self.queuedDirection!
                self.queuedDirection = nil
            }
        }

        // Bail if we don't have a valid next node
        guard let _ = nextNode else {
            self.direction = .None
            self.queuedDirection = nil
            self.isMoving = false
            return
        }

        // Set flag
        self.isMoving = true;

        // convert graphNode coordinates to Scene coordinates
        let xPos: Float = Float(nextNode!.gridPosition.x) + 0.5
        let zPos: Float = Float(nextNode!.gridPosition.y) + 0.5
        let nextPosition: SCNVector3 = SCNVector3Make(xPos, 0, zPos)

        // Configure actions
        let moveTo = SCNAction.moveTo(nextPosition, duration: speed)
        let repeatAction = SCNAction.runBlock( { _ in self.move() } )
        let sequence = SCNAction.sequence([ moveTo, repeatAction ])
        spriteNode.runAction( sequence )

    }


    //----------------------------------------------------------------------------------------

    func getCurrentGridGraphNode() -> GKGridGraphNode {

        // Acces the node in the scene and gett he grid positions
        let spriteNode: SCNNode = (self.entity?.componentForClass(NodeComponent.self)!.node)!

        // Account for visual offset
        let currentGridPosition: vector_int2 = vector_int2(
            Int32( floor(spriteNode.position.x) ),
            Int32( floor(spriteNode.position.z) )
        )

        // return unwrapped node
        return level.gridGraph.nodeAtGridPosition(currentGridPosition)!

    }


    //----------------------------------------------------------------------------------------

    func nodeInDirection( nextDirection:BRDirection? ) -> GKGridGraphNode? {
        guard let _ = nextDirection else { return nil }
        let currentGridGraphNode = self.getCurrentGridGraphNode()
        return self.nodeInDirection(nextDirection!, fromNode: currentGridGraphNode)
    }

    //----------------------------------------------------------------------------------------


    func nodeInDirection( nextDirection:BRDirection?, fromNode node:GKGridGraphNode ) -> GKGridGraphNode? {

        guard let _ = nextDirection else { return nil }

        var nextPosition: vector_int2?

        switch (nextDirection!) {
        case .Left:
            nextPosition = vector_int2(node.gridPosition.x + 1, node.gridPosition.y)
            break

        case .Right:
            nextPosition = vector_int2(node.gridPosition.x - 1, node.gridPosition.y)
            break

        case .Down:
            nextPosition = vector_int2(node.gridPosition.x, node.gridPosition.y - 1)
            break

        case .Up:
            nextPosition = vector_int2(node.gridPosition.x, node.gridPosition.y + 1)
            break;

        case .None:
            return nil
        }

        return level.gridGraph.nodeAtGridPosition(nextPosition!)

    }

}

我必须回答我自己的问题。首先,这是一个糟糕的问题,因为我试图做错事。我犯的两个主要错误是

  1. 我的组件试图做太多事情
  2. 我没有使用 updateWithDeltaTime 方法。

这就是使用 GameplayKit 的实体组件结构构建代码和行为的方式。最后我会尝试说明所有价格是如何组合在一起的。

节点组件

该组件负责管理代表我的游戏角色的实际 SCNNode。我已将角色动画代码从 ControlComponent 移到此组件中。

class NodeComponent: GKComponent {

    let node: SCNNode
    let animationSpeed:NSTimeInterval = 0.25
    var nextGridPosition: vector_int2 {

        didSet {
            makeNextMove(nextGridPosition, oldPosition: oldValue)
        }

    }

    init(node:SCNNode, startPosition: vector_int2){
        self.node = node
        self.nextGridPosition = startPosition
    }


    func makeNextMove(newPosition: vector_int2, oldPosition: vector_int2) {

        if ( newPosition.x != oldPosition.x || newPosition.y != oldPosition.y ){

            let xPos: Float = Float(newPosition.x)
            let zPos: Float = Float(newPosition.y)
            let nextPosition: SCNVector3 = SCNVector3Make(xPos, 0, zPos)
            let moveTo =  SCNAction.moveTo(nextPosition, duration: self.animationSpeed)

            let updateEntity = SCNAction.runBlock( { _ in
                (self.entity as! PlayerEntity).gridPosition = newPosition
            })

            self.node.runAction(SCNAction.sequence([moveTo, updateEntity]), forKey: "move")

        }

    }

}

注意,每次设置组件gridPosition 属性,都会调用makeNextMove方法。

控制组件

我最初的例子是想做太多。该组件现在的唯一责任是为其实体的 NodeComponent 评估下一个 gridPosition。请注意,由于 updateWithDeltaTime,它会在调用该方法时尽可能频繁地评估下一步。

class ControlComponent: GKComponent {

    var level: BRLevel!
    var direction: BRDirection = .None
    var queuedDirection: BRDirection?


    init(level: BRLevel) {
        self.level = level
        super.init()
    }

    override func updateWithDeltaTime(seconds: NSTimeInterval) {
        self.evaluateNextPosition()
    }


    func setDirection( nextDirection: BRDirection) {
        self.queuedDirection = nextDirection
    }


    func evaluateNextPosition() {
        var nextNode = self.nodeInDirection(self.direction)

        if let _ = self.queuedDirection {

            let nextPosition = self.entity?.componentForClass(NodeComponent.self)?.nextGridPosition
            let targetPosition = (self.entity! as! PlayerEntity).gridPosition
            let attemptedNextNode = self.nodeInDirection(self.queuedDirection)

            if (nextPosition!.x == targetPosition.x && nextPosition!.y == targetPosition.y){
                if let _ = attemptedNextNode {
                    nextNode = attemptedNextNode
                    self.direction = self.queuedDirection!
                    self.queuedDirection = nil
                }
            }

        }

        // Bail if we don't have a valid next node
        guard let _ = nextNode else {
            self.direction = .None
            return
        }

        self.entity!.componentForClass(NodeComponent.self)?.nextGridPosition = nextNode!.gridPosition

    }


    func getCurrentGridGraphNode() -> GKGridGraphNode {
        // Access grid position
        let currentGridPosition = (self.entity as! PlayerEntity).gridPosition

        // return unwrapped node
        return level.gridGraph.nodeAtGridPosition(currentGridPosition)!
    }


    func nodeInDirection( nextDirection:BRDirection? ) -> GKGridGraphNode? {
        guard let _ = nextDirection else { return nil }
        let currentGridGraphNode = self.getCurrentGridGraphNode()
        return self.nodeInDirection(nextDirection!, fromNode: currentGridGraphNode)
    }


    func nodeInDirection( nextDirection:BRDirection?, fromNode node:GKGridGraphNode? ) -> GKGridGraphNode? {

        guard let _ = nextDirection else { return nil }
        guard let _ = node else { return nil }

        var nextPosition: vector_int2?

        switch (nextDirection!) {
        case .Left:
            nextPosition = vector_int2(node!.gridPosition.x + 1, node!.gridPosition.y)
            break

        case .Right:
            nextPosition = vector_int2(node!.gridPosition.x - 1, node!.gridPosition.y)
            break

        case .Down:
            nextPosition = vector_int2(node!.gridPosition.x, node!.gridPosition.y - 1)
            break

        case .Up:
            nextPosition = vector_int2(node!.gridPosition.x, node!.gridPosition.y + 1)
            break;

        case .None:
            return nil
        }

        return level.gridGraph.nodeAtGridPosition(nextPosition!)

    }

}

游戏视图控制器

一切都在这里拼凑起来。 class 中发生了很多事情,所以我 post 只介绍相关的内容。

class GameViewController: UIViewController, SCNSceneRendererDelegate {


    var entityManager: BREntityManager?
    var previousUpdateTime: NSTimeInterval?;
    var playerEntity: GKEntity?

    override func viewDidLoad() {
        super.viewDidLoad()

        // create a new scene
        let scene = SCNScene(named: "art.scnassets/game.scn")!

        // retrieve the SCNView
        let scnView = self.view as! SCNView

        // set the scene to the view
        scnView.scene = scene

        // show statistics such as fps and timing information
        scnView.showsStatistics = true

        scnView.delegate = self

        entityManager = BREntityManager(level: level!)

        createPlayer()

        configureSwipeGestures()

        scnView.playing = true

    }

    func createPlayer() {

        guard let playerNode = level!.scene.rootNode.childNodeWithName("player", recursively: true) else {
            fatalError("No player node in scene")
        }

        // convert scene coords to grid coords
        let scenePos = playerNode.position;
        let startingGridPosition = vector_int2(
            Int32( floor(scenePos.x) ),
            Int32( floor(scenePos.z) )
        )

        self.playerEntity = PlayerEntity(gridPos: startingGridPosition)
        let nodeComp = NodeComponent(node: playerNode, startPosition: startingGridPosition)
        let controlComp = ControlComponent(level: level!)
        playerEntity!.addComponent(nodeComp)
        playerEntity!.addComponent(controlComp)
        entityManager!.add(playerEntity!)

    }

    func configureSwipeGestures() {
        let directions: [UISwipeGestureRecognizerDirection] = [.Right, .Left, .Up, .Down]
        for direction in directions {
            let gesture = UISwipeGestureRecognizer(target: self, action: Selector("handleSwipe:"))
            gesture.direction = direction
            self.view.addGestureRecognizer(gesture)
        }
    }

    func handleSwipe( gesture: UISwipeGestureRecognizer ) {

        let controlComp = playerEntity!.componentForClass(ControlComponent.self)!

        switch gesture.direction {
        case UISwipeGestureRecognizerDirection.Up:
            controlComp.setDirection(BRDirection.Up)
            break

        case UISwipeGestureRecognizerDirection.Down:
            controlComp.setDirection(BRDirection.Down)
            break

        case UISwipeGestureRecognizerDirection.Left:
            controlComp.setDirection(BRDirection.Left)
            break

        case UISwipeGestureRecognizerDirection.Right:
            controlComp.setDirection(BRDirection.Right)
            break

        default:
            break
        }

    }

    // MARK: SCNSceneRendererDelegate

    func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
        let delta: NSTimeInterval
        if let _ = self.previousUpdateTime {
            delta = time - self.previousUpdateTime!
        }else{
            delta = 0.0
        }

        self.previousUpdateTime = time

        self.entityManager!.update(withDelaTime: delta)

    }

}

实体管理器

我在这个 Ray Wenderlich tutorial 之后得到了这个提示。从本质上讲,它是一个存储所有实体和组件的桶,以简化管理和更新它们的工作。我强烈建议对该教程进行通读以更好地理解这一点。

class BREntityManager {

    var entities = Set<GKEntity>()
    var toRemove = Set<GKEntity>()
    let level: BRLevel

    //----------------------------------------------------------------------------------

    lazy var componentSystems: [GKComponentSystem] = {
        return [
            GKComponentSystem(componentClass: ControlComponent.self),
            GKComponentSystem(componentClass: NodeComponent.self)
        ]
    }()

    //----------------------------------------------------------------------------------

    init(level:BRLevel) {
        self.level = level
    }

    //----------------------------------------------------------------------------------

    func add(entity: GKEntity){

        if let node:SCNNode = entity.componentForClass(NodeComponent.self)!.node {
            if !level.scene.rootNode.childNodes.contains(node){
                level.scene.rootNode.addChildNode(node)
            }
        }

        entities.insert(entity)

        for componentSystem in componentSystems {
            componentSystem.addComponentWithEntity(entity)
        }
    }

    //----------------------------------------------------------------------------------

    func remove(entity: GKEntity) {

        if let _node = entity.componentForClass(NodeComponent.self)?.node {
            _node.removeFromParentNode()
        }

        entities.remove(entity)
        toRemove.insert(entity)
    }

    //----------------------------------------------------------------------------------

    func update(withDelaTime deltaTime: NSTimeInterval) {

        // update components
        for componentSystem in componentSystems {
            componentSystem.updateWithDeltaTime(deltaTime)
        }

        // remove components
        for curRemove in toRemove {
            for componentSystem in componentSystems {
                componentSystem.removeComponentWithEntity(curRemove)
            }
        }

        toRemove.removeAll()
    }

} 

那么所有这些如何组合在一起

ControlComponent.setDirection()方法可以随时调用。

如果实体或组件实现了 updateWithDeltaTime 方法,则应在每一帧调用它。我花了一点时间才弄清楚如何使用 SceneKit 执行此操作,因为大多数 GameplayKit 示例都是为 SpriteKit 设置的,而 SKScene 有一个非常方便的更新方法。

对于 SceneKit,我们必须使 GameVierwController 成为 SCNViewSCNSceneRendererDelegate。然后,使用 rendererUpdateAtTime 方法,我们可以在实体管理器上调用 updateAtDeltaTime,实体管理器负责在每一帧对所有实体和组件调用相同的方法。

注意 您必须手动将 playing 属性 设置为 true 才能正常工作。

现在我们进入实际的动画。 ControlComponent 正在评估 NodeComponents 下一个网格位置应该是每帧的位置,使用 direction 属性 (您可以忽略 queuedDirection 属性,这是一个实现细节)。

这意味着 NodeComponents.makeNextMove() 方法也在每一帧被调用。只要 newPositionoldPosition 不同,就会应用动画。每当动画结束时,节点的 gridPosition 就会更新。

现在,至于为什么我的初始 SCNNode 动画方法不起作用,我不知道。但至少它迫使我更好地理解如何使用 GameplayKit 的 Entity/Component 设计。