In SpriteKit, is there a callback when a scene has completed its transition?
It doesn't appear like the SKView presentScene function has a callback.
The alternative is to have the scene manually notify the caller after the scene moves into view, but was hoping there is a cleaner approach with a native callback.
presentScene has no known callback when a scene has finished transitions, instead, use either Notifications or create your own delegate of some kind on your outgoing scenes func willMove(from:view) to achieve the desired effect
func willMove(from view:SKView)
{
NotificationCenter.default.post(name: "TRANSITIONCOMPLETE", object: nil)
//or create a delegate using protocols, assign the delegate, and call it
delegate?.finishedTransition()
}
Note, you must use the outgoingScenes willMove(from:view), this is the last thing to happen during the transition. didMove(to:view) on the incomingScene is the start of the transition
Related
I understand that there are 3 ways to create a scene in SpriteKit, init(), didMove, and sceneDidLoad.
But I can't understand to separate the 3 ways. Reading other questions I understood the order to call is init -> SceneDidLoad -> didMove.
How can I use them to use effectively?
You're right about the order those functions are called in. But only init(size:) actually creates a scene.
init(size:) initializes a new scene object with the given CGSize as its bounds. Anything that must be set up prior to the scene becoming visible should happen here. That's the important bit because a newly initialized scene isn't visible to the user until it's presented by a view.
sceneDidLoad() is called as a result of init(size:) and can be used to do any more setup required before the scene is presented. init(size:) can be called from wherever you want to make a new scene, but sceneDidLoad() happens in the scene itself. This is useful for any setup that you want all scenes of this class to use.
didMove(to:) is different because it doesn't have to do with the initialization. This function is called when the scene is presented by a view. Basically, when it becomes visible to the user. UI adjustments and layout for elements inside the scene are typically handled here.
Apple states in https://developer.apple.com/documentation/spritekit/skscenedelegate :
Modifying SpriteKit objects outside of the ordained callbacks (a
background queue or anything else non-main-thread) can result in
concurrency related problems. Even dispatching work on the main thread
asynchronously or at a later time is risky because the closure is
likely to be done outside of the timeframe SpriteKit expects. If
you're experiencing a segmentation fault or other type of crash
occurring deep within the SpriteKit framework, there's a good chance
your code is modifying a SpriteKit object outside of the normal
callbacks.
I'm using gesture recognizers to interact with my sprite kit objects. A simple example would be to add a SKAction to a node when the user tapped an object:
func tapAction(gr:UITapGestureRecognizer) {
scene.childNode(withName: "balloon")!.run(SKAction.fadeOut(withDuration: 2))
}
Despite the fact that this "just works" for the moment, I'm afraid that this does not work in more complicated cases.
Is there any hint from Apple that this is allowed? Or do I really have to defer the modification of the SpritKit object from the gesture action to an ordained callback?
It looks like you are safe, you are just assigning an action. That is going to run during the normal sprite kit updates
if you were manipulating the actual object, or removing a node, you would come into problems. Let's say you tap to remove a node. This tap happens right before didContactBegin. didContactBegin would expect a node, but alas, you removed it, so it will crash.
If you want to feel safe about this, then set up a queue to fire at the beginning of your update.
class GameScene : SKScene
{
public typealias Closure = ()->()
public var processOnUpdate = [Closure]()
override func update(_ currentTime: TimeInterval) {
proceseOnUpdate.forEach{$0()}
processOnUpdate = [Closure]()
....//do other stuff
}
}
//SKView Code
func tapAction(gr:UITapGestureRecognizer) {
scene.processOnUpdate.append(
{
scene.childNode(withName: "balloon")!.run(SKAction.fadeOut(withDuration: 2))
}}
}
My apologies if this does not run the first time, I am not on a Mac now to test this.
I've created this block of code to create an animation to make a selection visible. called in touchesBegan.
private func setSelectionAnim(atPoint point:CGPoint) {
selectAnim.removeActionForKey(ANIM_SELECT)
let action = SKAction.repeatActionForever( SKAction.animateWithTextures(WBTextures.Select(), timePerFrame: 0.33))
selectAnim.position = point
selectAnim.runAction(action, withKey: ANIM_SELECT)
}
WBTextures.Select() is an instance of a TexturePacker generated class and selecAnim is a SKNode() instantiated and added as a child to the GameScene in the init.
But if I cancel the the animation (when a target is selected ) with the following code:
private func stopSelectionAnim()
{
selectAnim.removeActionForKey(ANIM_SELECT)
}
The Problem is: that when i call stopSelectAnim() the last presented texture stays visible on screen. which after searching the SpriteKit Programming Guide
Canceling Running Actions To cancel actions that a node is running,
call its removeAllActions method. All actions are removed from the
node immediately. If a removed action had a duration, any changes it
already made to the node remain intact, but further changes are not
executed.
would be expected. After that a added the line selectAnim.removeFromParent() now the artifact disappears. But now the problem occurs that the next time setSelectionAnim(atPoint point:CGPoint) is called nothing happens. Logic if i deleted the node, but don't get a run time error ,so 'selectAnim' is not nil. Why does adding the extra line, solve the problem and introduces the next.
what is the best practice to interrupt an animation and still be able to enable it.
I've come up with a couple of ideas which none of I really like and fit into my scene setup.
re-instantiating and addChild' theselecAnimin the beginning of each call tosetSelectionAnim(atPoint point:CGPoint)`
create an action sequence with a runBlock() which checks a Bool whether to stop and if so how do I stop the animation with in a block
adding the animation to a new SKNode and add this as a child to the selecAnim node. ( I've tried this one but the animation did not show at all see code:
selectAnim.removeActionForKey(ANIM_SELECT)
let node = SKNode()
let action = SKAction.repeatActionForever( SKAction.animateWithTextures(WBTextures.Select(), timePerFrame: 0.33))
node.position = point
node.runAction(action, withKey: ANIM_SELECT)
selecAnim.addChild(node)
Do I miss the trivial solution or witch one is the best or do one of you please have a better solution.
Is there a way to hook into the SKNode lifecycle in Sprite Kit? Specifically I would like to perform some code when the node gets removed from the scene.
The use case I would like to solve in a bit more detail :
I have some nodes that interact with each other, and I would like them to be notified of certain events that happen to the other nodes. For example, imagine a game where you can tap a node on the scene, and the node's details would appear on a HUD. I would like the HUD to disappear when the node gets removed from the scene.
I plan to use NSNotificationCenter as the notification engine.
Whenever a node gets removed from the scene I would like to post a notification. The easiest way would be to tie into a lifecycle method on SKNode (my nodes are subclasses of SKSpriteNode) like nodeWasRemovedFromParent, but I didn't find any such method.
How can this be done?
I put some thought into coding my own solution by overriding the removeFromParent method in my SKSpriteNode subclass, and posting a notification before calling the super implementation. I am not sure that the removeFromParent method will always be called though. For example, does it get called when I change scenes?
Thanks.
You need to subclass each node class. Override the removeFromParent method as you said. Use only the subclassed versions, otherwise your code won't take effect.
In addition you will want to override removeAllChildren and removeChildrenInArray: or just never use them.
The removeFromParent method will not be called when the scene changes. Instead, override the scene's willMoveFromView: method and send a message to registered observers or simply all child nodes recursively. Use the scene's enumeration function to do so. Note that I'm not 100% sure whether on willMoveFromView the scene's children are still attached, I assume they will.
Unfortunately it's impossible to just subclass SKNode and then expect the subclass' code to work for all other node classes, because those subclass from SKNode directly and not your custom SKNode subclass. Hence you need to subclass and add this code to every SK*Node subclass as well if you need it to be notified on removal.
See KoboldKit node classes for an example, which uses a macro to inject this "override" code into SK*Node subclasses to avoid duplicating the code. The actual functionality is in KKNodeShared: https://github.com/KoboldKit/KoboldKit/tree/master/KoboldKit/KoboldKitFree/Framework/Nodes/Framework
i keep seeing these methods,
– dynamicAnimatorDidPause: required method
– dynamicAnimatorWillResume: required method
But i've not found a way to call them. Ive set up an animator and called <UIDynamicAnimatorDelegate> in the .h, but I cannot seem to call pause on the self.animator for some reason.
Anyone have any tips for me?
Thanks!
Those are delegate methods, which means that you don't call them, they get called for you. You implement a delegate to get informed about certain events
The dynamic animator pauses by itself when all movement stops. Try adding a gravity effect with no collision boundaries. The animator should pause when all dynamic items fall offscreen.