#Alternative to strings or enums as readable keys?

1 messages · Page 1 of 1 (latest)

icy tapir
#

I'm making a modular attack system, where I have AttackData scriptable objects, that hold attack's parameters, and among those, I have a dictionary list of prefabs the attack uses for FX (Particle systems, lights, etc). I am currently thinking on how I can best retrieve those objects in code with most readability, because seeing something like GameObject fx = Instantiate(attack.fxList[0]) ain't very informative, plus you'll be unable to see in the editor which slot of the array needed for which FX reference.
So far I considered two alternatives - strings and enums.
downside of strings is that they can be mistyped, and there's no direct coupling between a string in the code and a string in the dictionary list (the "was it 'attack_muzzleflash' or 'muzzleflash_attack'?" problem), but the upside is that they're very flexible and you do not need to edit the code if you need to add a new reference to the list.
With enums its the exact opposite - you can't mess them up since it's a predefined list of options, but you need to edit the definition of the enum each time you want to reference a new type of element in the dictionary.
Ideal solution would be to construct enum dynamically based on the contents of keys of fxList across the created scriptable objects, but as far as I can tell that's not possible.
So far for my purposes I feel like using strings is a better option for my case, but the threat of mistyped names remains (even if I sanitize them by forcing lowercase during OnValidate()). Are there any alternatives that at least wouldn't have downsides of either?

faint prawn
#

You could use SOs as well.
Also code Gen to create enums.

#

Maybe generics.
It's not entirely clear what part exactly you're looking to fix.
Sharing some code where you access the strings would be helpful.

icy tapir
# faint prawn Maybe generics. It's not entirely clear what part exactly you're looking to fix....

Well, in my current setup, I use enums to determine the FX prefab in the Scriptable Object, like in the image.
I get this prefab via the following function in the attack logic script:

        protected bool TryAttachFx<T>(string fxType, Transform point, out T fxObject) where T : Component
        {
            fxType = fxType.ToLower().Replace(" ", "_");
            fxObject = null;
            if (!attackDef.attackFX.ContainsKey(fxType) || !attackDef.attackFX[fxType] || !point)
            {
                DevLog.LogWarning("Attack definition " +attackDef.name + " doesn't have '" +fxType+ "' FX effect prefab.",attackDef);
                return false;
            }

            GameObject o = GameObject.Instantiate(attackDef.attackFX[fxType],point);
            o.transform.localScale = Vector3.one;
            o.transform.localPosition = Vector3.zero;
            o.name = attackDef.attackFX[fxType].name + " (Instance)";
            fxObject = o.GetComponent<T>();
            if (!fxObject)
            {
                DevLog.LogError("Attack Fx '"+attackDef.attackFX[fxType].name+"' is used by "+
                                this.GetType().Name+" Attack Logic, but doesn't have required "+typeof(T)+" component!",attackDef);
                Object.Destroy(o);
                return false;
            }
            return true;
        }

It has some error reporting code in case the necessary FX key string fxType is missing from the attackFX dictionary, but I wonder if it is the best solution.

#

In the code of the attack logic, this function is used like this:


                if(TryAttachFx("beam", attachmentPoint, out Beam fx))
                {
                    laser = fx;
                    laser.SetBeamColor(attackDef.attackDataReadonly.projectile.color);
                    laser.width = attackDef.attackDataReadonly.projectile.size;
                    laser.Init();
                    laser.gameObject.SetActive(false);
                    
                }
faint prawn
icy tapir
#

Well, if needs be, yes.

faint prawn
#

What am getting to is perhaps use references directly? Or get the object by type(actual type, not a string or enum)

icy tapir
faint prawn
# icy tapir What do you mean?

Well, if you have 2 beams in the fx, how are you gonna get the correct one?
I assume that by the string ID/Name(so, like Beam0, Beam1) . And where are you gonna define these strings? Hardcode them in the logic? That's a pretty bad thing on its own. If you're gonna define them in the inspector somewhere, then you might as well reference the fx directly.
Using strings as IDs, usually means you have more problems than just the string IDs.

icy tapir
#

Well, if you have 2 beams in the fx, how are you gonna get the correct one?
Oh, I thought you meant "instantiate more than one instance of the prefab" when you said "So can you have several Beam type fx assigned?"

