María Karlsdóttir, makar17
Lea Fog-Fredsgaard , lefog14

Link to Bitbucket: https://bitbucket.org/iosprogrammingsdu/runningrabbit/src/master/

Introduction

The project is a game for children. The idea is to make a game, that differs from other games by using the phone’s sensors in the game.

In the game the user should be a character that moves through the level, with different obstacles and opportunities to earn points. The app’s theme is cartoon. The initial idea is that the character is going to be a bunny/rabbit.

One idea is that for every level the orientation of the phone should change. The first level in landscape, the second in portrait and the third in landscape, but seen from above. And then looping back to landscape.

In the portrait level the rabbit should jump, moving up the screen, the jump could be initiated by a verbal command or by tapping the screen. The landscape level seen from above, could be a labyrinth, were the rabbit would move when the phone is tilted.

The objective is to create an entertaining app that differs from other game apps by having different levels using the phone’s different orientation and using the phone’s sensors to control the game.

Methods and materials

Brainstorm

The original idea was obtained by brainstorming within the group.

Research

We looked into already existing apps in the App Store and Google Play store for already created games that we got inspired by.

Conceptual design and simple prototyping

Once the original idea was formed, we used pen and paper to draw up a simple prototype and a conceptual design.

Building a first navigational digital prototype

MarvelApp was used to create a digital prototype, where different screens and navigation were designed.

Evaluation

We showed the first simple prototype and the first digital prototype to potential users to get some early feedback. Those potential users were kids and young adults in our families.

Use-case diagrams and object diagrams

Lucidchart was used to create diagrams based on the conceptual design.

Finding requirements

Our idea and the evaluation from the test users, led to the specification of the requirements.

Implementation

To begin with we used XCode version 10.1 and Swift version 4.2. We used SpriteKit in Swift as the game engine as well as a typical Model-View-Controller class structure for the most part. The microphone sensor on the phone is used in the game.

Evaluation

Along the way of implementing the application, we got feedback a couple of times from our peers in class as well as family members.

Results

Brainstorm

In the below images the result of the original brainstorm is shown. The character moving through levels, for each level changing the orientation of the phone. The character overcoming different obstacles and having opportunities to earn points. At this point, it was not decided what the theme or the character should be. The brainstorm gave us the basic layout of the three levels.   

  

Research

The research led to the theme of the app. The background in “Keep Frog Alive” we found particularly nice, and we decided to keep working with something similar.

The research also led to us being more certain of the layout of the levels. The first two, Labyrinth and Modern Labyrinth, are examples of the “landscape seen from above” level. The next two, Keep Frog Alive and Riseup are examples of what we though of for the portrait level. The last three images give an idea of the landscape level, which is the one we ended up implementing.

 

 

Conceptual design and simple prototyping

In this part of the project we refined the ideas we got from our research by making simple prototypes in Marvelapp. We made a prototype for the menu and the leaderboard/high score list, as well as three conceptual levels. Landscape, portrait and landscape seen from above. We worked with a rabbit for the character and found some nice pictures, which we used as the background. As the character were a rabbit, it made sense that the points were carrots, and the leveling up was a rabbit hole.

Alternative design

We made an alternative design, changing the background and the “ground” and obstacles. When we saw the result of this design we noticed that changing the background had a big impact on the experience. That made us sure that we wanted to look into having good graphics in our own app.

The below drawing shows the navigation of / interaction with the app. The start screen is the menu, no. 1, from where it is possible to start a new game, no. 2, continue a game played earlier, no. 6, or see the leaderboard/high score list, no. 5.

When choosing to start a new game, the navigation from there is to play and finish the level, then the next level will start, no. 3 and after that no, 4. If all the levels are completed, the leaderboard is presented. If the rabbit dies in the game, the leaderboard is also presented. Similar navigation when choosing to continuing a game.

Evaluation

The prototype made us aware of how different backgrounds can change the experience for the user.

We had feedback from both the peers in class and family. They liked the appearance of the app and thought it was a fun idea that you should change the phone’s orientation, when the levels changed. In the testing is was obvious that the app needed a possibility to navigate back to the menu, from both the leaderboard and the levels.

Building a first navigational digital prototype

