In today's discussion, I'm excited to shed light on a fascinating system within my game. Now, I'm still considering ways this can be refactored,so bear with me as I share something functional, understandable, and pretty cool.
This system encompasses several intriguing topics:
- Abstraction
- Concrete interfaces
- Singleton pattern
- Scriptable Objects
- Object Pooling
- A mystery topic (Don't worry, we'll go through it together 😄)
One day, while taking a break from development, I decided to embark on a playthrough of the classic "Legend of Zelda." Since my current project also involves a top-down 2D game, I thought revisiting this iconic game might provide some inspiration. And indeed, it did! As I slashed, bombed, and shot my way through enemies, I noticed that enemies had a chance to drop certain items, but not all the time and not at equal rates. This concept of random item drops reminded me of other Zelda titles and how they would spawn items when, for instance, you broke a pot or cut down grass. You never knew when a green or red rupee might appear. This got me thinking: How could I implement a similar system with comparable behavior in my own game?
I already had numerous items in my game, each with varying values. So, I decided to start by organizing which items had a chance to spawn and how I could categorize them into different tiers of worth. To achieve this, I employed Scriptable Objects as a means to store data that identified items as Basic, Rare, or Extra Rare. While these labels may sound straightforward, they served as a reasonable naming convention. Additionally, these Scriptable Objects contained references to the items themselves and the spawn rates for each category. This data would later be used by the concrete interface and the ItemSpawnSystem class, respectively.
Once I had assigned the necessary information within the Scriptable Objects, I created the ItemSpawnSystem class. This class took on the responsibility of managing the data and determining which category of item had a chance to spawn. Now, I'm aware that the Singleton Pattern often carries a less-than-favorable reputation due to potential misuse. However, I firmly believe that it's a suitable choice for classes with a single responsibility that only require one instance in the game. In my eyes, the ItemSpawnSystem fits this description perfectly. It serves as a conduit for receiving, evaluating, and transmitting information, regardless of which game element provides that data. Multiple objects need access to a single instance of it, rather than creating their instances and potentially causing null reference exceptions. Hence, I implemented it as a Singleton.
However, I adhere to a coding rule I've set for myself: when I have a Singleton or any public reference, I aim to have one script or class mediating what is sent to it. This practice simplifies debugging. In this case, my Game Manager seemed like a natural fit. It already serves as a hub for various systems in my project, making it an ideal candidate. So, after hours of contemplation, I designated the Game Manager as the access point for objects to interact with the Item Spawn system.
Now, let's dive into the ItemSpawnSystem class's actual functionality. Imagine an enemy has been defeated. As it enters its 'dying' state, just before it's destroyed, deactivated, or returned to its object pool, it sends a call to the Game Manager and ItemSpawnSystem. This call includes its transform.position and the type of item it can potentially spawn, such as Basic, Rare, or Extra Rare. The ItemSpawnSystem uses this information, along with data from the Scriptable Objects, to determine whether an item will spawn. It employs some probability calculations. If the odds align, it sends the information for a Basic Item, for instance, to the concrete interface responsible for spawning. This is where the concept of abstraction enters the picture.
Interfaces are a common tool for abstraction, but considering the Interface Segregation Principle, we find that having a 'fat' interface containing information for all three item spawn categories forces each respective client to implement what it doesn't need. So, this is where concrete interfaces come into play. These interfaces derive from a common interface, implementing the required functionality, akin to a contract. Now, the specific behavior is divided into three different concrete interfaces, one for each category. This arrangement ensures that each interface is only responsible for its specific implementation. It results in a cleaner and more maintainable solution, as opposed to a monolithic interface script where it's challenging to discern the role of each part.
Let's delve deeper into what these concrete interfaces do using the example of a Basic Item. Since a 'dying' enemy called for the Basic item, the interface that receives this information and executes the behavior is the IBasicItemSpawn. When active, it utilizes Random.Range to generate an integer. This integer serves as an index to retrieve the corresponding item from the BasicItems list via the Scriptable Object mentioned earlier. The interface scans this list and returns the item matching the result of the Random.Range. So, there are initial odds for reaching this point and another set of odds for determining which item within that category will spawn. The interface then delivers the stored object, which matches the index number, to a SpawnedItemsObjectPool script. This is where the interface proves its worth. The SpawnedItemsObjectPool implements the interface, accepts the information, and activates the designated object at the enemy's 'dying' transform.position. Voila! The item spawns.
In summary, the system begins with a call from the 'dying' enemy. The Game Manager facilitates this call and selects the item category with a chance to spawn. The ItemSpawnSystem class receives this information, processes it, and passes it to the interface. The concrete interfaces, which receive information from the item spawning class and return objects based on identifiers, provides that information to the item object pool and that item is spawned based on the specific criteria.
Well, that's about it, and I hope this description has unraveled the mystery topic for you - it's the Factory Method Pattern.
As always, thank you for reading. I've been thrilled with the versatility of this system. I am contemplating refactoring the contents of the Scriptable Object and the function used in the interface for better clarity. Nevertheless, it marks a promising first implementation of an item spawn system.
Top comments (0)