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

时间:2015-12-31 17:39:31

标签: swift scenekit gameplay-kit

我正在尝试在场景周围移动SCNNode,约束为GKGridGraph。按照PacMan的想法思考但是用3D。

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

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

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

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

    移动到GKGridGraphNode

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

  4. 创建moveTo操作
  5. 创建一个调用runBlock方法
  6. move()
  7. moveTorunBlock操作作为序列应用于SCNNode
  8. 我已经粘贴在下面的全班中。但我会解释我先遇到的问题。上述逻辑有效,但只是间歇性的。有时动画停止工作几乎immediatley,有时它运行长达一分钟。但在某些时候,由于某些原因,它只是停止工作 - 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!)
    
        }
    
    }
    

1 个答案:

答案 0 :(得分:4)

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

  1. 我的组合试图做太多
  2. 我没有使用updateWithDeltaTime方法。
  3. 这就是使用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!)
    
        }
    
    }
    

    GameViewController

    这里的一切都在一起。课堂上有很多内容,所以我只发布相关的内容。

    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有一个非常方便的updatet方法。

    对于SceneKit,我们必须为SCNSceneRendererDelegate设置GameVierwController SCNView。然后,使用rendererUpdateAtTime方法,我们可以在实体管理器上调用updateAtDeltaTime,它处理每帧对所有实体和组件调用相同的方法。

    注意您必须手动将playing属性设置为true才能生效。

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

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

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