1.0      Introduction

Motivation

This application design was carried out as part of the requirements for the iOS programming course. One of the main attractions of mobile devices is their user-friendliness and ease of interaction. One method of interaction that is especially interesting to me at this time is audio signal processing. After being introduced to programming for the iOS platform, and the AudioKit tools, it was decided to make an audio-controlled application. And in order to keep the requirements as simple as possible, it was decided that a simple game format would be fine.

Aims & Objectives

One of the foremost aims is to develop familiarity with the iOS environment and the design of applications for this unique platform. We also aimed to be proficient at interface development in iOS using interface Builder in XCode IDE, and working with graphics, animation, sound and device sensors (such as touch).

Methodology

The IDE used was XCode7 and all source files were written in the swift programming language. For design of the game scenes, SpriteKit game technology was used, with support from the AudioKit libraries for audio effects.

The XCode7 Default IDE:

Screen Shot 2015-10-18 at 22.09.03

The product was designed iteratively. In the first stages, the proposed layout is as shown below.

Screen Shot 2015-10-18 at 22.09.18

After some user evaluation, and introduction to SpriteKit, it was decided to switch layout tools.

2.0      Implementation

Swift programming language is a recent upgrade from ObjectiveC. It is Apple’s new programming language. Swift 1 was released last year at WWDC 2014, and just recently Swift 2 was released as part of Xcode 7. Swift is an easy language to get started with, especially if you are a beginner to the iOS platform. In any case, it helps a bit to have some experience with designing user applications, to utilize previously acquired object oriented programming theory and practices in a new language on a new platform.

One of the nice things about Sprite Kit is it comes with a physics engine built right in! Not only are physics engines great for simulating realistic movement, but they are also great for collision detection purposes. The general consensus is; if you are a complete beginner, or solely focused on the Apple ecosystem, using Sprite Kit is preferred because it’s built in, easy to learn, and will get the job done. But if you want to be cross-platform, or have a more complicated game you may need to try something like Unity – it’s more powerful and flexible.

AudioKit is really simple to get started with. To include the static library in my project, all I had to do was drag the audiokit folder into my project in Xcode and add a linker header file.

Design Considerations

The work presented here is a very rudimentary demonstration of SpriteKit tools and the use of Swift programming language. It doesn’t pretend to be a sophisticated game, and the game logic could be improved.

System Overview

The  key components of the SpriteKit framework are an assets folder for sounds and images that will be used, a view controller(GameViewController) that is always first responder, and launches the first scene of the game. GameViewController is a normal UIViewController, except that its root view is a SKView, which is a view that contains a Sprite Kit scene.

Skærmbillede 2015-12-02 kl. 15.48.48

Scene Layout

One of the first things done was to make sure the screen orientation is set to portrait mode only. This means that the user is not able to play the game in another orientation mode. Setting the background color of a scene in Sprite Kit is as simple as setting the backgroundColor property. Another way to do this is to input the name of an image to be used.

The objects that appear on the screen during app execution are known as nodes. A sprite is a 2D image texture that can be applied to a node. Creating a sprite is as simple as putting in the name of the image to use. And to make it appear, it must be added as a child of the scene.

    let player = SKSpriteNode(imageNamed: "kid1")
    let goal = SKSpriteNode(imageNamed: "bus")
    let obstacle = SKSpriteNode(imageNamed: "skl")
    let obstacle1 = SKSpriteNode(imageNamed: "caf")
    let obstacle2 = SKSpriteNode(imageNamed: "block")
    let obstacle3 = SKSpriteNode(imageNamed: "bully")
    let obstacle4 = SKSpriteNode(imageNamed: "block")
    let obstacle5 = SKSpriteNode(imageNamed: "party")
    let obstacle6 = SKSpriteNode(imageNamed: "block")
    let background = SKSpriteNode(imageNamed: "bc")
    let slider = SKSpriteNode(imageNamed: "slider")
    let point = SKSpriteNode(imageNamed: "point")

 

In Swift, constants are declared with the keyword ‘let’. After the relevant sprites have been added (player, obstacles and goal), it is time to build the logic that controls how these sprites interact with one another. This is where SpriteKit’s built-in physics engine really comes in handy.

A physics world with configurable properties like gravity, is set up on the scene by default. The present scene is also set to be contact delegate in case of any collisions between sprites on that scene. The actions performed when a collision is detected are handled programmatically.  A physics body must also be created for each sprite in order to detect collisions. The physics body is essentially a bounding shape associated to a sprite.

        // position the player sprite
        player.position = CGPoint(x: size.width * 0.1, y: size.height * 0.1)
        player.physicsBody = SKPhysicsBody(rectangleOfSize: player.size)
        player.physicsBody?.dynamic = true
        player.physicsBody?.categoryBitMask = PhysicsCategory.Player
        
        // The contact listener watches out for the following
        player.physicsBody?.contactTestBitMask = PhysicsCategory.Goal
        player.physicsBody?.contactTestBitMask = PhysicsCategory.Obstacle7
        
        // Allow physics engine to handle contact responses i.e. bounce off of
        player.physicsBody?.collisionBitMask = PhysicsCategory.Obstacle
        player.physicsBody?.collisionBitMask = PhysicsCategory.Obstacle1
        player.physicsBody?.collisionBitMask = PhysicsCategory.Obstacle2
        player.physicsBody?.collisionBitMask = PhysicsCategory.Obstacle3
        player.physicsBody?.collisionBitMask = PhysicsCategory.Obstacle4
        player.physicsBody?.collisionBitMask = PhysicsCategory.Obstacle5
        player.physicsBody?.collisionBitMask = PhysicsCategory.Obstacle6
        
        // To catch fast collisions
        player.physicsBody?.usesPreciseCollisionDetection = true
        
        // To make the sprite appear on the scene.
        addChild(player) 

 

