- Home /
Circular Dependency Issue
Hi,
I defined a MonoBehaviour
, a ScriptableObject
and a basic class to describe how my characters function:
Player.cs: ScriptableObject
that holds information about a Character
( Damage
, Name
, Price
, Attack Speed
etc.), the data here shouldn't be altered in any way (Like increasing the damage or changing the name).
PlayerData.cs: Class
that represents changeable & savable information about a Character ( Experience
, Tier
, isPurchased
)
PlayerController.cs : MonoBehaviour
that represents how that Character behaves ( Shooting
/Fighting
/Moving
)
The issue I'm facing is that I've created a Circular Dependency between those 3 classes: Player
contains a reference to PlayerController
to know which Prefab of a Player it holds. PlayerData
contains a reference to Player to know which character is being described. PlayerController
contains a reference to PlayerData
to know how much damage is dealt and what attack speed should be used, etc.
Is this a "good" Circular Dependency? I'd like to hear some ideas on how to remove this dependency if possible. Thanks!
Edit:
Player.cs
has no functions, it is only used to store data for every character.
Why does Player
need to have a reference to a PlayerController
prefab? Your spawner object should have a direct reference to the prefab.
Why do PlayerData
needs to know which character is being described?
Without code, it's hard to help.
Hi, it does seem like Player may not need PlayerController; a reference to a prefab is just a reference to a GameObject. If Player uses Controller to validate the prefab, maybe you could move that validation elsewhere?
Also, this doesn't solve the problem, but it may make things clearer: it seems that PlayerData's Player is never changed and its only used to pass its data to the Controller. That tells me it could be cleaner to pass the player to the PlayerController directly. I prefer to avoid using classes as bridges for other classes when I can.
Now, some subjective ideas about things that can be refactored:
Maybe Player should be called PlayerArchetype. It seems like a more accurate description, which could help you think about it differently.
Instead of using a prefab, Player could be an abstract class with an abstract method to create new GOs, with each concrete class would holding the data it needs to implement it. It can be nice to have all that defines a particular player type on the same place. I personally prefer to be able to define this kind of stuff with prefabs, though, and I wouldn't go that way if your designers don't code.
If Player needs a prefab, it might be overkill to have a separate ScriptableObject. It could be just another component in the prefab. Maybe it could even be integrated to the PlayerController if it makes sense. If one prefab is currently used in more than one Player for some reason, maybe you could use prefab variants instead.
This re$$anonymous$$ds me of a Prototype pattern. If it doesn't impact performance, maybe you can combine PlayerData and Player; you could clone a Player inside your Controllers with Instantiate. You can also store a reference to the prototype asset in case you need it; although, usually that can be replaced with a simple id field that gets copied to the instances.
Something very opinionated:
Usually, Circular Dependencies are treated as problem when talking about modules or assemblies; but circular dependencies as classes don't get such a bad rap in my experience. That's not to say that not to say that Circular Class Dependencies don't have problems; here are some:
It makes it harder to do stuff like Dependency Injection, mocking in tests, or automatic filling of fields in deserialization.
It makes it easier for circular references to exist and harder to track them.
It tends to make code more coupled.
All circular class dependencies have these problems, but it doesn't mean they're wrong. Conditionals make code harder to follow, but we just avoid overusing them. Sometimes it's the most practical way: interfaces don't play well with Unity's Serialization, events can be cumbersome. Example: It's common to have Circular dependencies AND references in hierarchical data; parents store children, and children reference parents to ensure there's only one.