What is the dependency problem in the examples you’ve given? I understand that you could reduce dependency in both cases, but you seem to be approaching it as an absolute that you must achieve, as opposed to an ideal to strive for in order to have generally improved code maintainability.
To solve something you must first understand what the problem is. You’ve given it a name, but I’m not sure what you think is bad about those examples.
Why do they take different data? Based on their names I assume that their job or role is to aim at a specified object or position. With that in mind, why do they take different input? All the input should do is make them aware of the desired aim point. Even if that desired aim point isn’t possible for them to reach, that is an implementation detail that they should take care of internally.
For example, lets say that BodyAim can only rotate on its Y axis (as you’ve described) and you give it an aim location that is at a different height. That’s… actually perfectly fine, isn’t it? It should just rotate so that it’s pointing at the desired point along the horizontal plane, and all it has to do to achieve that is ignore the vertical component of the input.
Furthermore, lets consider the role split here. You have “AimBot”, and its role is (I assume) to decide where to aim. You then have “GunAim” and “BodyAim”, their roles are to point their relevant Transforms at the nominated point of aim. With that in mind:
- “AimBot” should not know or care how “GunAim” or “BodyAim” implement their aiming. It can both make and communicate its decision without caring about these details. It also probably shouldn’t be aware of any constraints they impose. (If it does need to be I’d design it differently.)
- “GunAim” and “BodyAim” should be able to accept identical inputs, and should not care where those inputs come from. Any differences in how those inputs are used are internal implementation details.
- As such, both “GunAim” and “BodyAim” can share an interface, eg. “IAimable”, or a base class, eg. “TransformAimer”.
- As such, “AimBot” can now be dependent on that interface or base class rather than on specific component types.
You could go one step further and use events or message passing to further reduce dependency and/or coupling, but I’d ask what the point is. What practical benefit does it achieve? Does it make sense to have a component responsible for deciding what to aim at that is completely unaware of what is doing the aiming?
On that note…
… I normally wouldn’t, because it doesn’t often make sense.
Sometimes it does make sense, and in those cases it’s usually pretty easy to do. For example, the other day I wanted to be able to represent a score value for any GameObject for a game I was making. In that case it does make sense to be able to drop it on anything and have it “Just Work”. And in that case it was also trivially easy - it’s just a MonoBehaviour with a couple of exposed values. It doesn’t depend on anything. Anything that depends on it can just look it up. Job done.
Does it truly make sense to have an AimBot component, however, that doesn’t depend in some way on components that can actually do aiming? Sure, you can use complicated mechanisms to avoid it throwing errors, but even without errors it still can’t do anything. In fact, by avoiding error messages in that case you’re just obfuscating something that is wrong with your code or your design. You’re making life harder for yourself.
Contrary to intuition, thrown or logged errors are your friends. They tell you when you’re doing something that doesn’t make sense. If you’ve attached an AimBot to a GameObject that doesn’t have any IAimables… what exactly were you trying to achieve? Was it a mistake? Did you forget to add IAimables? Has something been deleted by accident? If I do something like that I want an error message to tell me about it, immediately, loudly and clearly. That way I know something needs fixing and I know where and what it is.
If I avoid the error messages, though, all I’m doing is hiding a problem. I’m not going to know about it until later, and it’s going to take me far longer to solve it because the details are buried.
All of that said, there are cases where completely decoupling things makes a lot of sense. For instance, my Health component really shouldn’t have to be aware of my scoring system, nor vice versa. For this kind of thing, I strongly recommend looking into event systems. Using an event system messages can be passed between objects that are completely unaware of one another. Conveniently, Unity has just such an event system built in.
(Note that I’m referring to UnityEvent. EventSystem is for input handling.)