首页 文章

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

提问于
浏览
6

我试图围绕一个场景移动 SCNNode ,约束为 GKGridGraph . 按照PacMan的思路,但在3D中 .

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

  • 设置 queuedDirection 属性 .

  • 如果 isMoving 标志为false,则调用move()方法

  • 使用GKGridGraph评估下一步行动

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

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

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

  • 创建 moveTo 动作

  • 创建 runBlock ,调用 move() 方法

  • moveTorunBlock 操作作为序列应用于SCNNode

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

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

这里's the full class, but I think the important bit'是 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 回答

  • 3

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

    • 我的组合试图做太多

    • 我没有使用 updateWithDeltaTime 方法 .

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

    NodeComponent

    该组件负责管理代表我的游戏角色的实际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 方法 .

    控制组件

    我最初的例子是试图做太多 . 这个组件现在唯一的责任就是为它评估下一个's entity' 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,我们必须为 SCNView 设置GameVierwController SCNSceneRendererDelegate . 然后,使用 rendererUpdateAtTime 方法,我们可以在实体管理器上调用 updateAtDeltaTime ,该处理器每帧处理对所有实体和组件调用相同的方法 .

    Note 您必须手动将 playing 属性设置为true才能使其生效 .

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

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

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

相关问题