We found a nice background on the webpage www.gameartguppy.com we decided to use. We also found a monkey which matched great with the background theme, so we decided to change the character from a rabbit to a monkey.

Evaluation

We got a lot of positive feedback on the first digital prototype. The users liked the background and found the monkey funny.

One of the users found that a pause button was missing, so it would be possible to pause the game and maybe navigate back to the menu. As the actual end of the game was still not implemented so the users were a bit confused how the game would end.

Use-case diagrams

We created a use-case diagram that show the user interaction within the app. The user can choose to start a new game, check the high score table or read the about page. If a user is playing a game he can pause the game at any point and if he reaches a high enough score to be in the top 10 he is able to input his name for the leaderboard.

This diagram evolved as we were implementing the app. At first we had a Continue game possibility for the user but we were not able to implement that in time. About was added to the use-case since we needed to acknowledge where the graphics came from we used in the app.

Object diagrams

We created a MVC diagram based on the idea presented in the Stanford lectures.

Controllers are marked in purple, views in green, models in blue and data storage in white.

Our main model structure is how the game is set up. The object Game has one monkey and some bananas, rubbles, statues, diamonds and fire. All of them inherit from the object Sprite which has some common features that are used across all sub types.

The data storage in the application is UserDefaults which keeps track of the high scores in the game.

Since we are using SpriteKit the game itself is conducted in GameScene. As far as we could tell that is kind of a view but it doesn’t really encourages you to use a MVC pattern within GameScene.

Finding requirements

Based on our idea and the evaluations, the following requirements were specified:

  • The character should jump, either by tapping the screen or verbal command.
  • The character should be moving towards the right side of the screen while the background and obstacles moved in the opposite direction
  • There should be obstacles and the character should have the possibility to die.
  • Points gathering is possible and a high score list.
  • It should be possible to pause the game, and navigate back to the menu.
  • In level 2 and 3, the movement of the character should be by tilting the phone.

Implementation

The structure of our application is originally based on this SpriteKit tutorial from Ray Wenderlich: https://www.raywenderlich.com/71-spritekit-tutorial-for-beginners.

All of the views can be viewed in both portrait and landscape, except the game itself, the landscape mode is forced. Below screenshots can be found of all of the views:

 

 

The connection between them can be seen in the below image of the storyboard where segues can be seen:

We decided not to use a NavigationController that Swift offers because we had some problems with forcing the GameViewController to be in landscape orientation that we decided to be quite important. We didn’t want to focus to much of our time for that problem since our research online only revealed for us that people have had trouble with this exact problem. Therefore we chose to have the navigation in the app created by hand with segues for now since the application is not large.

HighScore.swift

The HighScore is stored in UserDefaults since the data is rather simple. A list is stored for the highest 10 scores. These functions are called from the GameOverController and the HighScoreController to read and write the data from the UserDefaults.

    static func getData(from userDefaults: UserDefaults) -> Array<HighScore>? {
        let rawData = userDefaults.object(forKey: DefaultKeys.highScoreList.rawValue)
        if rawData == nil { return nil }
        let data = NSKeyedUnarchiver.unarchiveObject(with: rawData as! Data) as? Array<HighScore>
        return data!.sorted(by: { $0.score > $1.score })
    }
    
    static func setData(from userDefaults: UserDefaults, with highScores: Array<HighScore>) {
        let encodedData = NSKeyedArchiver.archivedData(withRootObject: highScores)
        userDefaults.set(encodedData, forKey: DefaultKeys.highScoreList.rawValue)
    }

Furthermore UserDefaults are used to store the score of the current player when that player dies so that the user can input their name before it can be stored in the high score list.

                    userDefaults.set(score, forKey: DefaultKeys.currentPlayerScore.rawValue)

A tableView is used to display the high score list.

Game.swift

The layout of the game is defined in the class Game. With the layout we mean the structure of the statues, bananas, rubble, fires and diamonds. To help us figuring out the best location for each sprite we setup kind of a grid, that had 29 blocks and three levels; ground, first, second. The blocks represent the X position in the space and the levels represent the Y position. We decided to have three levels so that the statues could be positioned on the ground, a bit above the ground and even higher from the ground (seen in block, 13, 14 and 15).

