How to control the speed of nodes in SpriteKit - swift

I have a bunch of objects (SKNode) starting from the top of the screen 'falling' to the bottom via SKAction.move(to:duration:) and node.run(moveAction). In addition I have node in the center of the screen with its own physics body that can be dragged side to side with touch input. I can detect collisions fine, but I was wondering if there was a paradigm for 'pausing' all the objects while the center node is contact with any of the objects. Additionally, I want to able to move the center node while the other objects are 'paused' so I can move it out of the way and then let the objects resume their motion. I figured I could probably iterate through all the existing objects and set their isPaused property, but I am unsure how the app would know when the center node is no longer 'colliding' so I can toggle the property back.

To pause things, you will have to detect the contact in didBegin() , tofill some array with nodes that should be paused and finally to pause the nodes. Actual pausing could be done in didSimulatePhysics() for example. To pause all the nodes you could use
self.enumerateChildNodesWithName("aName") {
node, stop in
// do something with node or stop
}
or use children property of a node and loop through it (eg. to loop through the container of your nodes that should be paused).
Also you can pause certain action with:
if let action = square.actionForKey("aKey") {
action.speed = 0
}
and unpause it with action.speed = 1, or make it slow-mo with action.speed = 0.5
To slow down physics simulation, there is a property called physicsWorld.speed (determines the rate at which the simulation runs).