The most interesting bits of the above code are the contactTestBitMask and the collisionBitMask. The contactTestMask indicates what categories of interactions, the contact listener should be notified about.

The collisionBitMask indicates what categories of objects that the default physics engine should handle contact responses to (i.e. bounce off of).

    func didBeginContact(contact: SKPhysicsContact) {
        
        var MyPlayer: SKPhysicsBody
        var MyGoal: SKPhysicsBody
        
        if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
        MyPlayer = contact.bodyA
        MyGoal = contact.bodyB
            print ("Found player")
            } else {
            MyPlayer = contact.bodyB
            MyGoal = contact.bodyA
            print("Found goal")
            }
        
        if ((MyPlayer.categoryBitMask == PhysicsCategory.Player) &&
            (MyGoal.categoryBitMask == PhysicsCategory.Goal)) {
                print("Detected a collision")
                playerHitGoal(MyPlayer.node as! SKSpriteNode, goal: MyGoal.node as! SKSpriteNode)
        }
        
        if ((MyPlayer.categoryBitMask == PhysicsCategory.Player) &&
            (MyGoal.categoryBitMask == PhysicsCategory.Obstacle7)) {
                print("Uh Oh")
                playerHitBomb(MyPlayer.node as! SKSpriteNode, goal: MyGoal.node as! SKSpriteNode)
        }

    }

 

For this application, the contact listener is notified whenever the player sprite intersects with either the goal sprite or the moving obstacle (“monster”) sprite. When this happens, the didBeginContact method is called. This method passes the two bodies that collide, but does not guarantee that they are passed in any particular order. So this bit of code just arranges them so they are sorted first and then checked to see if the two bodies that collided are either the player and the goal, or the player and the monster. If so, it calls the next appropriate function to be executed.

Animations

The game would be pretty boring without adding actions. Sprite Kit provides a lot of extremely handy built-in actions that help to easily change the state of sprites over time, such as move actions, rotate actions, fade actions, animation actions, and more. Here we define some custom actions for each obstacle and then chain together a sequence of actions that are performed in order, one at a time.

        // Obstacle actions
        let wait = SKAction.waitForDuration(0.3)
        let movLeft = SKAction.moveBy(CGVector(dx: -8, dy: 0), duration: NSTimeInterval(obstDuration))
        let movRight = SKAction.moveBy(CGVector(dx: +8, dy: 0), duration: NSTimeInterval(obstDuration))
        let movUp = SKAction.moveBy(CGVector(dx: 0, dy: +10), duration: NSTimeInterval(obstDuration))
        let movDown = SKAction.moveBy(CGVector(dx: 0, dy: -10), duration: NSTimeInterval(obstDuration))
        let rotLeft = SKAction.rotateByAngle(-0.7853981, duration: NSTimeInterval(obstDuration))
        let rotLeftUp = SKAction.rotateByAngle(0.7853981, duration: NSTimeInterval(obstDuration))
        let rotRight = SKAction.rotateByAngle(0.7853981, duration: NSTimeInterval(obstDuration))
        let rotRightUp = SKAction.rotateByAngle(-0.7853981, duration: NSTimeInterval(obstDuration))
        
        // obstacle.runAction(SKAction.repeatActionForever((SKAction.sequence([rotLeft, wait, rotLeftUp, wait, rotRight, wait, rotRightUp]))))

 

Scene Transitions

While some games can essentially be executed on one scene, this games have several scenes, partly to develop familiarity with switching scenes in Swift for SpriteKit.

Transitioning to a new scene is pretty straightforward. Choose the desired scene to display, an animated transition, and the desired length of the transition between scenes. Transitioning to a scene is similar to calling a function. Depending on the scene in question, the correct parameters must be passed to the new scene.

For the GameOverScene, it expects two parameters namely, the level of gameplay (Int), and a win (true/false) Boolean value.

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        menuHelper(touches)
    }
    
    func menuHelper(touches: Set<UITouch>){
        for touch in touches {
            let nodeAtTouch = nodeAtPoint(touch.locationInNode(self))
            
            if nodeAtTouch.name == "Back" {
                
                runAction(
                    SKAction.runBlock() {
                        let reveal = SKTransition.fadeWithDuration(1.0)
                        let scene = StartScreen(size: self.size)
                        self.view?.presentScene(scene, transition:reveal)
                    })
            }
        }
    }

 

Game Levels and Persistent Data

