- Get familiar with the Creational DPs;
- Choose a specific domain;
- Implement at least 3 CDPs for the specific domain;
- Singleton
- Factory
- Object Pool
-
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 managingEnemyEntity
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
;private EnemyHandler() {}
Then, we first need to save it in a
private static
variable.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.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.
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:
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.
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. TheProjectilePool
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:
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 returnnull
:
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:
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:
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 theProjectileHandler
class:- Constructor
Initializes the
ProjectileHandler
with aProjectilePool
instance:
private ProjectileHandler() { projectilePool = new ProjectilePool(false); }
cycle(float delta)
Method This method updates and renders all active projectiles using the pool's functionality:
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:
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:
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.
You can see in the debugger, that Projectiles are capped at 100 due to object pooling
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.