I have circular dependencies in my project where Code in the „player“ namespace depends on code in the „combat“ namespace and vice versa. What is a good way to solve this cycle? Maybe by putting the combat code in the player namespace?
Decoupling
Typically via interfaces.
Sometimes that’s overkill for games.
In your case: determine what should work without the other? Can there be combat without a player? Can there be a player without combat?
Both seem to be true statements so perhaps the solution is to abstract the concepts of player-based combat. Combat system may only need to work with “combatants” which implement ICombatant interface to, for example, execute attacks and take damage. Player implements that interface, and maybe enemies too.
Does the Player need to be aware of the combat system? How it works and what it does? Unlikely. It probably suffices to give the player a way to attack, block and to take damage. That’s dealt by implementing ICombatant.
This will get extra fun once you use assembly definitions.
You could put them on the same namespace, or if it is player combat code you could make it namespace Player.Combat.
The best way is to get all data in via 1 direction. If the player needs to control the combat, only call combat code from the player or vice versa. It is good to make small classes that are needed for this, like a Health class. This way you do not have to stick everything in the player script.
I also suggest to look into C# Actions (or Unity Events), since these can bridge dependencies by listening to, or invoking an event from another class and call code that way.
I think it should be mentioned that circular dependencies between namespaces aren’t really any issue at all. However circular dependencies between assembllies are not possible as assemblies essentially represent a directed graph / tree. Namespaces are just an organisational tool for type names.
Like it was mentioend, you should avoid such dependencies in the first place. If it’s necessary for some reason, interfaces are usually the way to go. Though events and callbacks could be a solution as well. Depends on the actual design and requirements.
these hierarchical pyramids of dependencies are an anti-pattern that is often thrown around among people who program with an object-oriented mindset. The reason why it’s a bad idea is essentially the same reason as the “composition > inheritance” heuristic. Creating a beautiful hierarchy of dependencies (which is conceptually the same thing as a superclass in OOP) looks great on paper… until you realize that the parts which combine to create complex systems don’t like being shoved into neat little categories that depend/inherit nicely with each other. There are always exceptions and scenarios which break the hierarchy of inheritors/dependents.
There is absolutely nothing wrong with circular dependencies. Many languages often support them, including C# namespaces as mentioned above, and they work just fine. Unity enforces this hierarchical pyramid anti-pattern with assembly references because they don’t know what they are doing.
You can use interfaces to resolve the cycle between assemblies, but in the end it’s only a “workaround” and there is still a cyclic dependency between the objects, just hidden behind more layers. If your combat code needs the player, then it needs the player, doesn’t matter if it uses Player, IPlayer or something hidden in some other layer that will access the player somehow.
Don’t get me wrong, there are many other good use cases for interfaces, but if you only add them to avoid a cyclic reference between assemblies, then you should maybe think about the just putting the code in the same assembly and avoid all the additional bloat.
Cyclic references inside one assembly is not an issue, even .NET and Unity assemblies have some internal cyclic references between types.
I would say you don’t need interfaces to deal with cyclical dependencies between assemblies. Doing so points to deeper architectural issues, and usually comes up when I see folks adding assembly definitions midway into a project.
It’s not an issue that comes up when you use them from day one and can keep a clear one-way chain of dependencies between assemblies/systems.
And calling it an anti-pattern is certainly… a take, I guess.
There doesn’t have to be a deeper issue, it could also mean that people try to split things into different assemblies which should simply stay in the same assembly, not everything has to work “independent”.
Although you are right, claiming that .NET doesn’t allow cyclic references is not a great excuse. Unity could have chosen to find a different way of solving the problem for which they use assembly definitions to solve. It could have been a solution that don’t have this unnecessary baggage tied to it. And assembly defs have a lot more weird, anti-dev baggage other than this one particular case. Again, they don’t know what they are doing, and their product has flaws.
Unity won’t do this now even if they can. Because they’re moving to CoreCLR, and after the move completes they will be using the same facility provided by .NET without any private modification. Supporting cyclic references now only ensures a great pain for the future migration since .NET doesn’t allow it.
And since majority of people doesn’t have issue with not having cyclic references, I doublt Unity will spend any effort into this.
Are you really suggesting to make projects generally without assembly definitions?
Honestly the fact that they disallow circular dependencies is how they force you to a more streamlined architecture which is a good thing…
As annoying as it is; I struggled with this too; but in the end once it worked out, the design felt much better than if I had just given up, not using the asemblies.
Technically, yes. There’s no problem for a computer to handle that.
Architecturally, circular dependencies are the root of all evil. It breaks layering, and control flow.
The solution is really, really simple and straightforward: inversion of control.
Circular dependencies are particularly nasty in memory managed environments. A references B. B references A. Result: memory leak. At best. More commonly you’ll get an exception if these classes don’t unregister event handlers for instance.
No, there are some cases where circular dependencies are totally fine and some cases where it is totally wrong, it’s not black and white.
It’s funny how the OP didn’t post a single line of yet but some people already assume the worst based on some keywords.
Btw your memory leak example is also wrong, sure A references B and B references A, but if nothing else references A or B, then the GC is smart enough to free both of them, so it’s not a real leak unless one of them is still used, but I wouldn’t call this a “leak”, since it’s still in use.
Don’t put words in my mouth I never said. No, I am not suggesting that the Unity should not use assembly defs. I’m suggesting that they shouldn’t use it to solve the problems that they are trying to use it for, such as how devs rely on it to have acceptable compiler times
Here’s a quote from what I said earlier, “Unity could have chosen to find a different way of solving the problem for which they use assembly definitions to solve. It could have been a solution that don’t have this unnecessary baggage tied to it.”
Maybe I could have communicated this point more clearly, but I think what I said is relatively clear regardless.
The types of people who frequent these forums the most (especially those who like to appear to others like they are wise experts of this engine) really hate harsh criticism aimed towards Unity. If the criticism is valid, then they hate it even more because it’s more of a threat to them.
I don’t believe I’m dodging anything here. Realy I’m asking a question you don’t seem to be willing to answer.
I’m asking how Unity would have worked around a fundamental limitation of .NET here? And is it worth Unity’s time when the current focus is to bring Unity away from Mono and onto Core CLR. And not just a “could have”, but a concrete explanation of how they would do so.
I’m also wondering how this limitation could be the source of so much ire. It’s really not that restrictive.