1

I need some debugging help, because an error I run into is really hard.

This is a game with complex animations. However the question isn't about SpriteKit. I want animations to follow each other in strict order, so I implemented a subclass of Operation:

class ActionOperation: Operation
{
    var debugLabel: String?

    private(set) var actionNodes: Set<ActionNode>

    //... Other vars, isFinished etc.

    init(node: SKNode, action: SKAction) {
       actionNodes = [ActionNode(node: node, action: action)]
       super.init()
    }

    init(nodesAndActions: [(SKNode?, SKAction)]) {
        actionNodes = Set(nodesAndActions.map( {(tuple) in return 
            ActionNode(node: tuple.0, action: tuple.1)
        }))
        super.init()
    }

    override func start() { /* ... */ }
}

For this class details you can see the source question. A helper struct:

extension ActionOperation {
    struct ActionNode: Hashable {
        static func ==(lhs: ActionOperation.ActionNode, rhs: ActionOperation.ActionNode) -> Bool {
            return lhs.node == rhs.node && lhs.action == rhs.action
        }

        weak var node: SKNode?

        // This constant is causing problems!
        let action: SKAction

        let setIdForHash: Int

        var hashValue: Int { return setIdForHash ^ action.hashValue }
    }
}

The Problem

The instances of the ActionOperation are added to the animationQueue. Queue setup:

fileprivate let animationQueue = OperationQueue()

// Setup:
animationQueue.maxConcurrentOperationCount = 1
animationQueue.qualityOfService = .userInteractive

Operation setup:

let duration = 0.35
var groupActions = [SKAction]()
for n in 0..<from.count {
    let fromIndex = from[n]
    let toIndex = to[n]
    let move = SKAction.move(to: fieldPosition(at: toIndex), duration: duration)
    let seq = SKAction.sequence([
        SKAction.run(move, onChildWithName: "\(fromIndex)"),
        SKAction.wait(forDuration: duration),
        SKAction.run({
            self.piece(at: fromIndex)?.name = "\(toIndex)"
        })
    ])
    groupActions.append(seq)
}
let gravOperation = ActionOperation(node: piecesLayer, action: SKAction.group(groupActions))
gravOperation.debugLabel = "gravity"
animationQueue.addOperation(gravOperation)

Sometimes this queue get stack, that means one operation is executing forever. I'm trying to debug it using Xcode command line and type this:

p (animationQueue.operations[0] as! ActionOperation).actionNodes.first!.action

(SKAction) $R6 = <uninitialized>

What does it mean? How a let constant in a struct can be uninitialized?

If I print node, everything is ok:

po (animationQueue.operations[0] as! ActionOperation).actionNodes.first!.node

<SKNode> name:'(null)' position:{0, 0} scale:{1.00, 1.00} accumulatedFrame:{{178.13499450683594, -26.469999313354492}, {908.33502197265625, 1012.9400024414062}}

kelin
  • 11,323
  • 6
  • 67
  • 104
  • Where is the initialiser of ActionOperation called? I would guess that actionNodes is nil. – colmlg Dec 13 '17 at 12:04
  • @colmlg, `actionNodes` isn't Optional. How can it be `nil`? The problem is more interesting and deep. – kelin Dec 13 '17 at 15:06
  • In the case where you get bad access, what does `po (animationQueue.operations[0] as! ActionOperation).actionNodes` show you? – Phillip Mills Dec 13 '17 at 15:33
  • @PhillipMills, the same EXC_BAD_ACCESS. – kelin Dec 13 '17 at 16:48
  • Interesting. How about `po animationQueue.operations[0]` or `po animationQueue`? (Trying to isolate exactly what is triggering the "address=0x0" part of the error message.) – Phillip Mills Dec 13 '17 at 16:51
  • @PhillipMills, it's ok. – kelin Dec 13 '17 at 16:51
  • And `animationQueue.operations[0]` says it's really an `ActionOperation` type of object? (If so, I'm baffled.) – Phillip Mills Dec 13 '17 at 16:55
  • Yes, it prints something like ``. I'm also more than puzzled by this bug. – kelin Dec 13 '17 at 17:03
  • `node?.hashValue ?? 0)` strikes me as potentially very dangerous. When a node is deallocated, the hash value of the object changes. The hash value of an object should never change, otherwise you can have really serious problems. – Sulthan Dec 13 '17 at 17:21
  • Yes, I also don't like it, I will create additional id for hashing purposes and post my results here. *Possible* that it can affect `Set` memory management. – kelin Dec 13 '17 at 17:26
  • I updated the question. Now `hashValue` of `ActionNode` is more safe. All problems is still here. It's interesting that `po (animationQueue.operations[0] as! ActionOperation).actionNodes.count` outputs "1". (While `actionNodes` leads to bad access). – kelin Dec 13 '17 at 17:40
  • try to use `p` instead of `po` for debugging structs – Sulthan Dec 13 '17 at 18:21
  • @Sulthan, see the update. – kelin Dec 13 '17 at 18:32
  • I think I resolved the issue. I will post the answer later. – kelin Dec 14 '17 at 10:04

1 Answers1

0

Well, the problem was caused by the way I crate group action:

let gravOperation = ActionOperation(node: piecesLayer, action: SKAction.group(groupActions))

When groupActions array is empty action will never be executed on the node and if we call:

node.run(badGroupAction, completion: { /* never called */ })

the completion will be never called. That's why queue was stuck. I think it's a SpriteKit flaw. I would prefer completion to be called immediately if we try to run empty group or sequence.

Why does action become uninitialized? I don't know, I think it's a bug, probably related to the empty array.

kelin
  • 11,323
  • 6
  • 67
  • 104