Working on the BEST Projectile System for my project... so far :)
Tags: unity3d, csharp, gamedev, programming
Published: 2023-08-24 17:00:11

**PROJECTILES**

In today’s post, I want to discuss the way I handle projectiles in my current project. In the past, creating a projectile system that wasn’t tightly coupled to the game object using them was challenging. After some experimentation, I developed an initial system comprised of:

**Input Handler
PlayerShootState
Serialized References to the desired projectiles**

While functional, this approach had limitations in terms of flexibility and unnecessary dependencies on classes that didn't require knowledge about projectiles. I was looking for a more adaptable solution, especially since I intended to add projectiles with different types and behaviors.

Another challenge arose from the fact that in my game design, players could shoot projectiles repeatedly, requiring frequent instantiation of objects during runtime. The most straightforward solution to this is Object Pooling.

Object Pooling is a topic that deserves its own dedicated discussion, but grasping its basic purpose and functionality is essential to understanding how my projectile system works. Essentially, the object pooling script takes references to my projectile prefabs. It uses adjustable parameters to determine the pool size, i.e., how many instances of a specific projectile prefab will be created at runtime and stored in respective lists. The script includes functionality for 'un-pooling' projectiles, although this function remains private and inaccessible from elsewhere – an interesting aspect of this system.

Before we continue, I'd like to emphasize that I have various types of projectiles – currently, I have eight different projectiles, each with unique behaviors. My system became more complex when I encountered another issue. For an independent projectile system, how could the Object Pool identify which projectile to 'unpool'? The solution lay in using an identifier paired with a specific projectile. I utilized an Enum as the identifier – a list of all projectile varieties. I created a dictionary associating these Enum types with projectile prefabs. In the Object Pooling script, using the inspector, I assigned an enum, creating a key-value pairing stored in the dictionary for referencing. Instead of retrieving pooled objects from a list, the dictionary allows retrieval by passing in the corresponding enum. This improved the approachability of the pooled projectile objects by the systems needing access.

However, a question remained: What component would access the Object Pool? I realized that not only the player but also enemies, hazards, and bosses needed to shoot projectiles and might require access to this system. To address this, I kept the Dependency Inversion Principle (DIP) in mind. I required abstraction to ensure that high-level modules requiring a projectile weren't dependent on pooled projectiles or the Object Pooling class. After careful consideration, I introduced a ProjectileHandler class. This class holds functions that can be triggered wherever needed through an observer pattern – primarily, in the Player Weapon class. The Weapon class triggers an event that sends an identifying enum to the Object Pool script to 'un-pool' the corresponding projectile. A second event is raised, passing identifying information such as transform.position, damage amount, Vector2 direction, speed, or any unique data the weapon requires for desired behaviors. The actual projectiles subscribe to this second event.

While this might appear to result in chaotic projectiles spawning in random directions across the game world, pay attention to the sequence of events and the order in which the ProjectileHandler executes them:

The Weapon class raises the event to 'un-pool' a specific projectile.
The identifying enum is passed as an argument to this event.
The Object Pooling script, via the ProjectileHandler, receives this information and 'un-pools' the projectile by searching the dictionary and returning the first inactive projectile with that enum (key).
*Note: The pooling script can also create new projectiles if the current pool has no inactive ones.

Once the correct projectile is un-pooled, the specific projectile class subscribes to the second event raised by the Weapon class. This event contains arguments with behavior-specific data required by the projectile. The projectile uses this data to spawn at the Weapon's location, with a direction matching the Weapon's aim, and other necessary data for desired behavior.

This process reproduces when the player shoots again, changes weapons, or changes positions. The event is raised with the latest information, ensuring consistent functionality.

And that's essentially how it works! My system now comprises a Weapon class, an Object Pooling class, a Projectile class, and a ProjectileHandler – all collaborating while remaining separate. Abstraction through the ProjectileHandler makes this possible, providing a channel for essential information to observers. I'm pleased with the evolution of this system; it's modular, decoupled, and capable of expanding to different weapon classes, as well as accommodating new projectile types. Setting it up in the inspector is relatively straightforward, and the use of enums offers clarity and a visual representation of the used projectile.

Further enhancements are possible, especially for more intricate weapon behaviors or features. One idea I've been considering, which could be valuable for designing boss fights, is combining this system with a command pattern. I'm brainstorming ways the Projectile Handler can receive requests from a boss's specific attack or weapon state based on player behavior. It would store these requests and signal to execute them in sequence when a player meets a certain condition. This approach could create a sequence of different attacks based on player behavior, adding depth to boss battles. Details need refinement, but it holds promise for interesting encounters.

As always, thank you for reading. If you're just starting out and looking to architect a more optimal design for your projectiles, I hope the insights from my own experience can inspire some creative ideas.