Tuesday, July 28, 2015

Combining Balls Animation -- Epic Color Crash Step 4

OK it has been a couple of days since the last step was posted. I am splitting my time between a couple of web service applications, a new game, this tutorial, selling stuff on eBay, and sometime I should work on getting a job. Anyway it is time for the next step.

Preview: Here is what the game will be like at the end of this step:


Now that we have the images for the animations created we can start the game logic. The first thing I am going to add is the ability to swipe between two primary colored balls to produce a secondary colored ball.

I don't really need to do a swipe detection, all I really need to do is on a touchesBegan check and see what ball, if any, the user touched. Then on the touchesMoved check and see if the touch has moved to a different ball. Then if the balls are allowed to combine we can combine the balls.

The touchesBegan function in GameScene.swift currently looks like this:
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        /* Called when a touch begins */
        
        for touch in (touches as! Set<UITouch>) {
        }

    }

First we want to get the location of the touch in the local coordinates. Inside the for loop add:
let location = touch.locationInNode(self)

Now we want to see if there are any balls at this location. self.nodesAtPoint(location) will give us a list of all of the scenes child nodes that are at the location. These nodes may or may not be a colored ball. So I will create an optional called startBall. Add startBall right after the declaration for newBallDelay in the Game scene class.
class GameScene: SKScene {
    var lastBallDropped: CFTimeInterval = 0
    var newBallDelay: CFTimeInterval = 1.5
    var startBall: ColoredBall?

We will then get the list of nodes at the touch location, assuming there is at least one node we will try and set the startBall to the touched node as a ColoredBall. If the node is not an object with the class of ColoredBall then startBall will be set to no value. However if it is a ColoredBall object then the optional will be set. When this happens we don't need to look at anymore nodes or anymore touches and we can just return. So after let location = touch.locationInNode(selfadd:

            let balls = self.nodesAtPoint(location)
            if balls.count >= 1 {
                for aBall in balls {
                    startBall = aBall as? ColoredBall;
                    if startBall != nil {
                        return
                    }
                }
            }

Now we want to get touchesMoved events and see if the user swipes into a different ball. We add the touchesMoved function and like the touchesBegan convert the touch location to our local coordinates, and check to see if there is a ColoredBall at that location. If there is a ColoredBall at that location than we make sure we have a starting ball, and the starting ball is not the same as the ball being touched. If that is the case than we can try and combine the balls.
 
    override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
        for touch in (touches as! Set<UITouch>) {
            let location = touch.locationInNode(self)
            let aBall:ColoredBall? = self.nodeAtPoint(location) as? ColoredBall
            if startBall != nil && aBall != nil && aBall != startBall {
                primaryContact(startBall!, spriteB: aBall!)
                startBall = nil
                return
            }
            
        }
    }

The way this is written the first touch has to be in a ball, however we could change it so that if the startBall is not set then we would set the startBall to the current ball being touched which would allow a swipes that pass through two balls but didn't start in a ball. I could change this in my current game, but the method above works well.

I have added a call to a new function primaryContact let's write that function. The function will take two colored balls and check to see if they are colors that can be combined, if so combine to make a new colored ball. This is a big nested set of if else code. Given that in sprite you can have switch statements with strings this could be rewritten with switch statements. However eventually I will convert this code to Objective-C and to C++ which would require it to be if else statements. Since either works just fine I will stick to the classic if else method.

We create a the function:
    func primaryContact(spriteA: ColoredBall, spriteB: ColoredBall) {

If the first ball is red then check and see if the second ball is yellow or blue, if so create an orange or purple ball.
        if spriteA.name == "red" {
            if spriteB.name == "yellow" {
                // make an orange ball
                ebcc_combineSprites(spriteA, spriteB: spriteB, color: "orange")
                orangeBalls.insert(spriteB)
            } else if spriteB.name == "blue" {
                // make a purple ball
                ebcc_combineSprites(spriteA, spriteB: spriteB, color: "purple")
                purpleBalls.insert(spriteB)
                
            }
OK, same thing if the first ball is yellow but test for red and blue to get orange and green. 
        } else if spriteA.name == "yellow" {
            if spriteB.name == "red" {
                // make an orange ball
                combineSprites(spriteA, spriteB:spriteB, color: "orange")
            } else if spriteB.name == "blue" {
                // make a green ball
                combineSprites(spriteA, spriteB: spriteB, color: "green")
            }

And finally if the first is blue then red to purple and yellow to green.
        } else if spriteA.name == "blue" {
            if spriteB.name == "red" {
                // make a purple ball
                combineSprites(spriteA, spriteB: spriteB, color: "purple")
                purpleBalls.insert(spriteB)
            } else if spriteB.name == "yellow" {
                // make a green ball
                combineSprites(spriteA, spriteB: spriteB, color: "green")
            }
        }
        
    }
