Jonas Støve Rasmussen (jonar17)

Mads Lykke Bach (mabac16)

Tobias Ahrenschneider Sztuck (toszt17)

https://bitbucket.org/iosprogrammingsdu/noodlejump/src/master/

Introduction

This blog post serves as documentation for the groups application for the course iOS programming (E19) at the Southern University of Denmark. This is an exercise in building/creating an application for iOS devices utilising the Swift programming language and iOS development environment Xcode.

The product of the coursework is a proof-of-concept presentation, as well as a working prototype/proof-of-concept application for iOS, built in the Swift programming language utilising one or more sensors in any iOS devices. This blog post serves as documentation for the whole project.

Noodle Jump is game heavily inspired by Doodle Jump. The game is intended to create entertainment in a noodle themed environment where the player must jump as far up as possible. 

The reason that we choose to create a game was because of the freedom to be creative and explore different approaches was appealing to us. Furthermore, it gave us several ways to implement theory that we have learned from the course which has been one of the main intentions of the project. 

The following sections of the blog will explain the methodologies and tools used, describe the applications design and functionality, and serve as a development manifesto.

Methods and materials

This section will sequentially introduce how the work on the project was performed and present the information in a (hopefully) easily understandable format. This section will not necessarily describe the methodologies used, but rather how and why the work was done in the way it was.

Brainstorm

A simple brainstorm was performed by the group in order to first identify what kind of application would be in part to develop in order to best exemplify the different tools that can be utilised in the iOS development environment, and in part what kind of application would seem fun and interesting to develop, and lastly what kind of application would best exemplify the curriculum of the course.

In the end, the group decided to develop a game in order to best utilise the sensors of any hand-held iOS device for the most user-interactivity while still using software patterns and methodologies from the curriculum.

Conceptual Design & Prototyping

The group identified an already existing game (Doodle Jump) that utilised the sensors that would be the most interesting to work with, and provide ample opportunity to implement game  functionality that directly correlated with the users actions with the physical iOS device.

As such, the conceptual design for the application was very much in the same vein as Doodle jump, identified key aspects of the existing application and modifying them. Initial design was mostly done with sketches on paper and paper prototypes in order to get a clear understanding of the core game concepts that were supposed to be implemented. 

Evaluation

Based of this simple prototyping and concept design, the group made a presentation to present the core concepts and get feedback from the initial early design of the application in order to get potential user feedback, and make any early changes in design.

Building a Digital Prototype

Digital prototyping was done utilising the SpriteKit framework for Swift provided by Apple. The first digital prototype was a simple application that made use of the collision system and the accelerometer sensor on devices. The purpose of the prototype was to have a working prototype that in some way resembled the Doodle Jump concept and at the same time made use of the Model View Controller pattern. This was a good starting point for the application and it ended up being the base for further development after a few improvements.

Use-case diagrams

Object Diagram

On this diagram you can see how the system start and what types of other controllers that can be presented. Furthermore it tells if a class is being used and what other classes that is being updated. This is done to give an overview of how the system is operating within itself.

Finding Requirements

Requirements for the application were elicited by identifying key actors and their intractability with the system from existing applications, then extrapolating this information and presenting the information in an easily understandable format in a use-case requirements descriptions.

NameMove character
ActorPlayer
DescriptionAs a player, I want to be able to move the playable character in a meaningful way, utilising the tilt sensor in my handheld iOS device.
FlowPlayer tilts phonePlayable character moves in corresponding direction horizontally on the x axis.
PreconditionsPlayer has pressed “start” and is actively playing the gamePlayer character is present in the gamePlayers phone has an accelerometer
PostconditionsPlayable character moves on the x axis

figure 1.3 – example of use-case requirements description

Implementations

SpriteKit

For this project the main framework that has been applied is SpriteKit. SpriteKit provides the foundation of the project as it maintains everything to physics simulation, graphics and more in a 2D environment. In most of the classes in the project, SpriteKit is used to bring life to each object in the form of sprites and in the actual gameScene it is used to simulate physics required for the game to function optimally. SpriteKit has therefore been a necessity in order to create Noodle Jump. An example from the Player class has been added below to demonstrate some of the use of the SpriteKit framework.

 var sprite = SKSpriteNode() 
        sprite = SKSpriteNode(imageNamed: "Player1")
        sprite.name = "Player"
        sprite.physicsBody = SKPhysicsBody(rectangleOf: sprite.size)
        sprite.setScale(0.75)

Core Motion
Another framework that has been applied to the development of Noodle Jump is the Core Motion framework. Core Motion is a framework used to gain access to different sensors within a device, such as accelerometer, gyroscope, pedometer and so on. It was used for Noodle Jump in order to gain access to the accelerometer of the device running the game, so that the player character is able to move when the device is tilted from side to side. A code example from the Player class has been added below, to demonstrate the use of Core Motion.

