Fallbound is a 2D platformer game where the player must fall down a series of platforms and gain power while defeating enemies. How far can you fall?
- Simão Barbosa (up202306609)
- Pedro Araújo (up202306606)
- Martim Cadilhe (up202307833)
Left ArroworA- move left.Right ArroworD- move right.Space- jump and shoot.Escape- open pause menu.
Up Arrow- select previous option.Down Arrow- select next option.Enter- confirm selection.
- Player Movement - the player can move left, right, and jump. Movement was very carefully designed to feel smooth and responsive.
- Procedural World Generation - the game has a procedural platform generation system that allows the player to play infinitely.
- Bullets - the player has bullets that can be used to shoot monsters and traverse the world with its recoil. Bullets can only be shot when the player is in the air and are recharged when the player touches the ground.
- Breakable Walls - the player can break some walls by shooting them.
- Player Health - the player has a life counting system. When the player runs out of lives the game ends.
- Enemies - the player will have to defeat several types of enemies, each with their own unique ways to be defeated.
- Difficulty Progression - the game will get harder as time passes. Enemies will spawn more frequently and new enemy types will appear.
- Coins - the player can collect coins to purchase items at the shop.
- Shop and Power-Ups - the player can buy different items at the shop, like the Power-Ups that permanently increase the player's abilities or extra lives.
- Menus - the game has a main menu, a pause menu and a game over menu.
- Sound - the game has sound effects and music.
- Timer - the game has a timer that counts the time the player has been playing. This is the main score of the game.
- Highscore Functionality - the game will keep a local track of the player's highest score.
- █ - Normal Enemies: This enemy can fly. Can be defeated by bullets or a player's stomp.
- Δ - Spike Enemy : This enemy can fly and is slower than the others. Can only be defeated by bullets.
- ∩ - Shell Enemy : This enemy is bound to the platforms. Can only be defeated by stomping.
- ♡ - Extra Health: Increases the player's max health.
- ↑ - Jump Height: Increases the player's max height while jumping.
- ♥ - Health Refill: Refills the player's health.
- 🗲 - Speed Boost: Makes the player move faster.
- | - Extra Bullets: Increases the player's bullet count.
- ⇢ - Faster Bullets: The player's bullets move faster.
Design patterns are standardized solutions that developers can apply to address recurring challenges in application or
system design. For a project of this nature, it's important to have a good code design to make the project more
maintainable and scalable.
When developing software with a user interface, it's crucial to implement a proper structural pattern. The code must be
organized and modular to uphold the Single Responsibility Principle.
For this project, we decided to use the Model-View-Controller (MVC) pattern.
This pattern organizes the application into three connected components, isolating the internal data representations from how the information is displayed and interacted with by the user:
- Model: Manages the data, business logic, and rules of the application.
- View: Renders the data from the model to the user.
- Controller: Acts as an intermediary, handling user input and updating the model
Here is how we implemented the MVC pattern:
- Clear separation of concerns aligns with SOLID principles, making the code easier to extend, test, and debug.
- Adding features requires minimal changes to existing components.
- Independent layers ensure efficient unit testing and adherence to SRP.
- Organized structure improves collaboration and reduces complexity.
An important characteristic to every application's design is the need to constantly update every entity while said application is running. Having a way to control how fast the game runs is extremely important and must be considered during its design.
To ensure that the game is responsive and updates occur at a constant rate, we need to apply the Game Loop pattern. This was applied to the Game class and describes the continuous cycle of updating and rendering required to run a game efficiently and smoothly, consisting of three main stages that repeat throughout the game's execution:
- Input - represents the key presses;
- Update - involves calculating the new game state, player movement, collisions, etc.
- Render - the loop calls the methods to draw or render the current game state on the screen.
Here are some examples of the Game Loop pattern at work:
- If not carefully optimized the game loop can consume a lot of processing power, leading to performance issues or excessive battery consumption.
- As the game grows in complexity the game loop may need to accommodate additional processing. Without scalable design, the loop could become overloaded and rigid, leading to degraded performance or the need for a major refactor.
When developing a game, it's important to have a clear way to manage different states, such as the main menu, pause menu
or the gameplay. Without proper organization, transitions between these states can become complicated and
error-prone.
There are multiple ways to obtain this, such as creating classes that use several boolean statements to check which
state the game is currently on.
For example, a menu class that changes its behavior according to the state of the application (i.e. pause, game over or
start).
However, this solution violates two of the principles of Object-Oriented design, the Single Responsibility Principle and
the Open-Closed Principle, as having all possible types of menus within the same class, forces them to be dependent on
each other. Also, if there is ever need to add another state to the application, it would force the modification of the
entire class.
To address this, we decided to use the State Pattern. This pattern simplifies state management by organizing behaviors into several distinct classes that extend an abstract class, in accordance with the Open-Closed Principle and the Single Responsibility Principle, ensuring cleaner, less rigid and more modular code. The state class is merely responsible for handling transitions between states not the behavior of the states themselves.
Here is an example of the State Pattern at work:
- If not carefully implemented, state transitions might lead to tightly coupled state objects, making changes harder in the long run.
- Frequent state changes or complex state transitions might introduce performance overhead or make debugging more challenging.
- Each state class has a single responsibility, encapsulating specific behavior, making the code easier to understand and maintain.
- Adding new states or modifying behaviors doesn’t require changes to existing state classes, supporting scalability and promoting the Open-Closed principle.
This game follows the MVC design structure, hereby taking into account multiple components at the same time that
interact with each other, such as the UI, the controller and the models.
With these components, the need to control the game's state consistently across the different components rise.
A solution to this is the use of the Singleton pattern to ensure that a class has only one instance and can be accessed from all parts of the project.
Here is the Singleton pattern at work:
- The use of Singleton can make testing more difficult, as the global instance may maintain state between tests.
- Since the Singleton instance is globally accessible, modifying the State class could impact multiple classes, making the design harder to extend without changing existing code, contradicting the Open/Closed Principle.
- This design reduces the need for synchronization.
- Simplifies code maintenance by adhering to the Single Responsibility Principle, as only the State class is responsible for game state management.
For our game, we envisioned several collectable items that can be obtained throughout the game, that give the player a
certain advantage.
These collectables, despite giving different advantages, have to behave in a cohesive and organized manner.
A way to obtain this would be to let the scene handle the creation of the collectables, via a method similar to the one
used for platform generation.
However, this would violate the Open/Closed principle, as if we ever wanted to add another type of collectable, it would
require modification to the entire scene class, which is just unnecessary.
The way we chose to deal with this issue is by using the Factory pattern.
A "factory" class was created to specifically handle the creation of said collectables according to their type and with
no need to specify which class the object belongs to.
This way, we abide by the Single Responsibility Principle and the Open/Closed Principle, making our code less
susceptible to code decay.
Here is where we implemented the Factory method:
- Introducing a Factory class adds a layer of abstraction. While this can simplify object creation for the scene class, it also makes the codebase more complex overall.
- If collectables were to ever require dynamic parameters or context-specific initialization, the factory pattern can become rigid or cumbersome.
- While the Factory pattern helps the scene class adhere to OCP, the factory itself might still need modifications when new collectable types are introduced.
Throughout the development process and after a re-analysis of our code, we identified some code smells that could be improved:
- God Class: The Scene class handles numerous responsibilities, including player management, bullet movement, collision detection, enemy spawning, and platform generation. In hindsight, this centralization of logic makes the class overly complex and harder to maintain, and could probably be fixed with some more time by breaking it down into smaller classes, like we did for Collectable Factory.
- Duplicate Code: There is some duplicated logic, such as the handling of key events in PlayerController and SceneController. In retrospective, this could be refactored into a common utility or base class.
- Lengthy switch statements: Big switch statements in the State and SoundController classes manage application states and sound behaviors, respectively. While justified by their responsibilities, this could be improved by using polymorphism to delegate behavior to specialized classes.
- To ensure effective mutation testing, we excluded the LanternaGUI and Game classes. LanternaGUI could conflict with Pitest due to its use of Lanterna, while testing the Game class, which runs the main function and game loop, would be unnecessary and problematic.
Click here to see the files generated by Pitest
This project was inspired by the game Downwell, developed by
Moppin and published by Devolver Digital.
The game's structure was mainly inspired on the Hero example presented in the classes. Some parts were also based
on: https://github.com/FEUP-LDTS-2023/project-l04gr03
- Simão Barbosa: 60%
- Pedro Araújo: 20%
- Martim Cadilhe: 20%





