Swift - Adding animation and sound: Using animateWithTextures: - ios

I am trying to create a specific type of animation that requires a sound and animation to occur for exactly 1 second per texture cycle.
Each time someone clicks a button, it appends a new texture to an array of SKTextures[], then I have an SKSpriteNode animate through each of the textures once. Only problem is, I can't seem to figure out a way to play a sound file for each texture as it animates. I would also like to create an animation for each texture the SKSpriteNode changes to as it goes through the textures.
Here is an example of the flow:
Button Clicked
Append SKTexture array
SKSpriteNode.animateUsingTextures(SKTextureArray, timePerFrame: 1)
While each individual texture displays, animate it, play sound.
Here is what I have that is not working.
(Code is just an abstraction of what I have)
var textures: [SKTexture] = []
let sprite: SKSpriteNode()
let scaleUp = SKAction.scaleTo(300, duration: 0.2)
let scaleDown = SKAction.scaleTo(200, duration: 0.2)
let popAnimation = SKAction.sequence([scaleUp, scaleDown])
func buttonClicked() {
textures.append("Specific Texture")
sprite.animation() // Calls Animation Function
}
func animation() {
let sound = SKAction.playSoundFileNamed("DisplayGesture.mp3", waitForCompletion: true)
let animation = SKAction.animateWithTextures(textures, timePerFrame: 1)
let completeAnimation = SKAction.group([sound, animation, popAnimation])
sprite.runAction(completeAnimation)
}

I think what is happening is that you play the sound once, then animate all of the textures. What you could try doing:
fund animation() {
for myTexture in textures {
let sound = SKAction.playSoundFileNamed("DisplayGesture.mp3", waitForCompletion: true)
sprite.texture = myTexture
}
}

Related

How to darken texture of sprite node

I have the following code where I create a sprite node that displays an animated GIF. I want to create another function that darkens the GIF when called upon. I could still be able to watch the animation, but the content would be visibly darker. I'm not sure how to approach this. Should I individually darken every texture or frame used to create the animation? If so, how do I darken a texture or frame in the first place?
// Extract frames and duration
guard let imageData = try? Data(contentsOf: url as URL) else {
return
}
let source = CGImageSourceCreateWithData(imageData as CFData, nil)
var images = [CGImage]()
let count = CGImageSourceGetCount(source!)
var delays = [Int]()
// Fill arrays
for i in 0..<count {
// Add image
if let image = CGImageSourceCreateImageAtIndex(source!, i, nil) {
images.append(image)
}
// At it's delay in cs
let delaySeconds = UIImage.delayForImageAtIndex(Int(i),
source: source)
delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
}
// Calculate full duration
let duration: Int = {
var sum = 0
for val: Int in delays {
sum += val
}
return sum
}()
// Get frames
let gcd = SKScene.gcdForArray(delays)
var frames = [SKTexture]()
var frame: SKTexture
var frameCount: Int
for i in 0..<count {
frame = SKTexture(cgImage: images[Int(i)])
frameCount = Int(delays[Int(i)] / gcd)
for _ in 0..<frameCount {
frames.append(frame)
}
}
let gifNode = SKSpriteNode.init(texture: frames[0])
gifNode.position = CGPoint(x: skScene.size.width / 2.0, y: skScene.size.height / 2.0)
gifNode.name = "content"
// Add animation
let gifAnimation = SKAction.animate(with: frames, timePerFrame: ((Double(duration) / 1000.0)) / Double(frames.count))
gifNode.run(SKAction.repeatForever(gifAnimation))
skScene.addChild(gifNode)
I would recommend to use the colorize(with:colorBlendFactor:duration:) method. It is an SKAction that animates changing the color of a whole node. That way you don't have to get into darkening the individual textures or frames, and it also adds a nice transition from a non-dark to a darkened color. Once the action ends, the node will stay darkened until you undarken it, so any changes to the node's texture will also be visible as darkend to the user.
Choose whatever color and colorBlendFactor will work best for you to have the darkened effect you need, e.g. you could set the color to .black and colorBlendFactor to 0.3. To undarken, just set the color to .clear and colorBlendFactor to 0.
Documentation here.
Hope this helps!

Finish action running on a SpriteNode without disabling touches

