Accessing properties of multiple SKShapeNodes - swift

In my program I have a method called addObstacle, which creates a rectangular SKShapeNode with an SKPhysicsBody, and a leftward velocity.
func addObstacle(bottom: CGFloat, top: CGFloat, width: CGFloat){
let obstacleRect = CGRectMake(self.size.width + 100, bottom, width, (top - bottom))
let obstacle = SKShapeNode(rect: obstacleRect)
obstacle.name = "obstacleNode"
obstacle.fillColor = UIColor.grayColor()
obstacle.physicsBody = SKPhysicsBody(edgeLoopFromPath: obstacle.path!)
obstacle.physicsBody?.dynamic = false
obstacle.physicsBody?.affectedByGravity = false
obstacle.physicsBody?.contactTestBitMask = PhysicsCatagory.Ball
obstacle.physicsBody?.categoryBitMask = PhysicsCatagory.Obstacle
obstacle.physicsBody?.usesPreciseCollisionDetection = true
self.addChild(obstacle)
obstacle.runAction(SKAction.moveBy(obstacleVector, duration: obstacleSpeed))
}
In a separate method, called endGame, I want to fade out all the obstacles currently in existence on the screen. All the obstacle objects are private, which makes accessing their properties difficult. If there is only one on the screen, I can usually access it by its name. However, when I say childNodeWithName("obstacleNode")?.runAction(SKAction.fadeAlphaBy(-1.0, duration: 1.0)), only one of the "obstacles" fades away; the rest remain completely opaque. Is there a good way of doing this? Thanks in advance (:

You could probably go with:
self.enumerateChildNodesWithName("obstacleNode", usingBlock: {
node, stop in
//do your stuff
})
More about this method can be found here.
In this example I assumed that you've added obstacles to the scene. If not, then instead of scene, run this method on obstacle's parent node.
And one side note...SKShapeNode is not performant solution in many cases because it requires at least one draw pass to be rendered by the scene (it can't be drawn in batches like SKSpriteNode). If using a SKShapeNode is not "a must" in your app, and you can switch them with SKSpriteNode, I would warmly suggest you to do that because of performance.
SpriteKit can render hundreds of nodes in a single draw pass if you are using same atlas and same blending mode for all sprites. This is not the case with SKShapeNodes. More about this here. Search SO about this topic, there are some useful posts about all this.

Related

How to execute multiple custom animation functions synchronously in Swift 4

I am working on an IOS App that is supposed to help people in a tourist region to make sense of means of transport. To make it crystal clear to them how to get from A to B, routes are animated with Annotation objects. For example once the user chooses to see the route from A to D, a cable car object slides from A to B. Once it finishes, a Shuttle Bus object moves along a road from B to C, followed by a boat sliding from C to D.
So I wrote the following functions.
This one lets a transportMode object (small image of a boat, cable car, etc.) slide in a straight line from A to B or B to A.
func animateLinearRoute(transportMode: TransportAnnot, startcoor:
CLLocationCoordinate2D, destcoor: CLLocationCoordinate2D){
UIView.animate(withDuration: 3, animations:
{
if (transportMode.coordinate.latitude == startcoor.latitude && transportMode.coordinate.longitude == startcoor.longitude) {
transportMode.coordinate = destcoor
} else {
transportMode.coordinate = startcoor
}
})
}
For moving an object along a nonlinear route (usually a road) drawn on a map I use the following function:
// pass mode of transport and coordinates along the travel route
func animateNonLinearRoute(transportMode: TransportAnnot, animroute: [CLLocationCoordinate2D]){
let path = UIBezierPath()
// get start point of route from coordinates and start drawing route
let point = self.mapView.convert(animroute[0], toPointTo: self.mapView)
path.move(to: point)
// translate each coordinate along the route into a point in the view for drawing
for coor in animroute {
let point = self.mapView.convert(coor, toPointTo: self.mapView)
path.addLine(to: point)
}
// create keyframe animation to move annotation along the previously drawn path
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path.cgPath
animation.duration = 5.0
animation.isRemovedOnCompletion = false
let transportview = self.mapView.view(for: transportMode)
transportview?.layer.add(animation, forKey: "animate position along path")
transportMode.coordinate = animroute[animroute.count - 1]
CATransaction.commit()
}
Now the full route can consist of an arbitrary chain of these methods. For example the user may chose to get to a point that requires a linear route -> nonlinear route -> linear route -> nonlinear -> nonlinear.
Ultimately the animations need to be executed in a strictly consecutive manner so the user won't be confused (the second animation should not start unless the first one has finished, etc.).
One consideration would be a keyframe animation like this:
UIView.animateKeyframes(withDuration: 8, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 5/8, animations: {
self.animateNonLinearRoute(transportMode: self.bus, animroute: self.br.coordinates)
})
UIView.addKeyframe(withRelativeStartTime: 5/8, relativeDuration: 3/8, animations: {
self.animateLinearRoute(transportMode: self.cableCar, startcoor: self.lowerstation, destcoor: self.midstation)
})
// dynamically fill up as needed using appropriate relative start times and durations
}, completion: nil)
That doesn't execute the code synchronously though. I guess it conflicts with the timings and keyframes defined within the functions.
I've been messing around with custom completion closures and then put each method in the completion closure of the previous one as well as with dispatch queues. But I don't really seem to understand them because I wasn't able to achieve the desired effects. And as routes get longer nested completion closures don't seem to be an ideal option as they make the program unnecessarily complex. Any suggestions are highly appreciated.
I guess I traced down the problem.
Since animateNonLinearRoute triggers an animation on the CALayer, the function returns after triggering the animation without waiting for its completion. Therefore all functions in the methods completion handler will be executed, after the CALayer Animation HAS BEEN TRIGGERED, NOT AFTER IT HAS FINISHED.
A simple hacky solution would be to wrap the functions that should be executed after the CALayer Animations have finished, into a CATransaction block:
CATransaction.begin()
CATransaction.setCompletionBlock({
self.animateLinearRoute(transportMode: self.cableCar, startcoor: self.lowerstation, destcoor: self.midstation)
})
self.animateNonLinearRoute(transportMode: self.bus, animroute: self.br.coordinates)
CATransaction.commit()
I would love to hear a better explanation from someone who understands multithreading and concurrency in Swift and a suggestion for a clean way to chain several of these function calls.

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
}
}

