2016-07-11 38 views
7

Bug - hit-test non funziona come previsto quando i fratelli si sovrappongono:Bug: hit-test con i nodi di pari livello e la proprietà userInteractionEnabled in Sprite Kit

Ci sono 2 nodi sovrapposti in una scena che hanno la stesso genitore (cioè fratelli)

Il nodo più in alto ha userInteractionEnabled = NO mentre l'altro nodo ha userInteractionEnabled = YES.

Se la sovrapposizione viene sfiorata, dopo che il nodo più in alto è stato colpito al test e fallisce (perché userInteractionEnabled = NO), anziché il nodo inferiore è il prossimo ad essere testato, viene saltato e il genitore dei 2 fratelli è hit-test.

Quello che dovrebbe accadere è che il fratello successivo (il nodo in basso) viene colpito dal problema piuttosto che il test per colpire il genitore.

Secondo la documentazione Sprite Kit:

"In una scena, quando Kit Sprite elabora gli eventi di tocco o del mouse, cammina la scena per trovare il nodo più vicino che vuole accettare l'evento Se quel nodo non vuole l'evento, Sprite Kit controlla il prossimo nodo più vicino e quindi su. L'ordine in cui viene elaborato hit-testing è essenzialmente il contrario dell'ordine di disegno. Per un nodo da considerare durante il colpo -testing, la proprietà userInteractionEnabled deve essere impostata su YES. Il valore predefinito i s NO per qualsiasi nodo tranne un nodo di scena. "


Si tratta di un bug come fratelli di un nodo vengono resi prima che i loro genitori - un fratello dovrebbe essere il prossimo ad essere testato, e non il suo genitore. Inoltre, se un nodo ha userInteractionEnabled = NO, allora sicuramente dovrebbe essere "trasparente" per quanto riguarda il hit-testing - ma qui non è come si traduce in un cambiamento di comportamento come un nodo viene saltato nel test.

Ho cercato online, ma non riesco a trovare nessuno che abbia segnalato o pubblicato su questo bug. Quindi dovrei segnalarlo?