Below can then be found part of the implementation where the numbers each variable is the number of pixels from the start of the level. For the first level (number 0) the start is at 0.

    struct Layout {
        static let fireOnGroundY : Double = 35
        static let rubbleOnGroundY : Double = 10
        static let statueOnGroundY : Double = 40
        static let statueOnFirstY : Double = 120
        static let statueOnSecondY : Double = 180
        static let bananaOffsetY : Double = 70
        static let diamondOffsetY : Double = 120
        
        static let block1X : Double = 300
        static let block2X : Double = 410
        ...
        static let block28X : Double = 2240
        static let block29X : Double = 2280
    }

The initialiser of the game then creates the other sprites that are used in the game.

    init(screneHeight: Double, screneWidth: Double, ground: Double) {
        self.height = screneHeight
        self.width = screneWidth
        self.groundY = ground
        
        self.monkey = Monkey(height: Double(screneHeight/5),
                             x: screneWidth / 8,
                             y: groundY + (screneHeight/5)/2)
        
        self.bananas = [Banana]()
        self.statues = [Statue]()
        self.rubbles = [Rubble]()
        self.diamonds = [Diamond]()
        self.morefire = [Fire]()

        // We create the first and second levels right away
        createMoreSprites()
        createMoreSprites()

GameScene.swift

The background is repeated by creating two different sprites that are the background. Both are drawn up at the beginning, one after another, and when we reach the end of the first one it is repositioned at the end of the second one. The inspiration was found in this post: https://stackoverflow.com/questions/26347559/endless-scrolling-repeating-background-in-spritekit-game-swift

        if(background1.position.x - frame.size.width < -background1.size.width) {
            background1.position = CGPoint(x: background2.position.x + background2.size.width - frame.size.width, y: background1.position.y )
        }
        if(background2.position.x - frame.size.width < -background2.size.width) {
            background2.position = CGPoint(x: background1.position.x + background1.size.width - frame.size.width, y: background2.position.y )
        }

The action of a game over is controlled by the variable isGameOver which has a didSet that performs the appropriate segue.

    var isGameOver = false {
        didSet {
            if isGameOver {
                isPause = true
                var highScoreList = HighScore.getData(from: userDefaults)
                if (highScoreList == nil) {
                    // First time user runs the app, we need to store datastructure to be able to keep high scores
                    highScoreList = setupHighScoreList()
                    HighScore.setData(from: userDefaults, with: highScoreList!)
                }
                if (score > highScoreList![(game?.highScoreCount)!-1].score) {
                    // You only get a new high score if you beat the pervious ones
                    // if you get equal score as the 10th seat then you get nothing
                    userDefaults.set(score, forKey: DefaultKeys.currentPlayerScore.rawValue)
                    self.gameViewController?.performSegue(withIdentifier: "showGameOverController", sender: self)
                }
                else {
                    self.gameViewController?.performSegue(withIdentifier: "showHighScoreController", sender: self)
                }
            }
        }
    }

In sceneDidLoad and didMove(to) we setup the entire game

    func dataSetup() {
        let orientation = UIDevice.current.orientation
        var frameHeight = frame.size.height
        var frameWidth = frame.size.width
        if orientation == UIDeviceOrientation.portrait {
            // We need to use the height and width of the device to setup the graphics
            // so if the device is in portrait mode (which it should be, becuase the game is played in landscape)
            // we use height as width and width as height
            frameHeight = frame.size.width
            frameWidth = frame.size.height
        }
        
        game = Game(screneHeight: Double(frameHeight), screneWidth: Double(frameWidth),ground: Double(frameHeight / 7))
        monkey = SKSpriteNode()
        
        scoreLabel.text = "0"
        scoreLabel.position = CGPoint(x: frameWidth - frameWidth/20, y: frameHeight - frameHeight/10)
        pauseButton.name = "pause"
        pauseButton.size = scoreLabel.frame.size
        pauseButton.position = CGPoint(x: frameWidth/20, y: frameHeight - frameHeight/10 + pauseButton.size.height/2)
        pauseButton.isUserInteractionEnabled = false
        playButton.name = "play"
        playButton.size = scoreLabel.frame.size
        playButton.position = CGPoint(x: frameWidth/20, y: frameHeight - frameHeight/10 + pauseButton.size.height/2)
        playButton.isUserInteractionEnabled = false
        playButton.isHidden = true
    }
    
    override func didMove(to view: SKView) {
        buildBackground()
        buildGround()
        
        buildMonkeySprite()
        animateMonkey()
        buildStatueSprite(gameStatues: (game?.statues)!)
        buildRubbleSprite(gameRubbles: (game?.rubbles)!)
        buildBananaSprite(gameBananas: (game?.bananas)!)
        buildDiamondSprite(gameDiamonds: (game?.diamonds)!)
        buildFireSprite(gameFires: (game?.morefire)!)
        addChild(scoreLabel)
        addChild(pauseButton)
        addChild(playButton)
        
        physicsWorld.contactDelegate = self
    }

We have the monkey jump by applying impulse to it’s physicsBody but this is triggered from touchBegan.

    func monkeyJump() {
        monkey?.physicsBody?.applyImpulse(CGVector(dx: 5, dy: 45))
    }

The collision detection in the game is handled with spriteKit’s collision detection but we want to know when the monkey earns points and when it dies. The collection of diamonds it twofold since we have two diamonds. The regular ones (blue) earn more points than a banana, but the red ones imply that a level has been finished. The speed is increased and the number of points each banana/diamond will give are doubled. 

    func didBegin(_ contact: SKPhysicsContact) {
        let bodyA = contact.bodyA.categoryBitMask
        let bodyB = contact.bodyB.categoryBitMask
        
        if (bodyA == Game.PhysicsCategory.monkey && bodyB == Game.PhysicsCategory.banana) {
            if let banana = contact.bodyB.node as? SKSpriteNode {
                collectBanana(banana: banana)
            }
        } else if (bodyA == Game.PhysicsCategory.monkey && bodyB == Game.PhysicsCategory.diamond) {
            if let diamond = contact.bodyB.node as? SKSpriteNode {
                collectDiamond(diamond: diamond)
            }
        } else if (bodyA == Game.PhysicsCategory.monkey && bodyB == Game.PhysicsCategory.fire) {
            isGameOver = true
        }
    }

The sprites are built all in the same way but to demonstrate we’ll take a look at the monkey sprite. The monkey is the only one that is animated by a texture but the other ones are a spriteNode initialised with an image.

    func buildMonkeySprite() {
        
        let monkeyAnimatedAtlas = SKTextureAtlas(named: "MonkeyImages")
        var runFrames: [SKTexture] = []
        
        let numImages = monkeyAnimatedAtlas.textureNames.count
        for i in 1...numImages {
            let monkeyTextureName = "monkey_run_\(i)@2x.png"
            runFrames.append(monkeyAnimatedAtlas.textureNamed(monkeyTextureName))
        }
        monkeyWalkingFrames = runFrames
        
        let firstFrameTexture = monkeyWalkingFrames[0]
        monkey = SKSpriteNode(texture: firstFrameTexture)
        monkey?.position = CGPoint(x: (game?.monkey?.startPosX)!, y: (game?.monkey?.startPosY)!)
        let monkeyNewHeight = game?.monkey?.CGFloatHeight
        let monkeyNewWidth = game?.monkey?.CGFloatWidth(
            oldHeight: (monkey?.size.height)!,
            oldWidth: (monkey?.size.width)!)
        monkey?.size = CGSize(width: monkeyNewWidth!, height: monkeyNewHeight!)
        
        monkey?.physicsBody = SKPhysicsBody(texture: firstFrameTexture, size: (monkey?.size)!)
        //monkey?.physicsBody = SKPhysicsBody(rectangleOf: (monkey?.size)!)
        monkey?.physicsBody?.categoryBitMask = Game.PhysicsCategory.monkey
        monkey?.physicsBody?.collisionBitMask = Game.PhysicsCategory.ground | Game.PhysicsCategory.statue | Game.PhysicsCategory.rubble | Game.PhysicsCategory.diamond
        monkey?.physicsBody?.affectedByGravity = true
        monkey?.physicsBody?.allowsRotation = false
        monkey?.physicsBody?.velocity.dx = 1
        
        addChild(monkey!)
    }

The animation of the monkey uses texture but the movement itself is done by adding velocity to it’s physicsBody as can be seen above.

    func animateMonkey() {
        if isPause {
            return
        }
        
        // have monkey run in place
        monkey?.run(SKAction.repeatForever(
            SKAction.animate(with: monkeyWalkingFrames, timePerFrame: 0.05, resize: false, restore: true)),
                    withKey:"walkingInPlaceMonkey")
    }

The movement of the rest of the sprites is done in the update function. This also includes the detection of blowing out the fire but that idea we got from this post: https://stackoverflow.com/questions/31230854/ios-detect-blow-into-mic-and-convert-the-results-swift. The detection of wind does not include the sound, so it doesn’t work for the user to talk/shout to have the fire blown out. It only works when blowing/coughing.

    override func update(_ Time: CFTimeInterval) {
        
        if isPause || isGameOver {
            return
        }
        
        // Move the background and obstacles
        
        background1.position = CGPoint(x: background1.position.x - CGFloat(changeInBackground), y: background2.position.y)
        background2.position = CGPoint(x: background1.position.x - CGFloat(changeInBackground), y: background2.position.y)

        // Probably only want to do this for the visible ones, on screen
        for statue in statues {
            statue.position = CGPoint(x: statue.position.x - CGFloat(changeInBackground), y: statue.position.y)
        }
        for rubble in rubbles {
            rubble.position = CGPoint(x: rubble.position.x - CGFloat(changeInBackground), y: rubble.position.y)
        }
        for banana in bananas {
            banana.position = CGPoint(x: banana.position.x - CGFloat(changeInBackground), y: banana.position.y)
        }
        for diamond in diamonds {
            diamond.position = CGPoint(x: diamond.position.x - CGFloat(changeInBackground), y: diamond.position.y)
        }
        for fire in morefire {
            fire.position = CGPoint(x: fire.position.x - CGFloat(changeInBackground), y: fire.position.y)
        }
        
        if (monkey?.position.x)! < CGFloat(0) {
            isGameOver = true
        }
        
        // Repeat the background
        if(background1.position.x - frame.size.width < -background1.size.width) {
            background1.position = CGPoint(x: background2.position.x + background2.size.width - frame.size.width, y: background1.position.y )
        }
        if(background2.position.x - frame.size.width < -background2.size.width) {
            background2.position = CGPoint(x: background1.position.x + background1.size.width - frame.size.width, y: background2.position.y )
        }
        
        // Blow out the fire
        if gameViewController!.audioSession!.recordPermission == AVAudioSession.RecordPermission.granted {
            gameViewController!.recorder.updateMeters()
            
            let level = gameViewController!.recorder.averagePower(forChannel: 0)
            if level > -10 {
                let monkeyPositionX = monkey?.position.x
                if (morefire.count>0) { // checking if there are some fires to be found that the user has still to pass
                    var nextFire = morefire[0]
                    for fire in morefire.sorted(by: { $0.position.x > $1.position.x }) {
                        if fire.position.x < monkeyPositionX! {
                            // Monkey has already gone past this fire
                            continue
                        }
                        nextFire = fire
                    }
                    if (nextFire.position.x - monkeyPositionX! < 100) {
                        // Can only blow out next fire if close to it
                        removeChildren(in: [nextFire])
                        if let index = morefire.index(of: nextFire) {
                            morefire.remove(at: index)
                        }
                    }
                }
            }
        }

    }

We created two extensions as a part of a refactor to simplify our code.

extension Game {
    var CGFloatHeight: CGFloat {
        return CGFloat(height)
    }
    var CGFloatWidth: CGFloat {
        return CGFloat(width)
    }
}

extension Sprite {
    var CGFloatHeight : CGFloat? {
        return CGFloat(height!)
    }
    func CGFloatWidth(oldHeight : CGFloat, oldWidth : CGFloat) -> CGFloat? {
        return CGFloat(Double(oldWidth) * height! / Double(oldHeight))
    }
}

Second evaluation

A suggestion was to add background music to the app. We could see that the game started a bit fast, the users had difficulties finding out how control the monkey. Otherwise they liked the app.

Final evaluation

We observed that the users just accepts the permission on using the microphone without reading the text. So the user does not know how to put out the fire. So it would be a good idea to add some explanation on how to put out the fire.

One place in the app the monkey could get stuck between two statues. The first level is endless, so this needs to be changed so you navigate to the next level.

Things we changed based on the evaluations:

  • Navigation back to the menu, from the current level and from the high score list.
  • Added a pause button
  • Changed the speed of the game

In order to meet the requirement to use a sensor we decided to add the feature of a user “blowing out the fire”. In order to get over a particular obstacle in the game a user has to blow at the phone. The microphone detects it and removes the fire that is next on the screen.

At the end of the blog post there can be found videos where people are playing the final version of the game.

Discussion

Our results

The objective was to create a game which differs from other games. We feel that we met our goal, in particular by coming up with the “blowing out the fire” part.

During the making of the app our idea evolved and we decided to focus solely on one of the levels. We wanted to have a working complete first level instead of trying to create none finished. We created an application with a character which moves through the level, earns points, jumps over statues and fire. Originally we had the idea of having the character a bunny/rabbit but when we came across graphics that included a monkey in a jungle we opted in using that instead.

The two other levels, described above, that we didn’t implement do still apply and would be interesting to keep on going with this application.

Overall we met our requirements and feel like we fulfilled our goal.

Evaluations

Overall, we have received a lot of positive feedback on the app. There were suggestions and things that needed to be changed for the app to be functional for the user. And suggestions that the user through would be nice to have.

We have changed the things that needed to be changed for the app to be functional for the user.

Good things with our app

The feature of “blowing out the fire” is something we haven’t seen before in games we have played. That is definitely something that would make this app stand out compared to others.

Graphics played a big role in our game and the ones we used from www.gameartguppy.com really improve the user experience.

Things that could be changed

We are not using a navigationController as provided in Swift, which is something that we don’t like. We are handling the navigation with segues between views, which works since we don’t have that many views. But is definitely something that would be worth working on, since that implementation is much smoother than the one we have now. The reason why it is like that, is that we were not able to “fix” the gameViewController to landscape orientation when the gameViewController was a part of a navigationController. We found numerous articles online stating this so we decided not to spend too much of our time on that.

Comparison with other apps

Monkey Jump is a lot like other game apps, where the character automatically moves and jumps by tapping the screen. But Monkey Jump is using the microphone to blow out the fire, we have never seen a feature like that before in a game app. So on that specific point the app differs from other apps.

If we have had the time, and implemented level 2 and 3, the app would also have been different then other apps, with the change in orientation.

Conclusion

By creating this application we learned a lot since neither of us had any experience with Swift and iOS programming.

We were able to create a game that is entertaining and innovative that test users around us (family and friends) would be willing to play.

Perspective

To complete our app, there are different things that can be refined and additions to work on.

  • As the application is now we create a set number of levels once the game starts. A better way would be to have levels created as the user needs them. The same idea could be used as for the background.
  • The first level is endless, this of course needs to be changed, when the next level is implemented
  • Have a better indication for the player once the monkey dies. Either with a dead monkey on the screen or with a game over label across the screen or both.
  • The keyboard is blocking the “view” when the monkey dies and the user gets the opportunity to add his/hers name to the high score. The keyboard should disappear when the user has typed his/hers name. (gameOverView)   
  • Speed of game/monkey seems to not be exactly the same every time you run the game (eg. second time in simulator becomes much slower)
  • Find out how to have design responsive for game across devices.
  • Work on “pause” mode, not just pause/play button but eg. grayed out with a kind of menu as seen in many games.
  • Add some explanation on how to put out the fire. The users just accepts the permission on using the microphone without reading the text. Maybe it could be a textlabel, appearing if the user dies the same place twice, letting the user know hi/she can put out the fire by blowing at the phone.
  • As our plan was originally we had meant to implement that the user could continue their game that they had already started. We couldn’t get to that but it would be a nice addition.
  • Implement the portrait level.
  • Implement the landscape as seen from above level.

 

Videos of people playing app

 

Leave a Reply