How do run an SKAction on a group of SKSpriteNodes with the same name?

I'm making a game in Xcode 6 using spriteKit and swift. I have a plane on scene, and to make it look like its moving, I'm create clouds off the scene to the left of the screen. I then us an SKAction to move the cloud to the right side of the screen. This works great. You then click to jump off the plane, and the plane moves up off the scene. I then have it start making the clouds on the bottom of the scene, then they move up off the top of the scene, but the problem is, the already existing clouds still have to move to the right side of the screen. My question is, how do I make it so all of the existing clouds stop their action that moves them to the right, then begin to move up exactly where they are? How do I access the group of existing clouds all at the same time once they have been created? I also want the clouds to slow down after you have jumped when you tap the screen to open your parachute, but this should be able to be done by ending the SKAction that moves the clouds, then using another SKAction on them that moves them up slower, but I don't know how to access a group of SKSpriteNodes.
Here is the code that I have to make the clouds:
//This is how the cloud is first declared at the top of the .swift file
var cloud = SKSpriteNode()
//This is the function that runs every certain interval through an NSTimer
func createCloud()
{
cloud = SKSpriteNode(imageNamed: "cloud")
cloud.xScale = 1.25
cloud.yScale = cloud.xScale/2
cloud.zPosition = -1
self.addChild(cloud)
if personDidJump == false
{
let moveRight = SKAction.moveToX(CGRectGetWidth(self.frame) + CGRectGetWidth(cloud.frame), duration: cloudSpeed)
var apple = CGRectGetWidth(self.frame)
var randomNumber:CGFloat = CGFloat(arc4random_uniform(UInt32(apple)))
cloud.position = CGPointMake(-CGRectGetWidth(cloud.frame), randomNumber)
cloud.runAction(moveRight)
}
if personDidJump == true
{
let moveUp = SKAction.moveToY(CGRectGetHeight(self.frame) + CGRectGetWidth(cloud.frame), duration: cloudSpeed)
var apple = CGRectGetWidth(self.frame)
var randomNumber:CGFloat = CGFloat(arc4random_uniform(UInt32(apple)))
cloud.position = CGPointMake(randomNumber, -CGRectGetHeight(cloud.frame))
cloud.runAction(moveUp)
}
}
Also, should I be worried about deleting the clouds when they move off the scene? Or can I just leave them there because you can't see them, and from what I've seen, they don't lag you.
Any help would be greatly appreciated. Thank you.
-Callum-
Put your clouds in an array. When your player jumps (or whenever you need to move them) run through the array and run your action on each cloud.
stackoverflow.com/a/24213529/3402095
^ That is where I found my answere ^
When you make a node give it a name:
myNode.name = "A Node Name"
self.addChild(myNode)
If there are a lot of these nodes, later you can change properties or preform SKAcions on these nodes or do whatever you want to do by using enumerateChildNodesWithName:
self.enumerateChildNodesWithName("A Node Name")
{
myNode, stop in
myNode.runAction(SKAction.moveToY(CGRectGetHeight(self.Frame), duration: 1)
}
I hope this is useful to whoever may need it.

How do I remove a group of Nodes in Swift?

I'm new to objective c and swift and I created a small app where small circles are rendered and once the player collides with a circle, the game ends. I managed to get everything to work, but how do I remove the nodes after they collide. I tried removeAllChildren(), but none of them disappear. When I use removeFromParent(), only 1 disappears. I want a way to remove all 3 nodes that will be rendered in the code below
//addEvilGuys() is called first
func addEvilGuys()
{
addEvilGuy(named: "paul", speed: 1.3, xPos: CGFloat(self.size.height/3))
addEvilGuy(named: "boris", speed: 1.7, xPos: frame.size.width/4 + 50)
addEvilGuy(named: "natasha", speed: 1.5, xPos: frame.size.width/4 + 150)
}
func addEvilGuy(#named:String, speed:Float, xPos: CGFloat)
{
evilGuyNode = SKSpriteNode(imageNamed: named)
evilGuyNode.zPosition = 10
evilGuyNode.physicsBody = SKPhysicsBody(circleOfRadius: 16)
evilGuyNode.physicsBody!.affectedByGravity = false
evilGuyNode.physicsBody!.categoryBitMask = ColliderType.BadGuy.rawValue
evilGuyNode.physicsBody!.contactTestBitMask = ColliderType.Hero.rawValue
evilGuyNode.physicsBody!.collisionBitMask = ColliderType.Hero.rawValue
evilGuyNodeCount++
var evilGuy = EvilGuy(speed: speed, eGuy: evilGuyNode)
evilGuys.append(evilGuy)
resetEvilGuy(evilGuyNode, xPos: xPos)
evilGuy.xPos = evilGuyNode.position.x
addChild(evilGuyNode)
}
func resetEvilGuy(evilGuyNode:SKSpriteNode, xPos:CGFloat)
{
evilGuyNode.position.y = endOfScreenBottom
evilGuyNode.position.x = xPos
}
It looks like in addEvilGuy you are recreating a stored property (i.e. that is visible for the entire class + whatever the access level allows) to create the SKSpriteNode that you're adding. This means that you are orphaning the previously created EvilGuy.
In addEvilGuy, replace
evilGuyNode = SKSpriteNode(imageNamed: named)
with
let evilGuyNode = SKSpriteNode(imageNamed: named)
and remove the property from your class (it doesn't seem like you have a need for in in a larger scope).
It also looks like you're creating EvilGuys and storing them in an array, which is good. So when you can remove all of them from the screen with a function like:
func removeAllEvilGuys(evilGuys: [EvilGuy]) {
for evilGuy in evilGuys {
evilGuy.eGuy.removeFromParent()
}
}
As a best practice advice, since you mentioned you're a beginner:
I'd recommend defining the characteristics of the evil guys in a .plist and then use the file to create an array of evil guys. This way you can easily make changes to the evil guys in that file without having to change anything in your code.
The code that creates an EvilGuy object should be separated from the one that adds the evil guy to the screen. As long as you are storing the SKNode of each one, you'll be able to add/remove without unnecessarily recreating the entire object.