i have a player who's physicsBody is divided to parts (torso, legs, head, ect), now i have a button on screen that control the movement of the player, if the button is pressed to the right the player moves to the right, and that part works fine. the problem is every time the movement changes the walk() method is called and what it does is animate the player legs to look like its walking, but if the movement doesn't stop then is keeps calling the walk() without it finishing the animation, so it looks like its stuck back and forth. what i am trying to achieve is for the player to be able to walk constantly but for the walk() (animation method) to be called once, finish, then called again(as long as the button to walk is still pressed). here what i have so far:
func walk(){
let leftFootWalk = SKAction.run {
let action = SKAction.moveBy(x: 1, y: 0.1, duration: 0.1)
let actionReversed = action.reversed()
let seq = SKAction.sequence([action, actionReversed])
self.leftUpperLeg.run(seq)
self.leftLowerLeg.run(seq)
}
let rightFootWalk = SKAction.run {
let action = SKAction.moveBy(x: 0.4, y: 0.1, duration: 0.1)
let actionReversed = action.reversed()
let seq = SKAction.sequence([action, actionReversed])
self.rightUpperLeg.run(seq)
self.rightLowerLeg.run(seq)
}
let group = SKAction.sequence([leftFootWalk, SKAction.wait(forDuration: 0.2), rightFootWalk])
run(group)
}
extension GameScene: InputControlProtocol {
func directionChangedWithMagnitude(position: CGPoint) {
if isPaused {
return
}
if let fighter = self.childNode(withName: "fighter") as? SKSpriteNode, let fighterPhysicsBody = fighter.physicsBody {
fighterPhysicsBody.velocity.dx = position.x * CGFloat(300)
walk()
if position.y > 0{
fighterPhysicsBody.applyImpulse(CGVector(dx: position.x * CGFloat(1200),dy: position.y * CGFloat(1200)))
}else{
print("DUCK") //TODO: duck animation
}
}
}
First of all you can use SKAction.runBlock({ self.doSomething() })
as the last action in your actions sequence instead of using completion.
About your question, you can use hasAction to determine if your SKNode is currently running any action, and you can use the key mechanism for better actions management.
See the next Q&A
checking if an SKNode is running a SKAction
I was able to solve this, however i'm still sure there is a better approach out there so if you know it be sure to tell. here is what i did:
first i added a Boolean called isWalking to know if the walking animation is on(true) or off(false) and i set it to false. then i added the if statement to call walk() only if the animation is off(false) and it sets the boolean to on(true).
func directionChangedWithMagnitude(position: CGPoint) {
if isPaused {
return
}
if let fighter = self.childNode(withName: "fighter") as? SKSpriteNode, let fighterPhysicsBody = fighter.physicsBody {
fighterPhysicsBody.velocity.dx = position.x * CGFloat(300)
print(fighterPhysicsBody.velocity.dx)
if !isWalking{
isWalking = true
walk()
}
}
}
and i changed walk() to use completions blocks and set the Boolean to off(false) again
func walk(){
let walkAction = SKAction.moveBy(x: 5, y: 5, duration: 0.05)
let walk = SKAction.sequence([walkAction, walkAction.reversed()])
leftUpperLeg.run(walk) {
self.rightUpperLeg.run(walk)
}
leftLowerLeg.run(walk) {
self.rightLowerLeg.run(walk, completion: {
self.isWalking = false
})
}
}

Sprite moves two places after being paused and then unpaused

I am having trouble figuring out the solution to this and am starting to get very frustrated with it.
I have a pause button and an unpause button in my game scene to allow the player to pause the game, which is the following code
else if (node == pauseButton) {
pauseButton.removeFromParent()
addChild(unpauseButton)
addChild(restartButton)
self.runAction (SKAction.runBlock(self.pauseGame))
}
func pauseGame(){
pauseButton.hidden = true
unpauseButton.hidden = false
scene!.view!.paused = true // to pause the game
}
Problem
The problem is that when I pause the game then unpause the game my player sprite seems to move two spaces forward automatically
I also have a tap and swipe gesture that allows me to move the player up left right and down when I tap anywhere on the screen.
func tapUp(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(0, y: amountToMove, duration: 0.1)
menubutton.hidden = true
settingsButton.hidden = true
highscoreLabel.hidden = true
pauseButton.hidden = false
thePlayer.runAction(move)
}
func swipedRight(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(amountToMove, y: 0, duration: 0.1)
thePlayer.runAction(move) // links the action with the players
}
As the member above me said the player is not really moving 2 spaces.
Also you should maybe change your strategy when pausing your game, because pausing the scene.view makes it very hard to add SpriteKit elements afterwards.
I think a better way is to create a worldNode in your GameScene and add all the sprites that need to be paused to that worldNode. It basically gives you more flexibility pausing a node rather than the whole scene.
First create a world node property
let worldNode = SKNode()
and add it to the scene in ViewDidLoad
addChild(worldNode)
Than add all the sprites you need paused to the worldNode
worldNode.addChild(sprite1)
worldNode.addChild(sprite2)
...
Create a global enum for your game states
enum GameState {
case Playing
case Paused
case GameOver
static var current = GameState.Playing
}
Than make a pause and resume func in your game scene
func pause() {
GameState.current = .Paused
// show pause menu etc
}
func resume() {
GameState.current = .Playing
self.physicsWorld.speed = 1
worldNode.paused = false
}
And finally add the actual pause code to your updateMethod. This way it will not resume the game even if spriteKit itself tries to resume (e.g. reopened app, dismissed alert etc)
override func update(currentTime: CFTimeInterval) {
if GameState.current == .Paused {
self.physicsWorld.speed = 0
worldNode.paused = true
}
}
In regards to your tapGesture recogniser, in the method that gets called after a tap you can add this before the rest of the code
guard GameState.current != .Paused else { return }
....
Hope this helps

Simple Gif like animation with Spritekit

I can't really find a simple solution for this, every example I see only shows very complex solutions, but all I want is 2-3 images that cycle so it appears as if it is animated. Same effect as an animated Gif. For now I have this to create an image
MonsterNode = SKSpriteNode(imageNamed: "MonsterNode_GameScene")
but how would I set MonsterNode variable to an animation of this sort? I am really looking for the very least amount of code needed to achieve this.
The main idea is to use animateWithTextures for this task. You need to set all the frames that the sprite needs to animated and the displayed time of each frame. Then use repeatActionForever to run the animation loop.
// Add 3 frames
let f0 = SKTexture.init(imageNamed: "MonsterNode_GameScene_0")
let f1 = SKTexture.init(imageNamed: "MonsterNode_GameScene_1")
let f2 = SKTexture.init(imageNamed: "MonsterNode_GameScene_2")
let frames: [SKTexture] = [f0, f1, f2]
// Load the first frame as initialization
monsterNode = SKSpriteNode(imageNamed: "MonsterNode_GameScene_0")
monsterNode.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
// Change the frame per 0.2 sec
let animation = SKAction.animateWithTextures(frames, timePerFrame: 0.2)
monsterNode.runAction(SKAction.repeatActionForever(animation))
self.addChild(monsterNode)
I was getting the same issue of big red X.
So instead of defining the monsterNode in code. I created the monstor on sks screen by dragging first image of animation from atlas folder. Then assign it a name: monsterNode from properties section. Here's the code
var runAnimation = [SKTexture]()
override func didMove(to view: SKView) {
let runAtlas = SKTextureAtlas(named: "run")
for index in 1...runAtlas.textureNames.count{
let textureName = String.init(format: "run%1d", index)
runAnimation.append(SKTexture(imageNamed: textureName))
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let monsterNode = self.childNode(withName: "monsterNode")
if(monsterNode != nil){
let animation = SKAction.animate(with: runAnimation, timePerFrame: 0.1)
monsterNode?.run(SKAction.repeatForever(animation))
}
}

How do I make confetti?

I essentially want to emit confetti particles. Each particle is the same shape, however, I want each particle to be a random colour from a selection of colors I specify.
Is there a way for each emitted particle to have a random color or do I need a separate emitter for each particle color?
You can use single emitter to achieve what you want:
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
let emitter = SKEmitterNode(fileNamed: "particle")
let colors = [SKColor.whiteColor(),SKColor.grayColor(),SKColor.greenColor(),SKColor.redColor(),SKColor.blackColor()]
override func didMoveToView(view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
emitter.position = CGPoint(x: 200, y:300)
emitter.particleColorSequence = nil
emitter.particleColorBlendFactor = 1.0
self.addChild(emitter)
let action = SKAction.runBlock({
[unowned self] in
let random = Int(arc4random_uniform(UInt32(self.colors.count)))
self.emitter.particleColor = self.colors[random];
println(random)
})
let wait = SKAction.waitForDuration(0.1)
self.runAction(SKAction.repeatActionForever( SKAction.sequence([action,wait])))
}
}
EDIT:
Try changing duration of wait action to get different results.
You can play with color ramp too (in particle editor) to achieve the same effect:
Or you can use particleColorSequence and SKKeyframeSequence in order to change particle color over its lifetime. Hope this helps.
Just for anyone else who might want an answer to this. There is a framework called SAConfettiView https://github.com/sudeepag/SAConfettiView. Definitely check it out! It worked for me.

Resources