var motionManager = CMMotionManager()   
if motionManager.isAccelerometerAvailable {
             motionManager.accelerometerUpdateInterval = 0.01
             motionManager.startAccelerometerUpdates(to: .main) {
                 (data, error) in
                     guard let data = data, error == nil else {
                     return
                 }
                let currentX = self.sprite.position.x
                self.destX = currentX + CGFloat(data.acceleration.x * self.floatSpeed)
            }
             }

UserDefaults

UserDefaults was used in the application to enable persistence of data on the device running it. More specifically, it was used to save the score in a high score list, each time a player finished playing the game. Upon reopening the application, the high score would then be filled with the scores from previous runs with the use of UserDefaults. An example from the gameScene class has been added below to show how persistence has been applied in the code. The second example is from the TableViewController class and it demonstrates how we retrieve the data that has been saved.

Setting persistence data example:

if((playerObj.sprite.position.y < level1.fields[0].position.y - 50) && !gameIsOver){
                gameIsOver = true
                self.playerHighscoreList.append(playerObj.score.description)
                UserDefaults.standard.set(playerHighscoreList, forKey: "playerScorePersist")
                self.view?.window?.rootViewController?.dismiss(animated: true, completion: nil)
            }

Retrieving the persistence data example:

override func viewDidLoad() {
        super.viewDidLoad()
        array = UserDefaults.standard.object(forKey: "playerScorePersist") as? [String] ?? [String]()
        array = array.sorted() { $0.localizedCaseInsensitiveCompare($1) == ComparisonResult.orderedDescending }
    }
override func viewDidLoad() {
        super.viewDidLoad()
        array = UserDefaults.standard.object(forKey: "playerScorePersist") as? [String] ?? [String]()
        array = array.sorted() { $0.localizedCaseInsensitiveCompare($1) == ComparisonResult.orderedDescending }
    } 

Design Pattern

The design pattern used within the project is the Model-View-Controller pattern which enabled us to have a clear separation of concerns. The models in the project are the ones that contain data for each object, such as Player, Field and so on and they communicate with the controllers(gameScene) to enable the use of the data. The controllers then communicate with the views to update the graphical part being shown to the users. However in the case of making a game there are less views since majority of the application is running within the gameScene window and so it has a lot of responsibility. A model has been created to illustrate the appliance of the pattern in the project. 

Properties and Methods

In the HomeViewController there is a method called modalDismissed, which is used by a button that is connected to the current view’s exit, which then checks gets you to the previous window and within its controller checks for the modalDismissed. The reason why this method is necessary is that if it is not called, then it creates a memory leak which can stack more and more till there is no more memory on the device. 

import UIKit       
                
import SpriteKit       
                
       
                
       
                
class HomeViewController: UIViewController {       
                
       
                
    override func viewDidLoad() {       
                
        super.viewDidLoad()       
                
       // UserDefaults.standard.removeObject(forKey: "playerScorePersist")       
                
    }       
                
           
                
    @IBAction func modalDismissed(segue: UIStoryboardSegue) {       
                
        self.view?.window?.rootViewController?.dismiss(animated: true, completion: nil)       
                
    }       
                
       
                } 

The TableViewController is used for saving the data locally on the device, as well as showing the given array of input. 

import UIKit
class TableViewController : UITableViewController {
var array = UserDefaults.standard.object(forKey: "playerScorePersist") as? [String] ?? [String]()


@IBOutlet var highScoreTabel: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()
    array = UserDefaults.standard.object(forKey: "playerScorePersist") as? [String] ?? [String]()
    array = array.sorted() { $0.localizedCaseInsensitiveCompare($1) == ComparisonResult.orderedDescending }
}

   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return array.count
    }



 override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return "Highscores"
 }

   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "highScoreCell", for: indexPath)

    cell.textLabel?.text = "Score: \(array[indexPath.row])"
       return cell
}

The GameViewController is used to create check if the view that is getting presented is a SKView, and if it is then it will use an instance of the GameScene to present the game. 

import UIKit
import SpriteKit
class GameViewController: UIViewController {
@IBOutlet weak var playerScoreLabel: UILabel!

override func viewDidLoad() {
    super.viewDidLoad()



    if let view = self.view as! SKView? {
        // Load the SKScene from 'GameScene.sks'
        let scene = GameScene(size: view.bounds.size)

            // Set the scale mode to scale to fit the window
            scene.scaleMode = .aspectFill

            // Present the scene
            view.presentScene(scene)


        view.ignoresSiblingOrder = true
        view.showsFPS = true
        view.showsPhysics = false
        view.showsNodeCount = true
    }
}

The GameScene is the main functionality of the game. One function that is fairly important for the gameplay is the one called checkBoundaries() as it checks if you hit the outer edge of the screen, and depending on which side you hit the character is moved to the other side of the screen. However in order to get this to work properly the player was moved 5 units either left or right as if it would just have been the other side it would keep throwing the player back and forth at the same location. 

The next very important part of the game is the function called generateLevel() as it is the function that makes the level appear. The way of jumping is randomly generated constantly and it also checks if two fields spawn on top of each other, and removes the latest added field if it is colliding with another. Furthermore, to ensure that the player will actually be able to play the game, one field is always spawn directly below the player. 