#

but, yeah, hardcoding in the logic was the idea.

#

Each attack logic does its own thing, but the idea of the system as a whole is that any attack logic can use any attack definiton SO.

#

(Well, since the attack logic is defined in the attack definition, it be the reverse actually, come to think about it ^^")

faint prawn
#

Well, first I'd definitely avoid hardcoding the IDs. If the attack itself is an SO, then you can serialize the IDs in the inspector. At which point using an SO as an ID could be nice.

icy tapir
#

Attack logic aren't SOs, they're pure classes

faint prawn
#

Well, what's prevents you from making them SOs?

icy tapir
#

Since every NPC would need their own copy of that logic to run independently

#

Scriptable Objects are singletons IIRC

faint prawn
#

You can also separate it into a plain class for logic only and a pair SO for data, like the IDs.

faint prawn
faint prawn
icy tapir
faint prawn
#

No. That's not the only way.

  1. You can define the IDs in the paired data SO.
  2. You can get it by type and order. This would be easier if you want to keep hardcoding it.
icy tapir
#

Could you elaborate please? Since it sounds like option 1 is just moving the list to an additional different SO, and option 2 is unclear. You reference prefabs, so all types would be GameObject

faint prawn
faint prawn
icy tapir
#

Yeah but I expect my FX would contain a lot of, say, particle systems too.

faint prawn
#

Each FX prefab can have an orchestrator class at the root like Beam. So it wouldn't matter what else it has.

icy tapir
#

What I mean is that Beam is not a class unique to that specific effect used in that specific circumstance.

faint prawn
#

Yes, but it's unique in your list.

#

Or dictionary I guess.

#

And if you have several, you can distinguish by order.

icy tapir
#

Hm... I think I get it. Using Type's instead of Strings? Does Unity editor supports displaying those?

faint prawn
#

No, but you don't need to. That's why you need a more sophisticated storage system.

icy tapir
#

Distinguishing by order has its own problems though... I'll need to remember the order or even insert null references to not mess up the order (if say one attack doesn't use muzzleflash particles but other does, and they both must spawn two additional particle emitters when attack is happening)

faint prawn
#

There are several ways here too:

  1. Add the fx to a list in the inspector. Then at runtime create a dictionary of lists with Type - gameobject pairs and populate from the serialized list. Or loop the list each time you need to retrieve one of the fx.
faint prawn
#

This would take some effort to refactor though, so it's up to you.

Plainly speaking, your current approach is fine. Many games do it like that. Even AAA. It's not ideal. But better approaches require time and effort.

#

Whether it's worth it, is up to you as a dev to decide.

#

Just to make the SO approach clear: you would have a unique SO class for each attack logic/type that exposes exactly the FX type references that your logic expects.

icy tapir
#

Well, yeah, having a buttload of SO classes is what I was trying to evade, actually.

faint prawn
#

Well, think of it as an extension to your attack type. They can even be defined in the same file.

#

It would also make it easier to configure characters/skills/mobs with different attack types, as you would just need to assign the specific SOs to them in the inspector.

vital skiff
# icy tapir In the code of the attack logic, this function is used like this: ```csharp ...

Just read the conversation here and I think, one main culprit is, that you are trying to bake all events happening in a logic "chain" into one class. Which can easily result in a bunch of classes (or SOs) worst case. Did you think about creating a generic chain list that can fire events tied to specific classes? You can retrieve methods from your classes with reflection at edit mode and then try to fire them on the actual runtime reference for example.

icy tapir
#

So that I can freely swap and move them between NPCs or weapons.

crisp wren
#

SOs

#

asset bloat is expected, that's just how they are designed

#

editing enums could lead to breaking serialization

vital skiff
icy tapir
# vital skiff Can you elaborate more on what attacks you might have. I am just wondering, if y...

Yeah, it might be over the top, but when all attack logic is contained in the class and all NPC does is to trigger it to begin and then waiting for it to be finished - it ain't that hard or complicated, actually. Basically very similar in execution to a state machine.
Mainly I wanted that since majority of my enemies are going to be humans that can and should use shared set of moves, plus the ability to use items.