– by Daniel Schmidt (dasch15) og Mathias Nielsen (mathn16)

Video – Rolleo Mapeo

(1) Introduction

Rolleo Mapeo is a top down maze game, which takes its inspiration from the original board game Labyrinth Ball. The idea of the game is to navigate a ball through a maze, containing obstacles, that will block your path, but also obstacles that will result in a lost game. The game seeks to provide a challenge for the player, and at the same time being competitive. The game contains 3 major parts. The base game, which is a simple ball that can be navigated through a maze of obstacles. The game should then have a high score system implemented In order to accommodate the competitive aspect of the game. This system should a time score, which is set by measuring the time it takes the ball to go from the start node to the end node of the game. At last the game should contain a basic navigational menu, which will connect the two parts of the game with each other, enabling the player to see the high scores already achieved in the game.

 

 (2) Methods and materials

For the methods and materials, we have in this project utilized a brainstorm method in the requirements phase of the project, this we have done in order to get an idea of what we want to develop. After the idea was in place, we created a use case diagram, in order to identify base functionality to be contained in the game.

We then created our first prototype, which reflected the base game. And from the first prototype, we implemented all the functionality on top of the base game.

 

Version control

The game is developed to be played on a 13” iPad. This has been done, to ease the way the levels are designed. If the game is executed on a device with a different screen size than the current 13”, the levels would be formatted incorrectly and might deem the game unplayable.

 

Model view controller

The game is developed to comply with the Model view controller design patter (MVC). This pattern defines how the frontend and backend system are connected. The design pattern shows how the data from the View of the system (frontend) needs to be parsed through the system, using a set of controllers. These controllers then pass the information further on to the Model part (backend). Here the Model makes a set of calculations based on the input it gets from the controller, it can then parse mortifications or new data back to the controller, which then parses it further on to the View. The view then displays the modified data to the user of the system, creating a response to the users’ interaction with the system.

 

The model part of the MVC, functions as the backend of a system. Here lies all the base game data and functionality of the game. The controller side of the MVC can be seen as a handler, which handles the connection from the model to the View. The view is the frontend of the game, this is the visual representation of the system for the user. The controllers picks up data from the view, and parses it to the model, in turn the model then returns data that the controller gets tasked with displaying in the view for the user.

SpriteKit

SpirteKit has been used for creating and handling animations of objects in the game. It is a framework that makes it easy to create high-performance and battery efficient 2D games. Which is why we chose to use it. It is integrated in the Swift standard libraries, which makes it easily available. SpriteKit allows you to alter speed, gravity and mass of moving objects. It also contains collision detection, which is a central part of almost any game.

Prototyping

Throughout the course we have been assigned to present our working prototypes over multiple times. This has been done to assure that noticeable progress has been made throughout the course. The prototypes has each shown a new feature implemented, which in terms let up to the final product.

(3) Results

Base game implementation

The implementation of the base game is quite simple. Since the only functionality of the game, is that you have to navigate a ball through a maze of obstacles. The only thing the base game is consisting of, is a collision detector.

func didBegin(_ contact: SKPhysicsContact) {
        let bodyA = contact.bodyA.node as! SKSpriteNode
        let bodyB = contact.bodyB.node as! SKSpriteNode
    
        if bodyA.name == "rolleo" && bodyB.name == "startNode" || bodyA.name == "startNode" && bodyB.name == "rolleo"{
            
            HighScore.hs.start()
        }
        
        else if bodyA.name == "endNode" && bodyB.name == "rolleo" || bodyA.name == "rolleo" && bodyB.name == "endNode" {
            HighScore.hs.stop(level: level)
            NotificationCenter.default.post(name: NSNotification.Name(rawValue: "stop"), object: nil, userInfo: ["level":level])
        }
        
        else if bodyA.name == "deathNode" && bodyB.name == "rolleo" || bodyA.name == "rolleo" && bodyB.name == "deathNode" { 
            playerDies()
        }
    }

 

This collision detection has been placed in the above code snippet. The first part of the function, didBegin, is retrieving the two colliding objects and force converts them as SKSpriteNode. These two nodes, has the ability to be any objects in the game, and it allows us to define the nodes as whatever we want them to be. In our case, we have defined them as two constants, where the value of the constant, is any object that it has contact with. After the two constants has been defined, the method then checks if the two nodes making contact to each other is our player “rolleo” and the start node. If this is the case and the two nodes is, in fact, in contact with each other, it means that the player has passed the start node. Therefore we can call the Highscore.hs.start() which activates  the Highscore counter. 

If the player is not In contact with the startNode. The method will check if it is in contact with the endNode, which is defined as the end of the map, thereby meaning that the player has won the game, since he has successfully navigated through the entire map, without dying. When this happen, the Highscore.hs.stop() is called, making the highscore counter stop counting. And after that, a post notification will be sent from the model of the game towards the controller, making a switch in scenes and taking the player back to the main menu.

If however it is not the case that the player is in contact with the endNode, the method will check if the player is in contact with any deathNode on the map. If this is the case, the player has been in contact with an entity defined to kill the player. Therefore the method will call playerDies().