 // This function generates the level. This is where all the logic for the fields is located.
    // Such as how many fields, the maximum distance between them and so on.
    func generateLevel() {
        // For loop that creates fields.
        for i in 0...level1.fields.count - 1 {
            if(i > 0) {
                let previousPosition = level1.fields[i-1].position
                let randomX = CGFloat.random(in: (-maxWidth/2)...(maxWidth/2))
                var randomY = CGFloat.random(in: previousPosition.y...previousPosition.y+100)
                if(randomY - previousPosition.y <= 100) {
                    randomY += CGFloat.random(in: 20...120)
                }
                level1.fields[i].position = CGPoint(x: randomX, y: randomY)
                
                self.addChild(level1.fields[i])
                
                //checks whether the fields are colliding
                for j in 0...level1.fields.count - 1{
                    if(j > 0){
                        if(j != i){
                            if(level1.fields[i].intersects(level1.fields[j])){
                                level1.fields[j].removeFromParent()
                            }
                        }
                    }
                }
            }else{
                // Sets starting fields to ensure player does not fall to his death upon spawn.
               level1.fields[i].position.x = 0
               level1.fields[i].position.y = -10
               self.addChild(level1.fields[i])
            }
            
        }

The function checkFieldLoadHeight() has two primary purposes. The first one is to check all fields if they are below the screen, if they are they will be removed from the scene. The second purpose is that it checks the losing conditions, which in this case is that it checks for the lowest spawned table’s position and if the player falls 50 units below this, the game is over and the player is moved to the main menu.

//check if fields are below screen and remove if they are
    func checkFieldLoadHeight(minY: CGFloat){
        for i in 0...level1.fields.count-1{
            if(level1.fields[i].position.y < minY){
                level1.fields[i].removeFromParent()
            }
            
            //checks if the player is lower than the lowest field
            if((playerObj.sprite.position.y < level1.fields[0].position.y - 50) && !gameIsOver){
                gameIsOver = true
                self.playerHighscoreList.append(playerObj.score.description)
                UserDefaults.standard.set(playerHighscoreList, forKey: "playerScorePersist")
                self.view?.window?.rootViewController?.dismiss(animated: true, completion: nil)
            }
        }
    }

Results

The finished product is an implementation that functionality wise resembles the game Doodle Jump, with a different artistic spin on it. When the app first starts up, the player is greeted a start screen where the player can choose to either start the game or view the highscores persisted on the device. A highscore is the highest point on the y axis that a player reached during gameplay. If a player presses “start”, the game starts and switches view to the game screen, when the player dies, he is taken back to the first view.

A playable character appears that can be controlled by utilising the gyroscopic sensor in the iOS device, and moving horizontally. The character jumps vertically automatically if he collides with any platforms that are placed throughout the game screen. These have been strategically generated in order to create a vertical path for the player to traverse the game screen. The player is always centered, meaning that as the player moves upward, the screen “scrolls” up revealing more platforms and the path ahead. If the player fails to land on any platforms and falls below the lowest platform currently on the screen, the game stops and the view switches back to the start screen. 

Discussion

The group believes that the current implementation of the game sufficiently exemplifies the core game functionality, and that any additions would not change the gameplay experience enough to warrant the development time.

But had there been more time to implement additional features to the core gameplay loop, the group already identified several things that would be relatively simple to implement, yet greatly enhances the experience. Considering the fact that the majority of core game logic already there in some way or another, implementing more features is relatively easy. Some examples of these could be : enemies, bosses, more power ups, and more different types of platforms.

Powerups technically already exists as game objects that the player can interact with, but they just don’t do anything at the moment. It would be relatively easy to change it to greatly enhance the power of the players jump for an amount of time. Breakable platforms already have a texture ready, all that would need to be done is a designation for a specific type of platform and an animation loop when the player collided with the platform.

In conclusion, seeing as the group mainly saw this course as an opportunity to learn Swift and developing software in a iOS development environment, the main product of this project is not as much the application as it is the knowledge outcome for all the group members. Although it is a course and it is cool and fun to develop applications that might become a consumer product in the future, the group had no such intentions and focused primarily  on the learning outcome.

Bibliography

https://en.wikipedia.org/wiki/Doodle_Jump

Leave a Reply