SKAction Sequencing and Grouping Animations - swift

I'm doing some death animations for a game, and wanted to ask for some help. I want my monster to disappear in a puff of smoke, but not before it animates a slash effect going across his body.
I have 3 animations that I want to use:
weaponSlash - a line that draws across the monster. Looks like you slashed him with a sword.
smoke - a puff of smoke that slowly expands out
monsterFalling - the monster falls back, startled
What I want to do is play it in this order:
Simultaneously, the slash appears & the monster starts to fall back
About 0.25s into the above animation, I want the cloud to start to appear
When the cloud is about to end (so maybe after 1s) I want the monster to disappear
Remove the smoke, the monster, the sword, etc, and drop some coins on the ground
I started like this, as a test that works somewhat: (ignore the above times for now)
//Cancel any current actions, like a monster attacking
monster.removeAllActions()
//since you can't play 3 animations on one node at the same time, you have to create 3 separate nodes for everything
let slash = SKSpriteNode()
let cloud = SKSpriteNode()
cloud.size = monster.size
slash.size = monster.size
monster.addChild(cloud)
monster.addChild(slash)
//Start the slash animation
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
//Start the cloud animation (how I get it is elsewhere and not relevant)
cloud.run(cloudAnimation)
//Run the monster death animation, followed by the cleanup/coin dropping
monster.run(SKAction.sequence([monster.deathAnimation(), SKAction.wait(forDuration: 1), postDeathActions]))
The variable PostDeathActions above simply removes the monster node and animates some coins falling.
WHERE I NEED SOME HELP
So the above code doesn't work so great in that the animations all run independently of each other. Based on this, you can see how regardless of whether the slash/cloud finish, the monster will run two actions: him falling back, followed by cleanup, which just removes the monster and spawns the coins. As you can see I tried to delay this by adding a 1s delay but this is all somewhat of a hack since I may have different monsters or attacks, etc, that are faster/slower. I'd rather guarantee that everything finishes before I despawn the monster.
I tried to group this into an SKAction.Run like so:
let preDeath = SKAction.run {
[unowned self] in
monster.run(monster.deathAnimation()
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
cloud.run(cloudAnimation)
}
but this runs everything at the same time again.
What I want to do is sequence it like this (pseudo code):
let preDeathAnimations = SKAction.Group([slash, cloud, monsterDeathAnimation])
])
SKAction.sequence([preDeathAnimations, postDeathActions])
So this way it'll run all 3 before running cleanup.
Is there a way to do something like this? I know Sequnce/Group need to be run against an SKNode, but I don't have 3 separate ones.
Thanks for your time reading this and any advice you can offer!

This is one idea that I had, but you could use threading + state + onCompletion blocks to take the math out of it. I didn't test it out fully but this general concept should work:
let slash = SKAction.fadeIn(withDuration: 0.5)
let fall = SKAction.fadeOut(withDuration: 0.25)
let puff = SKAction.fadeIn(withDuration: 0.1)
// Put in ALL of the actions from ALL parties that you want to happen prior to puff:
func findLongestTime(from actions: [SKAction]) -> TimeInterval {
var longestTime = TimeInterval(0)
for action in actions {
if action.duration > longestTime { longestTime = action.duration }
}
// Note, if you put a sequence into this function I don't know if it will work right..
// Might need another func like `findDurationOfSequence(_ sequence: SKAction) -> TimeInterval
return longestTime
}
// Note, if you have the monster doing more than falling prior to puff, then you will
// need to subtract those as well:
let monsterActionsPriorToPuff = [fall]
// Add the duration of all monster animations prior to puff:
var MAPTP_duration = TimeInterval(0)
for action in monsterActionsPriorToPuff {
MAPTP_duration += action.duration
}
// Calculate our final wait time, with no negative numbers:
var waitTime = findLongestTime(from: [slash, fall]) - MAPTP_duration
if waitTime < 0 { waitTime = 0 }
let wait = SKAction.wait(forDuration: waitTime)
// Our monster sequence (I forgot to add the disappear, just add after puff)
let monsterSequence = SKAction.sequence([fall, wait, puff])
// Player slashes:
SKSpriteNode().run(slash)
// Monster will wait 0.25 seconds after falling,
// for slash to finish before puffing:
SKSpriteNode().run(monsterSequence)
et me know if this idea isn't working I can try updating it.

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.

Swift: SKConstraint for scale? (or equivalent) Stuttering

At the minute I'm using the SKConstraint.positionX(rangex, y: rangey) to confine my SKCameraNode within the game board I've created. This is nice because when you hit the boundary there's no stuttering. But my current way to cap the scale of the camera creates a stutter as it hits the bound as it goes past and pings back.
#objc func zoomedView(_ sender:UIPinchGestureRecognizer) {
if newCamera.xScale > 0.148{
let pinch = SKAction.scale(by: 1/sender.scale, duration: 0.0)
newCamera.run(pinch)
sender.scale = 1.0
} else {newCamera.setScale(0.148)}
}
Is there an SKConstraint for scale (or equivalent) which is a better way to stop this stutter? Thanks :)
There is no direct SKConstraint equivalent for scale, however the reason you're experiencing the stuttering is as you go over the bound it snaps back the next time the function is called, rather before a frame is rendered, so theoretically you could zoom in massively instantaneously, and stay there until you activate the zoom function again.
A way to create an equivalent is to put the code checking whether the scale is greater than x in the rendering loop as outlined here.
So if you were to check at the last possible moment:
override func didFinishUpdate() {
if newCamera.xScale < 0.148{
newCamera.setScale(0.148)
} else if newCamera.xScale > 10{
newCamera.setScale(10)
}
}

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

SpriteKit: Cloning Nodes

So I would like to make a block of code that will run inside the sprite node every frame. The result would be me tapping and creating a bubble there that would then float up using sine. Is that possible to code into the bubble? Sorry if this seems vague, I'm just not sure what to call it. The closest thing I can think of it cloning and each clone runs the same script every frame. Anyways, Here's what I have now.
let bubble = SKSpriteNode(imageNamed:"bubble")
bubble.position.x = CGFloat(rand())*self.frame.width
bubble.position.y = -20
//Code for bubble to run each frame
self.addChild(bubble)
I think that what you are looking for is enumerateChildNodesWithName. When it is called, it will run the code it contains on every node with the name you put in it.
bubble.name = "bubble"
If you name bubble "bubble", all of its child nodes will also be named "bubble".
enumerateChildNodesWithName("bubble"){node, stop in
let bubbleChild:SKSpriteNode = node as! SKSpriteNode
//Run the code telling them what to do here. You should make it so that
//it can figure out how to move the node a slight amount based on its position
}
You could call this by putting it in a function that is called by a timer, and the timer should be called frequently, like every 0.017 seconds (Approximately 60 times a second) so that the motion is smooth. For example:
var timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(0.017), target: self, selector: Selector("moveBubbles:"), userInfo: nil, repeats: true)
Then, to make a function that the timer calls:
func moveBubbles(timer: NSTimer!){
//enumerateChildNodesWithName here
}
There is more information on enumerateChildNodesWithName here, and more information on NSTimer here.

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.