report README.md
This commit is contained in:
262
README.md
262
README.md
@@ -1,33 +1,245 @@
|
|||||||
# Bugger
|
# Creational Design Patterns
|
||||||
|
|
||||||
A [libGDX](https://libgdx.com/) project generated with [gdx-liftoff](https://github.com/libgdx/gdx-liftoff).
|
|
||||||
|
|
||||||
This project was generated with a template including simple application launchers and a main class extending `Game` that sets the first screen.
|
## Author: Schipschi Daniil / FAF-223
|
||||||
|
|
||||||
## Platforms
|
----
|
||||||
|
|
||||||
- `core`: Main module with the application logic shared by all platforms.
|
## Objectives:
|
||||||
- `lwjgl3`: Primary desktop platform using LWJGL3; was called 'desktop' in older docs.
|
|
||||||
|
|
||||||
## Gradle
|
* Get familiar with the Creational DPs;
|
||||||
|
* Choose a specific domain;
|
||||||
|
* Implement at least 3 CDPs for the specific domain;
|
||||||
|
|
||||||
This project uses [Gradle](https://gradle.org/) to manage dependencies.
|
|
||||||
The Gradle wrapper was included, so you can run Gradle tasks using `gradlew.bat` or `./gradlew` commands.
|
|
||||||
Useful Gradle tasks and flags:
|
|
||||||
|
|
||||||
- `--continue`: when using this flag, errors will not stop the tasks from running.
|
## Used Design Patterns:
|
||||||
- `--daemon`: thanks to this flag, Gradle daemon will be used to run chosen tasks.
|
|
||||||
- `--offline`: when using this flag, cached dependency archives will be used.
|
|
||||||
- `--refresh-dependencies`: this flag forces validation of all dependencies. Useful for snapshot versions.
|
|
||||||
- `build`: builds sources and archives of every project.
|
|
||||||
- `cleanEclipse`: removes Eclipse project data.
|
|
||||||
- `cleanIdea`: removes IntelliJ project data.
|
|
||||||
- `clean`: removes `build` folders, which store compiled classes and built archives.
|
|
||||||
- `eclipse`: generates Eclipse project data.
|
|
||||||
- `idea`: generates IntelliJ project data.
|
|
||||||
- `lwjgl3:jar`: builds application's runnable jar, which can be found at `lwjgl3/build/libs`.
|
|
||||||
- `lwjgl3:run`: starts the application.
|
|
||||||
- `test`: runs unit tests (if any).
|
|
||||||
|
|
||||||
Note that most tasks that are not specific to a single project can be run with `name:` prefix, where the `name` should be replaced with the ID of a specific project.
|
* Singleton
|
||||||
For example, `core:clean` removes `build` folder only from the `core` project.
|
* Factory
|
||||||
|
* Object Pool
|
||||||
|
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
* **Singleton**
|
||||||
|
> In this project, I use the ___Singleton___ design pattern for classes and their instances that are responsible for managing
|
||||||
|
> the logic, rendering and behavior of their subordinates, such as a `EnemyHandler`, which is responsible for managing `EnemyEntity` objects throughout the application.
|
||||||
|
>
|
||||||
|
> Here is a comprehensive list of all the classes that were implemented using the ___Singleton___ pattern:
|
||||||
|
>
|
||||||
|
> - `CameraHandler`
|
||||||
|
> - `CleanupHandler`
|
||||||
|
> - `EnemyHandler`
|
||||||
|
> - `EnemyProjectileHandler`
|
||||||
|
> - `InputHandler`
|
||||||
|
> - `InterfaceHandler`
|
||||||
|
> - `ParticleHandler`
|
||||||
|
> - `ProjectileHandler`
|
||||||
|
> - `SpaceVFXHandler`
|
||||||
|
> - `SpawnerHandler`
|
||||||
|
> - `Player`
|
||||||
|
>
|
||||||
|
> The pattern is implemented by blocking direct instantiation of the constructor by making it `private`;
|
||||||
|
> ```java
|
||||||
|
> private EnemyHandler() {}
|
||||||
|
> ```
|
||||||
|
> Then, we first need to save it in a `private static` variable.
|
||||||
|
> ```java
|
||||||
|
> private static EnemyHandler instance;
|
||||||
|
> ```
|
||||||
|
> To access the instance, we implement the `public static` method `#getInstance()` that returns the instance from the stored variable, if it is not instantiated, we instantiate before returning.
|
||||||
|
> ```java
|
||||||
|
> public static EnemyHandler getInstance() {
|
||||||
|
> if (instance == null) {
|
||||||
|
> instance = new EnemyHandler();
|
||||||
|
> }
|
||||||
|
> return instance;
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
> By using Singletons, we can guarantee global state, centralized access, and unique instances of objects throughout the application, which often helps with performance, memory management, and codebase complexity.
|
||||||
|
>
|
||||||
|
> ```java
|
||||||
|
> public class EnemyHandler {
|
||||||
|
> // ... code ...
|
||||||
|
>
|
||||||
|
> private static EnemyHandler instance;
|
||||||
|
>
|
||||||
|
> private EnemyHandler() {
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> public static EnemyHandler getInstance() {
|
||||||
|
> if (instance == null) {
|
||||||
|
> instance = new EnemyHandler();
|
||||||
|
> }
|
||||||
|
> return instance;
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // ... code ...
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
|
* **Factory Method**
|
||||||
|
> In this project, I use the ___Factory Method___ design pattern to handle the instantiation of `EnemyEntity` objects. This pattern allows for creating objects without specifying the exact class of the object that will be created, providing flexibility and encapsulation in the object creation process.
|
||||||
|
>
|
||||||
|
> The `EnemyFactory` class is responsible for generating various types of enemies in the game. By using the Factory Method, the game can easily instantiate different enemy types based on the requirements, which promotes the Open/Closed Principle.
|
||||||
|
>
|
||||||
|
> Here is a comprehensive list of the key methods in the `EnemyFactory` class that demonstrate the Factory Method pattern:
|
||||||
|
>
|
||||||
|
> - `createRandomEnemy(World world, Vector2 position)`: Creates a random enemy type at the specified position.
|
||||||
|
> - `createEnemy(Enemies enemyType, World world, Vector2 position)`: Creates a specific type of enemy based on the provided enum.
|
||||||
|
>
|
||||||
|
> The factory method is implemented through a private static method that encapsulates the logic for creating the specific enemy types:
|
||||||
|
> ```java
|
||||||
|
> private static EnemyEntity getEnemyEntity(Enemies enemy, World world, Vector2 position) {
|
||||||
|
> return switch (enemy) {
|
||||||
|
> case STALKER -> new Stalker(world, position);
|
||||||
|
> case WASP -> new Wasp(world, position);
|
||||||
|
> case ULTRON -> new Ultron(world, position);
|
||||||
|
> case GOLEM -> new Golem(world, position);
|
||||||
|
> case STELLAR -> new Stellar(world, position);
|
||||||
|
> };
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
> This approach allows for centralized control of the object creation process, making it easier to manage and extend in the future without modifying the code that uses the factory.
|
||||||
|
>
|
||||||
|
> ```java
|
||||||
|
> public class EnemyFactory {
|
||||||
|
> private static final Random random = new Random();
|
||||||
|
>
|
||||||
|
> public static EnemyEntity createRandomEnemy(World world, Vector2 position) {
|
||||||
|
> int enemyType = random.nextInt(Enemies.values().length);
|
||||||
|
>
|
||||||
|
> return getEnemyEntity(Enemies.values()[enemyType], world, position);
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> public static EnemyEntity createEnemy(Enemies enemyType, World world, Vector2 position) {
|
||||||
|
> return getEnemyEntity(enemyType, world, position);
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> private static EnemyEntity getEnemyEntity(Enemies enemy, World world, Vector2 position) {
|
||||||
|
> return switch (enemy) {
|
||||||
|
> case STALKER -> new Stalker(world, position);
|
||||||
|
> case WASP -> new Wasp(world, position);
|
||||||
|
> case ULTRON -> new Ultron(world, position);
|
||||||
|
> case GOLEM -> new Golem(world, position);
|
||||||
|
> case STELLAR -> new Stellar(world, position);
|
||||||
|
> };
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
|
* **Object Pool Pattern**
|
||||||
|
> In this project, I utilize the ___Object Pool Pattern___ to manage the instantiation and reuse of `Ray` projectiles efficiently. This pattern minimizes the overhead of frequent object creation and destruction, which can lead to performance issues, particularly in a game with a high frequency of projectile generation. The `ProjectilePool` class maintains two arrays: one for free projectiles and one for deployed projectiles. By reusing objects instead of creating new ones, the application can manage memory more effectively and reduce garbage collection overhead.
|
||||||
|
>
|
||||||
|
> Here are the key methods in the `ProjectilePool` class that demonstrate the Object Pool Pattern:
|
||||||
|
>
|
||||||
|
> - **Constructor**
|
||||||
|
> The constructor initializes the pool with a predefined number of projectiles:
|
||||||
|
> ```java
|
||||||
|
> public ProjectilePool(boolean isEnemy) {
|
||||||
|
> for (int i = 0; i < INITIAL_PROJECTILES; i++) {
|
||||||
|
> freeProjectiles.add(new Ray(Bugger.getInstance().getWorld(), isEnemy));
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
> - **`obtain()` Method**
|
||||||
|
> This method retrieves a projectile from the pool. If no free projectiles are available, it can either reuse a deployed projectile or return `null`:
|
||||||
|
> ```java
|
||||||
|
> public Ray obtain() {
|
||||||
|
> Ray projectile;
|
||||||
|
> if (freeProjectiles.size > 0) {
|
||||||
|
> projectile = freeProjectiles.pop();
|
||||||
|
> } else if (deployedProjectiles.size > 0) {
|
||||||
|
> projectile = deployedProjectiles.first();
|
||||||
|
> deployedProjectiles.removeIndex(0);
|
||||||
|
> } else {
|
||||||
|
> return null;
|
||||||
|
> }
|
||||||
|
> deployedProjectiles.add(projectile);
|
||||||
|
> return projectile;
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> - **`free(Ray ray)` Method**
|
||||||
|
> This method resets and returns a projectile to the pool, marking it as available for reuse:
|
||||||
|
> ```java
|
||||||
|
> public void free(Ray ray) {
|
||||||
|
> ray.reset();
|
||||||
|
> deployedProjectiles.removeValue(ray, true);
|
||||||
|
> freeProjectiles.add(ray);
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> - **`updateAndRender(float delta)` Method**
|
||||||
|
> This method updates and renders all deployed projectiles. It manages their lifecycle and removes those marked for destruction:
|
||||||
|
> ```java
|
||||||
|
> public void updateAndRender(float delta) {
|
||||||
|
> for (int i = 0; i < deployedProjectiles.size; i++) {
|
||||||
|
> Ray ray = deployedProjectiles.get(i);
|
||||||
|
> if (!ray.isMarkedToDestroy()) {
|
||||||
|
> ray.update(delta);
|
||||||
|
> ray.render();
|
||||||
|
> } else {
|
||||||
|
> free(ray);
|
||||||
|
> i--;
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> The pooling mechanism promotes better performance and helps in managing resources, especially in a game where objects are frequently created and destroyed. Here is how the `ProjectilePool` is utilized in the `ProjectileHandler` class:
|
||||||
|
>
|
||||||
|
> - **Constructor**
|
||||||
|
> Initializes the `ProjectileHandler` with a `ProjectilePool` instance:
|
||||||
|
> ```java
|
||||||
|
> private ProjectileHandler() {
|
||||||
|
> projectilePool = new ProjectilePool(false);
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> - **`cycle(float delta)` Method**
|
||||||
|
> This method updates and renders all active projectiles using the pool's functionality:
|
||||||
|
> ```java
|
||||||
|
> public void cycle(float delta) {
|
||||||
|
> projectilePool.updateAndRender(delta);
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> - **`shootRay()` Method**
|
||||||
|
> Acquires a projectile from the pool and initializes it based on the player's aim:
|
||||||
|
> ```java
|
||||||
|
> public void shootRay() {
|
||||||
|
> Vector2 direction = new Vector2();
|
||||||
|
> float mouseX = Gdx.input.getX();
|
||||||
|
> float mouseY = Gdx.input.getY();
|
||||||
|
> Vector3 mousePosition = CameraHandler.getInstance().getCamera().unproject(new Vector3(mouseX, mouseY, 0));
|
||||||
|
> Vector2 playerPos = Player.getInstance().getPosition();
|
||||||
|
> direction.set(mousePosition.x, mousePosition.y).sub(playerPos).nor();
|
||||||
|
> shootRay(playerPos, direction, 20f);
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> - **`shootRay(Vector2 position, Vector2 direction, float speed)` Method**
|
||||||
|
> Obtains a projectile from the pool and initializes it with the given position and direction:
|
||||||
|
> ```java
|
||||||
|
> public void shootRay(Vector2 position, Vector2 direction, float speed) {
|
||||||
|
> Ray projectile = projectilePool.obtain();
|
||||||
|
> if (projectile != null) {
|
||||||
|
> projectile.init(position, direction.nor().scl(speed), false);
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> By implementing the Object Pool Pattern, we efficiently manage projectile instances, resulting in improved performance and resource utilization in the game.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusions / Screenshots / Results
|
||||||
|
|
||||||
|
In this project, various design patterns have been implemented to enhance code organization, improve performance, and simplify maintenance. The Singleton pattern ensures that essential classes, such as `EnemyHandler`, `ParticleHandler`, and `InputHandler`, have a single, global instance that provides centralized access to crucial game components. This pattern promotes efficient resource management and minimizes potential conflicts from multiple instances.
|
||||||
|
|
||||||
|
The Factory Method pattern is employed to create instances of `EnemyEntity` objects through the `EnemyFactory`. By encapsulating the instantiation logic, this pattern allows for greater flexibility in adding new enemy types without modifying existing code, adhering to the Open/Closed Principle.
|
||||||
|
|
||||||
|
The Object Pool Pattern is utilized to manage the lifecycle of `Ray` projectiles in the `ProjectilePool`. By reusing objects instead of creating new ones each time, this pattern significantly reduces memory overhead and garbage collection frequency, resulting in smoother gameplay performance.
|
||||||
|
|
||||||
|
Overall, the application of these design patterns not only streamlines the development process but also contributes to a more robust and scalable architecture. This approach allows for easier testing, debugging, and future enhancements, ensuring the project can evolve with additional features and complexity without compromising performance or maintainability.
|
||||||
|
|||||||
Reference in New Issue
Block a user