func playerDies(){

self.view?.window?.rootViewController?.dismiss(animated: true
, completion: nil)

NotificationCenter.default.post(name: NSNotification.Name(rawValue: "death"), object: nil)
}

Then function playerDies, has two functionalities. The first is to dismiss the current view of the game, so that the gamescreen is no longer run by the app. This has been implemented in order to accommodate a memory leak, where each time the game was started, it would stack the gamescreens on top of each other until the device ran out of memory. After the current view has been dismissed, the method sends a post notification from the model to the controller, changing the current scene to a Game Over Screen.

 

Highscore implementation with local save data

In order to give the game a competitive aspect, we have implemented a highscore system. Which notes the time it takes for the player to traverse from first contact with the StartNode to first contact with the EndNode. This is done through the Start() and Stop() method in the HighScore class.

func start() {
        currentTime = Date()
        passed = true
    }
    
    func stop(level: Int){
        if passed {
            highStruct.times.append(Double(Date().timeIntervalSince(currentTime)))
            passed = false
            Storage.store(highStruct, to: .documents, as: "times.json")
        }
    }

The start method notes the Date() at which the startNode has been passed, and the stop() method subtracts the Date() at which the endNode collides with the player, it then takes the TimeInterval between the start and stop time, and saves it as a double in the array times. Furthermore the stop() method calls a 3rd party class called Storage, which is in charge of saving our times Array to the local documents directory as a .json file.

Inorder for the 3rd party class to be able to save the highscores locally, we made the struct called HighscoreStruct.

struct HighscoreStruct: Codable {

var times: [Double]
    
    init(){
        times = []
    }

This struct only contains an Array consisting of doubles, but it extends codable making the Storage class able to encode it into a .json file, and saving it locally.

 

How the model view controller is held.

In order to uphold the Model View Controller design pattern. We have made sure that anything the user interacts with does not directly refer to the model part of our game, this means all classes that are not of type UIViewController. The navigational menu is made purely through the storyboard, meaning that any interaction from the user on buttons shown in the menu, only presents the next scene by showing them modally. Meaning that no scenes are switched programmatically when navigating the menu.

In order to switch scenes in case of the player dying or winning the game, is made through the NotificationCenter. The NotificationCenter then posts a notification and is observed in the respective controller, where the controller will instantiate the new view, based on the name of the notification, ultimately presenting it for the user. By doing this we uphold the restrictions of the model view controller, as the model only has contact to the controller and not the view of the game.

Such a relation can be seen in the GameScene and GameViewController.

In the method playerDies() in GameScene we utilize a notification post function

NotificationCenter.default.post(name: NSNotification.Name(rawValue: "death"), object: nil)

Here we post a notification with the name “death”.

We then have an observer located in the GameViewController

NotificationCenter.default.addObserver(self, selector: #selector(showDeathPopup), name: NSNotification.Name(rawValue: "death"), object: nil)

In this observer we then check for every notification post that contains the name: “death”. By doing this we can have execute functionality located in the GameViewController without instantiating it in the GameScene, thereby keeping a low coupling but a high cohesion.

 

(4) Discussion

Our initial goals for the project was to implement a game containing functionality for starting and stopping the game, this would give the game a main purpose. It would also open up the possibility of creating game ending entities, that stops the game early if the play comes in contact with them. It should also implement a highscore feature, that would let the player keep track of the time it took to complete each level. We have succeeded in implementing all of these requirements and therefore the base game is complete.

Bugs and missing implementations

[BUG] Death View: Whenever the player comes in contact with a death entity, the model sends a notification to the GameViewController to change the view into a game over screen. On this screen it should be possible for the player to choose between “Play Again” or “return to main menu”. However the view loads up correctly but instantly switches the view to the Main Menu. Thereby removing the ability to restart the game upon dying.

[BUG] Gyroscope: The physics of the way you handle the ball sometimes fails, and the ball does not move the same way that the player wants it to move.  This means that the player can experience “wrong movements” when playing the game. This bug does however not happen at every play through.

[Missing Implementation] Juicing: We initially wanted the game to feature “juicing”, where each interaction between game entities would set off different sounds, emerging the player into the gameplay. This is however not implemented, but since it was not a requirement for the project, it has been down prioritized.

[Missing Implementation] Multiple levels: We also wanted the game to feature multiple levels, in order to give the player a sense of progress. This is however not implemented and the game, as of now, only features one level.

 

Conclusion

In conclusion the project process has been a very interesting and enriching experience. And the project has expanded our knowledge, not only in Swift, but programming in general. The game ended up being partially incomplete, but still holds a lot of our initial declared features. Therefore we can conclude that the project has been a success despite missing some features. If you put our game in relation to the market standards, then it will be seen as inferior. This is mostly because of game not being completely “finished”, but also because our game does not feature the possibility of dsitributed gaming, allowing the player to play against friends online.

When talking about adding extra functionality in the future, we should first implement “juicing” and multiple levels, before considering adding new functionality.

 

Leave a Reply