I've got a Player class that basically routes connections to lower-level classes (I call these handlers (AttackHandler etc) So for example: Code (CSharp): public class Player : MonoBehaviour { private AttackHandler attackHandler; private AnimationHandler animationHandler; public void RequestAttack(Attack attack) { animationHandler.RequestAttack(attack); attackHandler.RequestAttack(attack) ; } public void OnAttackHit(Attack attack) { if (attackHandler.OnAttackHit(attack)) { animationHandler.SuccesfulAttack(attack); } else { animationHandler.FailedAttack(attack); } } } And the AttackHandler would look something like this: Code (CSharp): public class AttackHandler { public void RequestAttack(Attack attack) { // do attack stuff, calculate if possible to do attack etc } public bool OnAttackHit(Attack attack) { //evaluates attack //returns true if attack successful } } I end up with a bunch of smaller handler classes that do the actual logic. They also sometimes need to talk to each other (attackHandler might call method from animationHandler). It makes sense to me to have a main class that routes everything, but also feels a bit inefficient. I'm curious about how other people would architect something like this?
Looks reasonable... you might get some benefit by extracting common methods into some kind of interface, which is a pattern that works really well with Unity and MonoBehaviors. MBs can then implement specific interfaces and be found because they implement those interfaces. For instance, you might have an IAttackable interface that can represent a target that can be attacked. Some of its methods might be "what can hurt you?" and "you have been attacked by ..." This also means when a projectile hits a collider it can say "Hey, do you implement the IAttackable interface?" and if you do, have I got some business for you. A wall might not implement IAttackable, unless it makes sense. And of course you can always make Darkness implement IAttackable so that you can attack the darkness, a time-honored way of irritating your GM. Personally I prefer something like an IDamageable, then something before that which decides how much damage, perhaps considering information other interfaces related to the thing attacking, the attack itself and the target, as well as any other concerns like buffs, debuffs, environment, etc.. At some point in any complex game there has to be some level of linkage. You can extract and abstract it to the point where it becomes almost impossible to reason about when it malfunctions, or you can hard-wire all of it which becomes a sort of brittle bugs and replicated code. Somewhere in between lies a happy medium, and everybody works differently. But it's always good to consider all your tools, such as interfaces. Other folks like inheritance, but in Unity that always seems brittle and far more irritating to work with than interfaces, but your mileage may vary.
That's a good shout. So what you are suggesting is instead of using a monobehaviour manager class, which would hold a bunch of non-monobehaviour handlers, I would make individual monobehaviour components that would implement interfaces and have them found via GetComponent (or maintain a list of them or something)? Something like this, using the above example: Code (CSharp): public interface ICanAttack { void RequestAttack(); } public class AttackHandler : MonoBehaviour, ICanAttack { public void RequestAttack(Attack attack) { //attack stuff if (AttackSuccesful) { //sucess stuff } else { } } public bool AttackSuccesful() { } } public class AnimationHandler : MonoBehaviour, ICanAttack { public void RequestAttack(Attack attack) { //attack stuff } public class Input : MonoBehaviour { public void AttackButtonPressed() { // call the RequestAttack in each of the ICanAttack components of the game object, or whatever } } } And they'd just be added to the relevant game objects, doing away with the Player manager class.
Do what works. There’s no right or wrong. As long as it works, is not error prone. Is extensible to what you require for you project and doesn’t get in the way of workflow then just keep on with what you’ve got
I'm not sure it gets you completely away from a manager, but it helps you create a better model of who cares about what. Typically you would specify a series of methods in a single interface that encapsulate "all the things" you might do with something that implements it. Think of your car for instance. It might implement an IDriveable interface: Code (csharp): public interface IDriveable { void SetSteerAngle( float angle); void SetDesiredSpeed( float speed); } All regular cars might only implement that interface. But a bulldozer would implement IDriveable, but also an interface such as: Code (csharp): public interface IBucketEquipped { void SetBucketHeight( float height); void GetBucketContents(); } Or more to the point with games, a tank might implement an IFireable class that can fire its gun, reload its gun, etc. There's lots to read about proper interface design, but the great part in Unity3D is that you can get at them by GetComponent, which is very powerful.
Sure, I take that point but am more curious about other's opinions. Also I am working on a project where it is undecided how extensible it needs to be in the future, so wanna build something as resilient as possible.
I like this pattern, I guess the only thing is that there will be certain classes that would have to conform to loads of different interfaces (a tank that can shovel etc etc...) and might lead to pretty big classes.
Or you can have one class implement each interface. Code (csharp): public class TankSteeringController : MonoBehavior, IDriveable {} public class TankWeaponController : MonoBehavior, IShootable {} public class TankShovelController : MonoBehavior, IBucketLoader {} That is more in line with separation of concerns: https://en.wikipedia.org/wiki/Separation_of_concerns Honestly, you can also overthink this stuff as much as you want, as @Antony-Blackett hints at above. At the end of the day if you have three tanks types in your game, just write the controller, clone it for the next tank, maybe look for common code and extract it, and move on. Nothing wrong with just hard-coding it all, shipping the game, learning from it, and using better practices on subsequent games.