But I still have not written the code to combine the sprites. Let's do that now. The combineSprites function will take two colored balls and string designating the new color. 
    func combineSprites(spriteA: ColoredBall, spriteB: ColoredBall, color: String) {
First I will move the second ball to be halfway between the two balls.
        spriteB.position.x = (spriteA.position.x + spriteB.position.x) / 2
        spriteB.position.y = (spriteA.position.y + spriteB.position.y) / 2
Then we will run a little animation that will turn the second ball into one with the new color.
        switch color {
        case "orange":
            spriteB.runAction(orangeAction!)
        case "green":
            spriteB.runAction(greenAction!)
        case "purple":
            spriteB.runAction(purpleAction!)
        default:
            spriteB.texture = SKTexture(imageNamed: color)
        }

Here I did use the switch statement instead of a bunch of if else statements. When we create the Objective-C and C++ code this will have to change. The switch statement in Swift requires a default case. This should never happen because we always send orange, green, or purple. However if for some strange reason we added some other color then we will just change the texture to an image with that color as the name. Again this never happens.

Now I want to remove the first ball from the scene. And since it will no longer have anything referencing it, it will be freed.
        spriteA.removeFromParent()

And finally I will change the name of the second sprite to the new color. This is critical so that we can test if it is touching other secondary colored balls and also so our touchesBegan and touchesMoved functions don't think it is a primary colored ball.
        spriteB.name = color
    }

OK they are combined, but I added the use of some actions that are not defined yet. Finally we get to use the animation images we created in the last step.

First I'll add optionals for each color action, and then a sound action to play when the balls are combined. I found a public domain sound that I downloaded. I'll let you go find your own. On the sound action I use waitForCompletion to tell it to start the drip sound and then continue with other actions.
    var orangeAction: SKAction?
    var greenAction: SKAction?
    var purpleAction: SKAction?
    var dripSound = SKAction.playSoundFileNamed("drip.wav", waitForCompletion: false)

We need to actually assemble the actions. I'll do that in the scene's didMoveToView function right after the self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
First I'll check to see if the action already was created in a previous didMoveToView call
        if orangeAction == nil {

Then I'll get an atlas with the animation images. The problem is we are not guaranteed what order they will be in. So I sort them and then I can create an animation with 0.075 seconds between images for just a little less than half a second total. Then I can create an sequence that plays the sound and then runs the image animation.
            let orangeAtlas = SKTextureAtlas(named: "orange")
            let orangeTextures = sorted(orangeAtlas.textureNames as! [String]).map { orangeAtlas.textureNamed($0) }
            let orangeAnimation = SKAction.animateWithTextures(orangeTextures, timePerFrame: 0.075)
            orangeAction = SKAction.sequence([dripSound,orangeAnimation])
        }
Repeat for green and purple
        if greenAction == nil {
            let greenAtlas = SKTextureAtlas(named: "green")
            let greenTextures = sorted(greenAtlas.textureNames as! [String]).map { greenAtlas.textureNamed($0) }
            let greenAnimation = SKAction.animateWithTextures(greenTextures, timePerFrame: 0.075)
            greenAction = SKAction.sequence([dripSound,greenAnimation])
        }
        if purpleAction == nil {
            let purpleAtlas = SKTextureAtlas(named: "purple")
            let purpleTextures = sorted(purpleAtlas.textureNames as! [String]).map { purpleAtlas.textureNamed($0) }
            let purpleAnimation = SKAction.animateWithTextures(purpleTextures, timePerFrame: 0.075)
            purpleAction = SKAction.sequence([dripSound,purpleAnimation])
        }

Now we need to add the images and sound to our project. Previously I created a group called images, but I am going to be putting sounds in here too, so I am going to rename it assets, a much better name.

Then File->Add Files to "EpicColorCrash"... and select the atlas directories we created in the last step.
Make sure Copy items if needed is selected and Create groups is selected. And click Add. Then download or create a sound for the drip.wav and add it to the project.


Build and run. Now you should be able to swipe between balls, get a cool little animation and a sound.
Here is the GameScene.swift file at the end of this step:
//
//  GameScene.swift
//  Epic Color Crash
//
//  Created by Daniel Burton on 7/25/15.
//  Copyright (c) 2015 Daniel Burton. All rights reserved.
//

import SpriteKit

class ColoredBall: SKSpriteNode {
}

class GameScene: SKScene {
    var lastBallDropped: CFTimeInterval = 0
    var newBallDelay: CFTimeInterval = 1.5
    var startBall: ColoredBall?
    var orangeAction: SKAction?
    var greenAction: SKAction?
    var purpleAction: SKAction?
    var dripSound = SKAction.playSoundFileNamed("drip.wav", waitForCompletion: false)