A level counter was implemented to track the user’s progress through the game. This counter was also used to increase difficulty in subsequent levels of gameplay.

    func playerHitGoal(player:SKSpriteNode, goal:SKSpriteNode) {
        let decPlay = SKAction.scaleTo(0.3, duration: 3.0)
        player.runAction(decPlay)
        //fmOscillator.play()
        player.removeFromParent()
        goal.removeFromParent()
        counter++
        let nextScene = NextStep(size: self.size, counter: counter)
        self.view!.presentScene(nextScene, transition: SKTransition.fadeWithDuration(2.0))
    }

 

Level information and user name were stored using NSUserDefaults which makes the data persistent even after the application is shut down. During game play, a struct was used to store gameplay information. At the end of a game, the values stored in the struct are compared against the NSUserDefaults. If the game level reached is lower than the NSUserDefaults values, no action is taken, else the NSUserDefaults value is updated to the current struct variable value.

A struct allows you to create a structured data type which provides storage of data using properties and extending its functionality via methods. Since structs are value types, they are pass by value. This means their contents will be copied when passed into functions. Structs also help remove memory issues when passing objects in a multithreaded environment.

Player controls

  • Touch detection

SpriteKit includes a category on UITouch with locationInNode(_:) and previousLocationInNode(_:) methods. These are used to find the coordinate of a touch within a SKNode’s coordinate system.

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch = touches.first! as UITouch
        let touchLocation = touch.locationInNode(self)
        
        if (touchLocation.y > slider.size.height) || (touchLocation.y > slider.size.width) || (touchLocation.x < size.width * 0.1) || (touchLocation.x > size.width * 0.83) {
            print("Touch out of bounds")
                }
        else {
        // Tracking the slider
        let oldPos = point.position
        let newPos = touchLocation
        let dx = newPos.x - oldPos.x
        let dy = CGFloat(0.0)
        let displacement = CGVectorMake(dx, dy)

        let slideMove = SKAction.moveTo(touchLocation, duration: 0.2)
        point.runAction(slideMove)
        // Create the player actions
        let playDuration = CGFloat(2.0)
        let playMove = SKAction.moveBy(displacement, duration: NSTimeInterval(playDuration))
        player.runAction(playMove)
        
            }

    }

This functionality was also exploited to make clickable ‘button’ nodes.

  • Player Motion Simulation

The idea at the start of the project was to control the user by altering the frequency of an audio track. A slider was needed to perform this alteration. It is a tricky task to load a UISlider into a SpriteKit scene, so it was decided to build one from scratch. The slider present at the bottom of the game screen is actually composed of two separate sprites that have been constrained to move relative to each other to give the effect of a continuous sliding motion.

The displacement between consecutive slider positions is used to obtain a vector function by which the player sprite is moved. Also, the player sprite is by default set to perform a ‘float’ motion which moves it constantly up (along the y-axis) at a predefined rate, independent of slider position. In this version of the application, the slider (controlling the sound frequency) is used mostly to determine directionality (along the x-axis) of the player.

3.0      Conclusion

At the end of this project, it is safe to say that the desired objectives were met. There were a few challenges encountered that will be discussed further down. I have been able to use the basic necessary parts of the XCode IDE environment as well as external kits and libraries to create, develop and debug iOS applications. At the end of the project, a prototype was deployed on an iPad for user testing and feedback.

User Evaluation

Some users found the game too hard with the use of the slider control. The application was developed incrementally, and the player actions in particular was one of the functions that was gradually built on. At first, the player could only be controlled by tapping on the screen (the player then moves to the touched location) and then finally the slider controls were implemented.

Users generally showed a preference for the tap-to-move mode of control, with highscores being much higher than with the slider control.  Also, not all the users were happy about the constant sound and the frequently changing pitch, although care was taken from the programming side to limit the frequency values to a comfortable range and to use a ‘soothing’/’calm’ track.

Results

demonstration is available on YouTube.

Simulator Screen Shot 02 Dec 2015 15.18.31Simulator Screen Shot 02 Dec 2015 15.18.46Simulator Screen Shot 02 Dec 2015 15.19.01

The images above show the start screen and the two optional screens accessible from there. The actual gameplay and game over scenes are shown below:

Simulator Screen Shot 02 Dec 2015 15.19.28                Simulator Screen Shot 02 Dec 2015 15.20.05

The source code is available here.

Challenges

The player controls with the slider were a lot trickier than anticipated. Defining the constraints for the slider motion and generating realistic results for the player’s motion were tasks that required a lot of time and fine-tuning.

Recommendations

It would be a good idea to add borders to the game scene. This way, certain motions do not make the player move off the visible screen area. Another area that could be greatly improved in this application is the highscores list. Some suggestions include: an optional save button and a saved user list (player can just select their name from a list before play).

It would also be nice to have a restore/continue game option.

And finally, based on user feedback, aside from touch control, it would be nice to have control of the player with other input devices like joysticks, keyboard and mouse.

Acknowledgments

Special thanks to Jacob Nielsen for his patient guidance during the semester. And to Ray Wenderlich from raywenderlich.com for helpful tutorials.

 

 

 

Leave a Reply