Search Unity

Using enum's as part of a state_machine, but want to implement sub_states...

Discussion in 'Scripting' started by Valynna, Jul 15, 2022.

  1. Valynna

    Valynna

    Joined:
    Apr 1, 2021
    Posts:
    39
    Currently have

    public class Alien : MonoBehaviour
    {
    public enum _class { hunter, healer, digger, miner };
    public _class current_class;

    public enum _subclass { ---, ---, --- };
    }

    But it isn't really clear to me at all how to implement the _subclass feature the way I want to.

    After being spawned, the _class enum can't be changed, but the allowable _subclasses can be (so for instance, mid-game you can switch between one hunter subclass to another hunter subclass).

    I think I'm basically looking for a line of code that adds new possible enums to the _subclass. So for instance, I could do it with something like this :

    public void change_to_healer()
    {
    _subclass.clear();
    _subclass.add("healer subclass 1");
    _subclass.add("healer subclass 2");
    ...
    }

    but I'm not sure if these kind of methods exist?

    Alternatively, maybe enum's aren't the way to go? Maybe I should just use a list of strings for the subclasses?
     
  2. TheFunnySide

    TheFunnySide

    Joined:
    Nov 17, 2018
    Posts:
    200
    You are about to implement an antipattern!
    You are trying to use enums where you should use a class.
     
  3. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,928
    I assume you would have some traits or attributes associated to both each class and each subclass. So for instance, a "healer" that's also a "healer subclass 1" would have all attributes associated with a healer along with some that are specific to "healer subclass 1". This mirrors what class inheritance does in C#.

    I'd suggest using classes/structs instead of enums. You'd have a "CharacterClass" class, and subclasses just inherit from it, adding attributes or overriding methods as you see fit.

    A character would have a polymorphic reference to a CharacterClass, and the functionality of each concrete implementation could differ. So if you need a character to say, heal another, you'd do:

    character.class.Heal(otherCharacter);

    There would be a base implementation of Heal() for the basic HealerClass, which may be overridden by HealerSubclass1 or HealerSubclass2 to change how healing is performed for these subclasses. This way you keep class-dependent functionality nicely wrapped, and switching a character's class/subclass is just a matter of changing the reference.
     
  4. Valynna

    Valynna

    Joined:
    Apr 1, 2021
    Posts:
    39
    I guess that makes sense.

    Just make a hunter, healer, builder, etc... class, and have them inherit from the actor alien class, but each one will have it's own subclass inside of it.

    Feels a bit janky though, don't you think? Making a whole class just to override one or two methods?

    It would work for sure, it just doesn't feel so elegant.
    Still... if two people suggest going down that route, then it's probably the way to go.
     
  5. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,928
    Is the enum approach more elegant? hell no. :p

    Using enums, you would have to do this every time any functionality differs between classes.

    Code (CSharp):
    1. void Heal(Character otherCharacter)
    2. {
    3. switch(_class)
    4. {
    5.   case "Healer":{
    6.     switch(_subclass)
    7.     {
    8.       case "HealerSubclass1":{/*implement healing for healer subclass 1*/}break;
    9.       case "HealerSubclass2":{/* implement healing for healer subclass 2*/}break;
    10.     }
    11.   }break;
    12.  
    13.   case "Warrior":{
    14.     switch(_subclass)
    15.     {
    16.        case "WarriorSubclass1":{/* implement healing for warrior subclass 1*/}break;
    17.        case "WarriorSubclass2":{/* implement healing for warrior subclass 2 */}break;
    18.     }
    19.   }break;
    20. }
    21. }
    This is just for the Heal() method, and assuming all class-dependent methods are on the Character. As soon as you have other stuff around that might behave differently depending on the character class (weapons? skills/magic?), you'll have all the character class code scattered around multiple source files. Want to add a new subclass later on? well, search your entire codebase for everything that depends on it and add implementations for that subclass. This is verbose, hard to maintain, error prone, difficult to write tests for... and if you want a personal opinion, just plain ugly.

    Using classes for this could be understood as the Strategy pattern, so it's more or less the widely accepted "best" way to implement it.
     
    Last edited: Jul 15, 2022
  6. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,928
    Nope, that's not what we meant by "using classes". You just moved the problem of using enums one level down the inheritance hierarchy.

    A "Hunter" is not an "Alien". It's the other way around, "Alien" can either be a hunter, a healer, a builder, etc. So instead of this:

    Code (CSharp):
    1.  
    2. public class Alien
    3. {
    4. }
    5.  
    6. public class Hunter : Alien
    7. {
    8.    enum subclass {hunterSubclass1, hunterSubclass2}
    9.    Attack()
    10.    {
    11.       switch(subclass) //...etc
    12.    }
    13.    Heal()
    14.    {
    15.       switch(subclass) //...etc
    16.    }
    17. }
    18.  
    19. public class Healer : Alien
    20. {
    21.    enum subclass {healerSubclass1, healerSubclass2}
    22.  
    23.  
    24.    Attack()
    25.    {
    26.       switch(subclass) //...etc
    27.    }
    28.    Heal()
    29.    {
    30.       switch(subclass) //...etc
    31.    }
    32. }
    you do this:

    Code (CSharp):
    1.  
    2. public class Alien
    3. {
    4.    CharacterClass _class;
    5.    Heal(){_class.Heal();}
    6.    Attack(){_class.Attack();}
    7. }
    8.  
    9. public class CharacterClass
    10. {
    11. // base implementations of Heal and Attack, that all classes/subclasses use by default.
    12. virtual Heal()
    13. virtual Attack()
    14. }
    15.  
    16. public class Healer : CharacterClass
    17. {
    18. // override Heal and/or Attack for healers
    19. }
    20.  
    21. public class Hunter : CharacterClass
    22. {
    23. // override/extend Heal and/or Attack for hunters
    24. }
    25.  
    26. public class HealerSubclass1 : Healer
    27. {
    28. // override/extend Heal and/or Attack for healer subclass 1
    29. }
    30.  
    31. public class HealerSubclass2 : Healer
    32. {
    33. // override/extend Heal and/or Attack for healer subclass 2
    34. }
    35.  
    36. public class HunterSubclass1 : Hunter
    37. {
    38. // override/extend Heal and/or Attack for hunter subclass 1
    39. }
    40.  
    41.  
    ...etc.

    Need the Alien to become a hunter? do:
    Code (CSharp):
    1. alien._class = new Hunter();
    Or need it to become a healer subclass 1?
    Code (CSharp):
    1. alien._class = new HealerSubclass1();
    Now every time you call alien.Heal() or alien.Attack(), it will do the appropiate thing depending on its class/subclass.

    All code for each class/subclass is nicely packed into its own file, adding new classes or subclasses is simple and painless. Subclasses can reuse code written for their base class. You can easily write automated unit tests to check if a specific class/subclass does what it is supposed to.
     
    Last edited: Jul 15, 2022
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,726
    Enums are bad in Unity3D if you intend them to be serialized:

    https://forum.unity.com/threads/bes...if-not-do-something-else.972093/#post-6323361

    https://forum.unity.com/threads/unity-card-game-structure.1006826/#post-6529526

    It is much better to use ScriptableObjects for many enumerative uses. You can even define additional associated data with each one of them, and drag them into other parts of your game (scenes, prefabs, other ScriptableObjects) however you like. References remain rock solid even if you rename them, reorder them, reorganize them, etc. They are always connected via the meta file GUID.

    Collections / groups of ScriptableObjects can also be loaded en-masse with calls such as
    Resources.LoadAll<T>().


    ALSO, specific to state machines:

    This is my position on finite state machines (FSMs):

    https://forum.unity.com/threads/state-machine-help.1080983/#post-6970016

    I'm kind of more of a "get it working first" guy.

    Your mileage may vary.