Search Unity

Manager classes

Discussion in 'Scripting' started by kailin89, Sep 7, 2020.

  1. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    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):
    1. public class Player : MonoBehaviour {
    2.  
    3. private AttackHandler attackHandler;
    4. private AnimationHandler animationHandler;
    5.  
    6. public void RequestAttack(Attack attack) {
    7. animationHandler.RequestAttack(attack);
    8. attackHandler.RequestAttack(attack) ;
    9. }
    10.  
    11. public void OnAttackHit(Attack attack) {
    12. if (attackHandler.OnAttackHit(attack))
    13. {
    14. animationHandler.SuccesfulAttack(attack);
    15. }
    16. else {
    17. animationHandler.FailedAttack(attack);
    18. }
    19. }
    20.  
    21. }
    And the AttackHandler would look something like this:

    Code (CSharp):
    1. public class AttackHandler {
    2.  
    3. public void RequestAttack(Attack attack) {
    4. // do attack stuff, calculate if possible to do attack etc
    5. }
    6.  
    7. public bool OnAttackHit(Attack attack) {
    8. //evaluates attack
    9. //returns true if attack successful
    10. }
    11.  
    12. }
    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?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    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.
     
    tughanalpaslan, kailin89 and mopthrow like this.
  3. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    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):
    1. public interface ICanAttack
    2. {
    3. void RequestAttack();
    4. }
    5.  
    6. public class AttackHandler : MonoBehaviour, ICanAttack
    7. {
    8. public void RequestAttack(Attack attack)
    9. {
    10. //attack stuff
    11. if (AttackSuccesful)
    12. {
    13. //sucess stuff
    14. }
    15. else
    16. {
    17. }
    18. }
    19.  
    20. public bool AttackSuccesful()
    21. {
    22. }
    23. }
    24.  
    25. public class AnimationHandler : MonoBehaviour, ICanAttack
    26. {
    27. public void RequestAttack(Attack attack)
    28. {
    29. //attack stuff
    30. }
    31.  
    32. public class Input : MonoBehaviour
    33. {
    34. public void AttackButtonPressed()
    35. {
    36. // call the RequestAttack in each of the ICanAttack components of the game object, or whatever
    37. }
    38. }
    39.  
    40. }
    And they'd just be added to the relevant game objects, doing away with the Player manager class.
     
  4. Antony-Blackett

    Antony-Blackett

    Joined:
    Feb 15, 2011
    Posts:
    1,778
    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
     
    Vryken and Kurt-Dekker like this.
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    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):
    1. public interface IDriveable
    2. {
    3.   void SetSteerAngle( float angle);
    4.   void SetDesiredSpeed( float speed);
    5. }
    All regular cars might only implement that interface. But a bulldozer would implement
    IDriveable
    , but also an interface such as:

    Code (csharp):
    1. public interface IBucketEquipped
    2. {
    3.   void SetBucketHeight( float height);
    4.   void GetBucketContents();
    5. }
    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.
     
    kailin89 likes this.
  6. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    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.
     
  7. kailin89

    kailin89

    Joined:
    Dec 16, 2019
    Posts:
    13
    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.
     
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Or you can have one class implement each interface.

    Code (csharp):
    1. public class TankSteeringController : MonoBehavior, IDriveable {}
    2.  
    3. public class TankWeaponController : MonoBehavior, IShootable {}
    4.  
    5. 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.