【问题标题】:Why does my SCNAction sequence stop working intermittently?为什么我的 SCNAction 序列间歇性地停止工作?
【发布时间】:2015-12-31 17:39:30
【问题描述】:

我正在尝试在场景中移动 SCNNode,并限制为 GKGridGraph。沿用 PacMan 的思路,但以 3D 形式思考。

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

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

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

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

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

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

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

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

这是完整的课程,但我认为重要的是 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!)

    }

}

【问题讨论】:

    标签: swift scenekit gameplay-kit


    【解决方案1】:

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

    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 方法。

    控制组件

    我最初的例子是试图做太多事情。该组件现在的唯一责任是评估下一个gridPosition 的实体NodeComponent。请注意,由于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 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 设置为 SCNSceneRendererDelegate 以用于 SCNView。然后,使用rendererUpdateAtTime 方法,我们可以在实体管理器上调用updateAtDeltaTime,它处理每帧对所有实体和组件调用相同的方法。

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2014-07-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-08
      • 1970-01-01
      • 2021-10-31
      • 2016-01-10
      相关资源
      最近更新 更多