FrontBadger
FrontBadger

Reputation: 41

Using ScriptableObject instead of Enums in Unity, overcomplicated for this use case?

I've read through : Use ScriptableObject-based Enums in Your Project | Unity and a few threads here and agree with the following:

  1. Enums in the editor are a ticking time-bomb that will destroy everything you hold dear.
  2. You can kind of mitigate the danger of enums by assigning them explicit integer values with some spacing in their ranges ie. Option1 = 0, Option2 = 10, Option3 = 20, which is kind of ugly and kind of dumb but works in preventing a change in the ordering of the list or removal of an element from the enum from destroying everything, but it feels a little hacky and ugly.

However, using ScriptableObjects as a replacement is proving tricky and I'm hoping there's a very simple way to do what I want to do and I'm just not thinking clearly.

Let's say we have Units, and one of the Unit's field's is "Faction" you can be in Faction A B or C. Now in a world where enums were not terrible in the editor I would make Faction an enum and call it a day. If I wanted to see what the relationship was between factions I'd need some logic in a class that says that A is hostile to B and friendly to C, probably using some 2d array or similar. However to try and save some future pain I want to use ScriptableObjects instead, let's let our designer use the editor to break things! (I'm the designer too btw, so updating an SO or updating code is actually the same to me but I'm trying hard to use the editor more)

So I make a FactionSO , it has a String Name, and a FactionRelationshipSO[] relationships. The Name is "A" or "B" or "C" , I write a little code to constrain the value of Name to one of the valid faction names defined in the class, good so far! If I wanted to make it super type safe I make a wrapper class like FactionNameString, if that is relevant then I assume I do so.

public class FactionSO : SerializedScriptableObject
{
    public static readonly string[] FactionIds = new string[] {
        FACTION_A ,
        FACTION_B,
        FACTION_C,
        };
    public const string FACTION_A = "Faction A";
    public const string FACTION_B= "Faction B";
    public const string FACTION_C= "Faction C";

    [ValueDropdown("FactionIds")] //odin inspector is great!
    [field: SerializeField]
    public string FactionIdString;

    public List<FactionRelationship > RelationshipList = new List<FactionRelationship>();
}

FactionRelationship is a simple tuple for FactionSO and a RelationshipSO so I can have a list of them.

public class FactionRelationship
{
    public FactionSO Faction;
    public RelationshipSO Relationship;
}

So now I add a RelationshipSO, that has its own predefined and constrained string values for its one value RelationshipString, could be "hostile" or "friendly" or "neutral".

public class FactionRelationshipSO : SerializedScriptableObject
{
    public const string HOSTILE_STRING = "Hostile";
    public const string NEUTRAL_STRING = "Neutral";
    public const string FRIENDLY_STRING = "Friendly";

    public static readonly string[] RelationshipIds = new string[]
    {
        HOSTILE_STRING,
        NEUTRAL_STRING,
        FRIENDLY_STRING,
    };


    [ValueDropdown("RelationshipIds")]
    [field: SerializeField]
    public string RelationshipString;
}

So, a FactionSO has a list of FactionRelationships that tell it how it should treat other factions. So far nothing is terrible.

So far I feel like I'm using an enumeration class instead of an enum ( Using Enumeration classes instead of enum types - .NET | Microsoft Learn ), I've just wrapped a bunch of strings in scriptable objects.

But here is where it gets a little dumb, I want of course to be able to get the relationship between two units based on their faction. So if Faction A is Hostile to Faction B, then a unit in Faction A should attack a unit in Faction B.

So I have a function in FactionSO that says getRelationship() , so a unity would be able to say self.Faction.getRelationship(otherUnity.Faction).

public FactionRelationshipSO getRelationship (FactionSO otherFaction)
{
    foreach (FactionHostilityRelationship factionHostilityRelationship in RelationshipList)
    {
        if (factionHostilityRelationship.Faction == otherFaction)
        {
            return factionHostilityRelationship.FactionRelationshipSO;
        }
    }
return null;
}

See that return null? If a relationship isn't explicitly defined it returns null but let's say that this is a very hostile world, and if you don't have a defined relationship you are automatically hostile to each other.

But we can't do this without some more work, because you can't just access a static SO labeled hostile like you can access an enum, or a static string. We could just return the string value that is inside the RelationshipSO (RelationShipString), and we could add a wrapper like RelationShipStringWrapper to make it typesafe, but I can't just return a FactionRelationshipSO value.

So, instead of just using an enum and a static class that tells you what the relationship between two factions is, I'm going to have to do all of the above, and even then I couldn't return a FactionRelationshipSO with all its attendant functionality

Also, even when I do find an return a FactionRelationShipSO because it's explicitly defined, I'm still going to need to pick at the RelationshipString inside to figure out what to do with it, like if it is hostile then attack, if it is neutral ignore, if it is friendly then wave. Which brings me back to feeling like these are just glorified enums but more work.

One alternative solution I was considering is that instead of explicitly having RelationshipSO values, instead have lists in FactionSO of FriendlyFactions, HostileFactions, NeutralFactions and then instead of returning a relationship you would check something like IsHostile(FactionSO otherFaction) and see if it's on the list of hostilefactions, and if there was no defined entry for that otherFaction in any of the lists you could return true to IsHostile in the above case where we wanted no relationship defined to result in a hostile relationship by default.

public class AlternativeFactionSO : SerializedScriptableObject
{
    public static readonly string[] FactionIds = new string[] {
        FACTION_A ,
        FACTION_B,
        FACTION_C,
        };
    public const string FACTION_A = "Faction A";
    public const string FACTION_B= "Faction B";
    public const string FACTION_C= "Faction C";

    [ValueDropdown("FactionIds")] //odin inspector is great!
    [field: SerializeField]
    public string FactionIdString; //we could get rid of this and just use the name of the object for debugging purposes too since we wouldn't rely on this value for logical comparisons anyway?

    public List<FactionSO > HostileFactionList = new List<FactionSO >();
    public List<FactionSO > NeutralFactionList = new List<FactionSO >();
    public List<FactionSO > FriendlyFactionList = new List<FactionSO >();

    public bool isHostile(FationSO otherFaction)
    {
        //yadda yadda if HostileFactionList.contains then yes, otherwise check to see if I'm in the neutral or friendly list, and if in neither then return true otherwise false
    }
}

So I'm hoping that either there is something very fundamental I'm misunderstanding about how I should be using ScriptableObjects as enums and you all can correct me, or to get confirmation that for my use case I should just use ugly basic enums with explicit integer values and call it a day.

Upvotes: 0

Views: 139

Answers (0)

Related Questions