E poi il motivo per cui ho postato questo qui è perché vorrei un suggerimento per una 'correzione' di questo bug (cioè. Un suggerimento per l'implementazione di un codice da qualche parte in modo che lavora a SpriteKit alla maniera 'destinato' per hit-testing)


per replicare il bug:

Utilizzare il modello "Ciao mondo" previsto quando si avvia un nuovo progetto "Game" in Xcode (che ha "Hello World" e aggiunge sprite di razzi quando fai clic).

Opzionale: [Ho anche cancellato l'immagine razzo sprite dal progetto come il rettangolo con il X che si verifica quando l'immagine non si trova è più facile lavorare con per il debugging, visivamente]

Aggiungi un SKSpriteNode alla scena con userInteractionEnabled = YES (Mi riferirò come nodo A da ora in poi).

Eseguire il codice.

Noterai che quando fai clic sul nodo A, non vengono generati gli sprite del razzo. (comportamento atteso dal momento in cui il hit-test dovrebbe fermarsi dopo che ha avuto successo - si ferma come succede sul nodo A.) ​​

Tuttavia, se si generano alcuni razzi che si trovano accanto al nodo A, quindi fare clic su un luogo dove il Nodo A e un razzo si sovrappongono, è quindi possibile generare un altro razzo sopra il Nodo A - ma questo non dovrebbe essere possibile. Ciò significa che dopo che il hit-test fallisce sul nodo più in alto (il razzo che ha userInteractionEnabled = NO per impostazione predefinita), invece di testare il nodo A successivo, verifica invece il genitore del razzo, che è la scena.


Nota: io sto usando Xcode 7.3.1, Swift, iOS - non ho ancora testato per vedere se questo bug è universale, ancora.


dettaglio Extra: Ho fatto qualche debug aggiuntivo (piccola complicazione per la replica sopra) e determinato che l'hit-test viene inviato al genitore successivamente e quindi non necessariamente alla scena.

+0

Apprezzerei ogni e qualsiasi aiuto con questo problema che ho - non oso iniziare il mio nuovo progetto fino a quando questo viene risolto come tutti i tipi di insetti possono verificarsi grazie ad essa quando i nodi si sovrappongono. Sicuramente altri hanno notato/sperimentato questo bug quando realizzano dei progetti perché è piuttosto fondamentale? – Shuri2060

+0

Non sono sicuro se considererei questo un bug ... dipende molto dal contesto di gioco in quale direzione preferibilmente. Potrei facilmente immaginare un gioco in cui se uno sprite blocca l'altro ha senso che quello sotto non venga chiamato, ma invece viene controllata un'azione generica di scena. – GOR

+0

@GOR No, deve essere un bug. Il comportamento va contro ciò che dice la documentazione, il che significa che non è intenzionale. Inoltre, non ha alcun senso logico perché la proprietà 'userInteractionEnabled' è intesa a rappresentare la trasparenza per quanto riguarda il hit-testing, che fallisce a causa di questo comportamento. – Shuri2060

risposta

3

Sospetto sia un errore o la documentazione non sia corretta. In ogni caso, ecco una soluzione che potrebbe essere ciò che stai cercando.

Sembra che si desidera interagire con un nodo che può essere

  1. oscurato da uno o più nodi che hanno userInteractionEnabled proprietà impostata su false
  2. un figlio di un "background" nodo
  3. in profondità nell'albero dei nodi

nodesAtPoint è un buon punto di partenza. Restituisce una serie di nodi che interseca il punto di tocco. Aggiungere questo alla scena del touchesBegan e filtrare i nodi che non hanno userInteractionEnabled insieme a true da

let nodes = nodesAtPoint(location).filter { 
    $0.userInteractionEnabled 
} 

A questo punto, è possibile ordinare l'array di nodi dalla profondità zPositione nodo-albero. È possibile utilizzare la seguente estensione per determinare queste proprietà per un nodo:

extension SKNode { 
    var depth:(level:Int,z:CGFloat) { 
     var node = parent 
     var level = 0 
     var zLevel:CGFloat = zPosition 
     while node != nil { 
      zLevel += node!.zPosition 
      node = node!.parent 
      level += 1 
     } 
     return (level, zLevel) 
    } 
} 

e ordinare l'array con

let nodes = nodesAtPoint(location) 
    .filter {$0.userInteractionEnabled} 
    .sort {$0.depth.z == $1.depth.z ? $0.depth.level > $1.depth.level : $0.depth.z > $1.depth.z} 

Per verificare il codice sopra, definire una sottoclasse SKSpriteNode che consente l'interazione dell'utente

class Sprite:SKSpriteNode { 
    var offset:CGPoint? 
    // Save the node's relative location 
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { 
     if let touch = touches.first { 
      let location = touch.locationInNode(self) 
      offset = location 
     } 
    } 
    // Allow the user to drag the node to a new location 
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { 
     if let touch = touches.first, parentNode = parent, relativePosition = offset { 
      let location = touch.locationInNode(parentNode) 
      position = CGPointMake(location.x-relativePosition.x, location.y-relativePosition.y) 
     } 
    } 
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { 
     offset = nil 
    } 
} 

e aggiungere i seguenti gestori di tocco alla sottoclasse SKScene

var selectedNode:SKNode? 

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { 
    if let touch = touches.first { 
     let location = touch.locationInNode(self) 
     // Sort and filter nodes that intersect with location 
     let nodes = nodesAtPoint(location) 
      .filter {$0.userInteractionEnabled} 
      .sort {$0.depth.z == $1.depth.z ? $0.depth.level > $1.depth.level : $0.depth.z > $1.depth.z} 
     // Forward the touch events to the appropriate node 
     if let first = nodes.first { 
      first.touchesBegan(touches, withEvent: event) 
      selectedNode = first 
     } 
    } 
} 

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { 
    if let node = selectedNode { 
     node.touchesMoved(touches, withEvent: event) 
    } 
} 

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { 
    if let node = selectedNode { 
     node.touchesEnded(touches, withEvent: event) 
     selectedNode = nil 
    } 
} 

Il seguente filmato mostra come il codice precedente può essere utilizzato per trascinare gli sprites che si trovano sotto altri sprite (con userInteractionEnabled = true). Nota che anche se gli sprite sono figli dello sprite di sfondo blu che copre l'intera scena, lo touchesBegan della scena viene chiamato quando un utente trascina uno sprite.

enter image description here

2

La discrepanza sembra verificarsi perché SKView esempio del modello ha la sua serie ignoresSiblingOrder proprietà true nell'attuazione di viewDidLoad su GameViewController. Questa proprietà è false per impostazione predefinita.

Dalla documentazione:

Un valore booleano che indica se genitore-figlio e fratelli relazioni influenzano l'ordine di rendering dei nodi nella scena. Il valore predefinito è false, il che significa che quando più nodi condividono la stessa posizione z, tali nodi vengono ordinati e sottoposti a rendering in un ordine deterministico. I genitori sono resi prima dei loro figli, ei fratelli vengono resi in ordine di schiera. Quando questa proprietà è impostata su true, la posizione dei nodi nell'albero viene ignorata quando si determina l'ordine di rendering. L'ordine di rendering dei nodi nella stessa posizione z è arbitrario e può cambiare ogni volta che viene eseguito il rendering di un nuovo frame. Quando il fratello e l'ordine genitore vengono ignorati, SpriteKit applica ulteriori ottimizzazioni per migliorare le prestazioni di rendering. Se è necessario eseguire il rendering dei nodi in un ordine specifico e deterministico, è necessario impostare la posizione z di tali nodi.

Quindi nel tuo caso dovresti essere in grado di eliminare semplicemente questa riga per ottenere il comportamento normale. Come notato da @Fujia nei commenti, questo dovrebbe influenzare solo l'ordine di rendering, non il test dei risultati.

Come UIKit, i discendenti diretti di SpriteKit di UIResponder implementano presumibilmente i suoi metodi di gestione del tocco per inoltrare gli eventi lungo la catena di risposta. Quindi questa incoerenza può essere causata dall'implementazione di questi metodi sostituita su SKNode. Se sei ragionevolmente sicuro che il problema risieda in questi metodi ereditati, puoi risolvere il problema sovrascrivendoli con la tua logica di inoltro degli eventi. Se è così, sarebbe anche carino presentare una segnalazione di bug con il tuo progetto di test.

+3

'ignoresSiblingOrder' ha effetto solo sul comportamento di rendering, non su hit testing. – Fujia

+0

Sono d'accordo con @Fujia - L'ho provato con/senza 'ignoresSiblingOrder' e non ha fatto alcuna differenza.Anche se fosse così, e l'ordine di parentela è stato scambiato, l'evento sopra non si sarebbe verificato - il hit-test non avrebbe dovuto saltare un nodo fratello in entrambi i casi. – Shuri2060

+0

In tal caso, consiglierei di ricontrollare l'implementazione del nodo figlio dei metodi 'UIResponder'. Se quelli sembrano corretti, però, questo potrebbe effettivamente essere un bug SpriteKit. –

2

È possibile risolvere il problema sovrascrivendo la scena mouseDown (o eventi di tocco equivalenti) come indicato di seguito.Fondamentalmente si controllano i nodi nel punto e si trova quello che ha il più alto zPosition e userInteractionEnabled. Questo funziona come fallback per la situazione quando non si dispone di un nodo come la posizione più alta per cominciare.

override func mouseDown(theEvent: NSEvent) { 
    /* Called when a mouse click occurs */ 
    let nodes = nodesAtPoint(theEvent.locationInNode(self)) 

    var actionNode : SKNode? = nil 
    var highestZPosition = CGFloat(-1000) 

    for n in nodes 
    { 
     if n.zPosition > highestZPosition && n.userInteractionEnabled 
     { 
      highestZPosition = n.zPosition 
      actionNode = n 
     } 
    } 

    actionNode?.mouseDown(theEvent) 
} 
+0

Grazie. Sto indovinando che l'array 'nodes' è ordinato nell'ordine in cui dovrebbero essere controllati (dal padre più in basso che è la scena del nodo più in alto)? Tuttavia, sicuramente questo non funziona affatto: il 'mouseDown' della scena non viene chiamato quando ci sono nodi in un punto in quanto un clic non 'buca' attraverso tutto. – Shuri2060

+0

Sicuro funziona. Se è presente un nodo nel percorso con userInteractionEnabled, verrà chiamato il mouseDown di quel nodo. Se il nodo più in alto in quella posizione è qualcosa che ha userInteractionEnabled = false, la scena viene chiamata come hai notato e quindi questa funzione. Se non ci sono nodi nella posizione, viene chiamata anche la scena. Funziona, provalo. Per quanto riguarda la domanda sull'ordine dei nodi; questo è il motivo per cui le zPosition vengono confrontate nel ciclo. – GOR

+0

1. Quando le posizioni z sono uguali, l'ordine dei nodi deve essere confrontato con l'ordine di rendering, che sicuramente manca a meno che 'nodesAtPoint (...)' non indichi l'ordine corretto. 2. Il problema che sto pensando è che questa 'patch' verrà chiamata solo quando viene chiamato 'mouseDown' della scena. Se aggiungo un nodo 'blanket' che copre l'intera scena, e aggiusto il codice in modo che tutto venga aggiunto a questa 'coperta' invece che alla scena, allora il problema sopra descritto si verificherà ancora. Il 'mouseDown' della scena non viene necessariamente chiamato se il nodo più in alto non lo ha come genitore. – Shuri2060