Oh boy, things sure do get complicated. Every thing that Whirlwind says is correct, but to iterate through every node on the scene can become burdensome.
Instead, I recommend creating a special subclass for SKNode. Now this special class is needed due to a bug caused by apples framework
class UserPausableNode : SKNode
{
public var userPaused = false
{
didSet
{
super.isPaused = userPaused
}
}
override var isPaused : Bool
{
get
{
return userPaused
}
set
{
//Yes, do nothing here
}
}
}
The reason why we need to do this is because whenever isPaused is called, it iterates to all children and sets the child isPaused property.
This becomes a problem when scene pauses and unpauses, as it will change the pause state of the children, which we do not want to be doing.
This class prevents the isPaused variable from changing.
Now that this class exists, what we want to do is add it to the scene, and add all moving nodes to this new node.
Since all of the nodes are on this new node, all we need to do is set the userPaused to true, and this will stop all nodes.
Now make sure the node you are moving is not a part of this branch, it should be outside so that you could move it.
Example
class GameScene : SKScene
{
var userBranch = UserPausableNode()
func didMove(to view:SKView)
{
self.addChild(userBranch)
//generetedChildren are nodes that you create that you plan on pausing as a group
for generatedChild in generatedChildren
{
userBranch.addChild(node)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//this will pause and unpause based on opposite of previous state
userBranch.userPaused = !userBranch.userPaused
}
}

Related

Weird behavior of SKAction on children

Well, my aim is to extract the content of a SKScene and then run some actions to their node:
func loadSceneNodes() {
let loadedScene = SKScene(fileNamed: "newScene.sks")
// Reads the content of the "Content" node in the newScene.sks
let content = loadedScene!.childNode(withName: "Content") as! SKSpritenode
// I move the content to self
// (THANKS Knight0fDragon for suggestion)
content.move(toParent: self)
// Forces the unpause
self.isPause = false
// Try to run a test action on the node
content.run(.rotate(byAngle: 90, duration: 2.0)) {
print("DONE")
}
print(content.hasActions()) // DEBUG -> it prints TRUE
}
What happens is that I successful read the content and it shows on the scene, but the actions aren't shown even if hasActions() results true.
I found out something very interesting by playing with breakpoints and the debug console. content.hasActions() returns true because all of the SKActions get into a stack:
They seem like freeze because if I assign more actions, they will get stack in the pile one by one without getting run.
Only once I quit the breakpoint (or whether I pause and then play the application through xCode) everything is executed. Just like the app "needs" a refresh.
Use moveToParent on your sknode instead of addChild on your parent node to avoid the need to create a copy.
Ensure the isPaused state is set to false, you may be copying at a time where it is true

Swift: Event for when node leaves SKCameraNode's view?

Suppose:
you have an SKSpriteNode, node, off doing its own thing
you have implemented an SKCameraNode, cam, to dictate what the player can see
node has the function:
func onScreen(camera:SKCameraNode) -> Bool {
if camera.contains(self) { return true }
else { return false }
}
Question
How can one add an event listener to node so that when node transitions from within the camera's view to out of the camera's view, it triggers or calls a function?
This looks a bit complicated....
You can add PhysicsBodies to your nodes, and "special boundary" nodes around your camera, then you can use your physics delegate to get notified if your nodes intersect the boundary nodes.

Turn off touch for whole screen, SpriteKit, how?

I'm trying to temporarily disable touch on the entire screen, despite their being many sprites with touchesBegun onscreen.
I thought, obviously wrongly, turning off touch for the scene would do it:
scene?.isUserInteractionEnabled = false
But that didn't work, so I tried this, which also didn't work:
view?.scene?.isUserInteractionEnabled = false
That also didn't work, so I tried this, also from inside the scene:
self.isUserInteractionEnabled = false
There is no global method to turn off the touch, whatever is at the top of the drawing queue is the first responder.
You need to iterate through all of your nodes from your scene and turn them off:
enumerateChildNodesWithName("//*", usingBlock:
{ (node, stop) -> Void in
node.isUserInteractionEnabled = false
})
Now the problem is turning them back on, if you use this method, you will turn it on for everything, so you may want to adopt a naming convention for all your touchable sprites
enumerateChildNodesWithName("//touchable", usingBlock:
{ (node, stop) -> Void in
node.isUserInteractionEnabled = true
})
This will look for any node that has a name that begins with touchable.
This method involves recursion, so if you have a ton of nodes, it can be slow. Instead you should use an alternative method:
let disableTouchNode = SKSpriteNode(color:SKColor(red:0.0,green:0.0,blue:0.0,alpha:0.1),size:self.size)
disableTouchNode.isUserinteractionEnabled = true
disableTouchNode.zPosition = 99999
self.addChild(disableTouchNode)
What this does is slap on an almost transparent node on top of all elements the size of the scene. This way when a user touches the screen, this node will absorb it instead of anything else.
The following will disable all touches
self.view?.isUserInteractionEnabled = false

Detect other Spritenode within range of Spritenode?

I have a (moving) sprite node.
I'd like to detect other (moving) sprite nodes within a certain range of this node. Once one is detected, it should execute an action.
The playing an action part is no problem for me but I can't seem to figure out the within-range detection. Does have any ideas how to go about this?
A simple, but effective way to do this is comparing the position's in your scene's didEvaluateActions method. didEvaluateActions gets called once a frame (after actions have been evaluated but before physics simulation calculations are run). Any new actions you trigger will start evaluating on the next frame.
Since calculating the true distance requires a square root operation (this can be costly), we can write our own squaredDistance and skip that step. As long as our range/radius of detect is also squared, our comparisons will work out as expected. This example shows detect with a "true range" of 25.
// calculated the squared distance to avoid costly sqrt operation
func squaredDistance(p1: CGPoint, p2: CGPoint) -> CGFloat {
return pow(p2.x - p1.x, 2) + pow(p2.x - p1.x, 2)
}
// override the didEvaluateActions function of your scene
public override func didEvaluateActions() {
// assumes main node is called nodeToTest and
// all the nodes to check are in the array nodesToDetect
let squaredRadius: CGFloat = 25 * 25
for node in nodesToDetect {
if squareDistance(nodeToTest.position, p2: node.position) < squaredRadius {
// trigger action
}
}
}
If the action should only trigger once, you'll need to break out of the loop after the first detection and add some sort of check so it does not get triggered again on the next update without the proper cool down period. You may also need to convert the positions to the correct coordinate system.
Also, take a look at the documentation for SKScene. Depending on your setup, didEvaluateActions might not be the best choice for you. For example, if your game also relies on physics to move your nodes, it might be best to move this logic to didFinishUpdate (final callback before scene is rendered, called after all actions, physics simulations and constraints are applied for the frame).
Easiest way I can think of without killing performance is to add a child SKNode with an SKPhysicsBody for the range you want to hit, and use this new nodes contactBitMask to determine if they are in the range.
Something like this (pseudo code):
//Somewhere inside of setup of node
let node = SKNode()
node.physicsBody = SKPhysicsBody(circleOfRadius: 100)
node.categoryBitMask = DistanceCategory
node.contactBitMask = EnemyCategory
sprite.addNode(node)
//GameScene
func didBeginContact(...)
{
if body1 contactacted body2
{
do something with body1.node.parent
//we want parent because the contact is going to test the bigger node
}
}

Respawning a node after it's been deleted? Swift Spritekit

Once a node is removed, I'm needing to respawn it back to it's original position.
I've tried a few things:
1) In the didBeginContact where it gets deleted, I've just added an addChild() after the delete, but that just added it back where it was, making it look like it just paused in place
2) When it dies, I'm setting a variable to 0, and I created a function that adds the node back where it starts out, and then I set up an if statement that looks like :
if playerLife == 0 {
spawnPlayer()
}
and the player life gets set to 0 down in my didBeginContact. Don't know if that even makes sense
3) I tried slapping all of this down in the didBeginContact
The ball is either freezing when I try to addchild directly back right after I delete it in the DidBegin, otherwise it gets deleted and never is respawned.
Try moving your code inside of the update function. also in your spawnPlayer function make sure to add your node if node.parent == nil
override func update(currentTime: NSTimeInterval) {
if playerLife == 0 {
spawnPlayer()
}
}