    func addColorBall(location: CGPoint, withColor color:String) {
        let sprite = ColoredBall(imageNamed: color)
        
        sprite.position = location
        sprite.name = color
        let body = SKPhysicsBody(circleOfRadius: 25)
        sprite.physicsBody = body
        body.allowsRotation = false
        body.friction = 0.0
        self.addChild(sprite)
    }
    
    func combineSprites(spriteA: ColoredBall, spriteB: ColoredBall, color: String) {
        spriteB.position.x = (spriteA.position.x + spriteB.position.x) / 2
        spriteB.position.y = (spriteA.position.y + spriteB.position.y) / 2
        switch color {
        case "orange":
            spriteB.runAction(orangeAction!)
        case "green":
            spriteB.runAction(greenAction!)
        case "purple":
            spriteB.runAction(purpleAction!)
        default:
            spriteB.texture = SKTexture(imageNamed: color)
        }
        spriteA.removeFromParent()
        spriteB.name = color
    }

    func primaryContact(spriteA: ColoredBall, spriteB: ColoredBall) {
        if spriteA.name == "red" {
            if spriteB.name == "yellow" {
                // make an orange ball
                combineSprites(spriteA, spriteB: spriteB, color: "orange")
            } else if spriteB.name == "blue" {
                // make a purple ball
                combineSprites(spriteA, spriteB: spriteB, color: "purple")
                
            }
        } else if spriteA.name == "yellow" {
            if spriteB.name == "red" {
                // make an orange ball
                combineSprites(spriteA, spriteB:spriteB, color: "orange")
            } else if spriteB.name == "blue" {
                // make a green ball
                combineSprites(spriteA, spriteB: spriteB, color: "green")
            }
        } else if spriteA.name == "blue" {
            if spriteB.name == "red" {
                // make a purple ball
                combineSprites(spriteA, spriteB: spriteB, color: "purple")
            } else if spriteB.name == "yellow" {
                // make a green ball
                combineSprites(spriteA, spriteB: spriteB, color: "green")
            }
        }
        
    }
    
    override func didMoveToView(view: SKView) {
        
        /* Setup your scene here */
        self.scaleMode = .AspectFit

        self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
        if orangeAction == nil {
            let orangeAtlas = SKTextureAtlas(named: "orange")
            let orangeTextures = sorted(orangeAtlas.textureNames as! [String]).map { orangeAtlas.textureNamed($0) }
            let orangeAnimation = SKAction.animateWithTextures(orangeTextures, timePerFrame: 0.075)
            orangeAction = SKAction.sequence([dripSound,orangeAnimation])
        }
        if greenAction == nil {
            let greenAtlas = SKTextureAtlas(named: "green")
            let greenTextures = sorted(greenAtlas.textureNames as! [String]).map { greenAtlas.textureNamed($0) }
            let greenAnimation = SKAction.animateWithTextures(greenTextures, timePerFrame: 0.075)
            greenAction = SKAction.sequence([dripSound,greenAnimation])
        }
        if purpleAction == nil {
            let purpleAtlas = SKTextureAtlas(named: "purple")
            let purpleTextures = sorted(purpleAtlas.textureNames as! [String]).map { purpleAtlas.textureNamed($0) }
            let purpleAnimation = SKAction.animateWithTextures(purpleTextures, timePerFrame: 0.075)
            purpleAction = SKAction.sequence([dripSound,purpleAnimation])
        }

    }
    
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        /* Called when a touch begins */
        
        for touch in (touches as! Set<UITouch>) {
            let location = touch.locationInNode(self)
            let balls = self.nodesAtPoint(location)
            if balls.count >= 1 {
                for aBall in balls {
                    startBall = aBall as? ColoredBall;
                    if startBall != nil {
                        return
                    }
                }
            }
        }
    }
    override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
        for touch in (touches as! Set<UITouch>) {
            let location = touch.locationInNode(self)
            let aBall:ColoredBall? = self.nodeAtPoint(location) as? ColoredBall
            if startBall != nil && aBall != nil && aBall != startBall {
                primaryContact(startBall!, spriteB: aBall!)
                startBall = nil
                return
            }
            
        }
    }

    override func update(currentTime: CFTimeInterval) {

        // if the time has come to drop a new ball
        if currentTime > (lastBallDropped + newBallDelay) {
                lastBallDropped = currentTime
                let colors = ["red", "yellow", "blue"]
                var xpos = Int(arc4random_uniform(UInt32(self.size.width-100) ))+50
                let ypos = self.size.height - 120
                let location = CGPoint(x: xpos, y: Int(ypos))
                var colorIndex = arc4random_uniform(3)
                addColorBall(location, withColor: colors[Int(colorIndex)])
        }

    }
    

}

No comments:

Post a Comment


My Bicycle Store