I’m building out a system that is going to be driven by an enum in C++. During development, this enum is going grow and shrink and need to be reorganized as design evolves.
In Unreal, unfortunately, UENUMs are stored on disc as their byte values which means that if the enum changes, saved values will also change as their byte value will now point to a different enum value. On previous engines I’ve worked on, this was handled by storing the enums as strings on disc and resolving that as part of serialization. Without heavy modifications to the engine, this isn’t possible for me to do in Unreal. (I mean this on a global level. Yes, I could have an individual system save its own value as a string and handle the conversion, but I would have to do that in every place that uses the enum. Please correct me if I’m wrong.)
For perf reasons, it’s important that I’m working with enums in C++ so converting to a string or even a gameplay tag is a no go. Bottom line is that it needs to work in a switch statement, if that helps guide you. The system needs to be fully formed, accessible, and functional all from C++. I need to know all enum values (or whatever I end up using) in C++ in a type safe way (i.e. not using string literals or other things where typos can cause problems). The system processes data that uses the enums, so the solution needs to support easy data editing (i.e. drop downs) and reliable serialization even as the enum changes.
So, with all that in mind, what options do I have here, if any?
Both FName and FGameplayTag fill your requirements.
Ignoring FGameplayTag “for perf reasons” likely stems from a misunderstanding of how it works - gameplay tags aren’t strings, they’re backed by FName with some extra functionality. And names are stored as an index into a global table of strings. Comparing names is quick as you’re comparing indices into that table, not iterating strings. Hashing and just about every other operation is the same way up until you need to do real string operations on them (which involves getting a real string rather than doing anything with the name/gameplay tag, but this isn’t any slower than what you’d need to do with an enum anyway).
Initial creation of names from strings is slow (relative to simply assigning an enum value), but you can either use gameplay tags (which must be defined by engine startup and shouldn’t be created or looked up at runtime if you can help it) or reference static instances of names from code. So you should really only have to pay that cost on startup.
If you really want something based on actual enums it’d be possible to wrap your enums with a custom USTRUCT that has its own serialization/deserialization methods. You’d need to make a wrapper for every enum you want to work this way, however.
It’s funny you mentioned the enum wrapper as that’s what I’m currently working on. Having trouble figuring out how to implement custom serialization on a struct, though, so if you have any tips on that or can point me to an example, I’d appreciate it.
I debated added something else to my post and hoped that the switch statement point would be enough and of course now I wish I had. Everything needs to be known and usable in C++. GameplayTags are data driven and so are not const expressions.
For FName, I don’t think that will work either because I need them to be drop-down selectable in the editor but I’d be happy to learn otherwise.
I guess I wasn’t explicit in some of my requirements so I’ll mention them here and update the original post. The system needs to be fully formed, accessible, and functional all from C++. I need to know all enum values (or whatever I end up using) in C++ in a type safe way (i.e. not using string literals or other things where typos can cause problems). The system processes data that uses the enums, so the solution needs to support easy data editing and reliable serialization even as the enum changes.
Im just not seeing how its possible to fully manipulate an enum in such a manner, if you are removing them then the order changes and while you can resolve some issues to a degree Im not seeing how it can work at serialization time.
Acouple of scenarios:
If your enum name changes and your serialized data is the older value how do you resolve the change of name without a redirector?
If you remove an enum value from the middle of the pack how do you resolve that in the switch statement? You still have to update the enum in places where you use it.
The beauty of data driven models is they are content aware, they can know when you update a name and adjust all links accordingly. Const kinda means what it says, constant so its not intended to be variable in such a manner, reordering lists etc.
I appreciate your goal is performance but there will be trade offs, sometimes those have to come from our end but perhaps I am just not fully understanding the use case and the end goal because of the closed question
Oh and the Animation Editor uses a drop down list of names for bone selection I think it was.
C++ enums support assigning a specific value for each entry. Could you clarify why does this not work for you?
Also, combining specific values of each enum value by adding UMETA(DisplayName = “MyEnumValue”) is very flexible. You can change the display name any time you want without affecting anything.
If the values are serialized and stored on disc as a string (e.g. MyEnumVal) and converted back to the enum value when deserializing, that allows you to reorder and add values to anywhere in the enum without breaking existing data. If instead, as they are in Unreal, they are stored on disc by their integer value or index in the enum, reordering the enum means those integers will now map to different entries in the enum. Obviously removing entries requires special handling (usually resetting it to either the first or last entry in the enum) and showing an error to let the user know their data is bad and needs to be updated by hand.
I don’t want to support changing the enum name. If you mean changing the name of a value in the enum, that’s not something I need to support either. That’s effectively the same as removing and adding a new value. Which leads me to…
If we remove a value from the enum then, of course, the code that uses that enum will need to be removed as well. The question isn’t about the code side it’s about the data serialization. The issue is that if I have enum values of Val1, Val2, and Val3 and then I remove Val1 (and the associated code), anything that was Val2 will be Val3 and anything that was Val3 will be invalid.
Current enums support name changes, thats where I was coming from with that so maybe what you want isnt an enumerator at all! Just a line of thought continuing from Siliexs suggestion.
You dont have to repeat yourself, I got what you said about serialization but you are also introducing data inconsistencies which will need to be dealt with on a case by case basis. To the point that your system might require more code than just the brute force approach.
I just dont see your system being significantly more flexible because you are stuck on enums and not considering FNames seriously enough!
This is a good point. If the values are explicitly defined then reordering them matters less.
There are a few problems with this approach, though. First is that duplicate values are allowed in enums. This means that it will be super easy for someone to mistakenly double up on a value and tracking down those bugs might be difficult. The second (and somewhat related) is that as the enum grows (it’s likely to end up with dozens of entries by the time we ship) and get’s jumbled up, figuring out what the next unused value is will become more and more difficult leading to more mistakes that trigger problem #1.
Being able to use the enum order instead is much simpler and safer.
FNames (to my knowledge) can’t be used in a switch statement since they’re not const expressions. They also aren’t type safe i.e. I can’t have a property of type FName and be guaranteed that the value I’m being provided is valid.
I need a set list of values, selectable from a drop down, that can be reused in several different pieces of data (i.e. as a UPROPERTY), all without requiring custom editor code. If there’s a way to make FNames do that, I’d definitely be interested in seeing an example.
You’ve apparently never done MFC Windows programming because those two issue were never issues. MFC programmers keeping track of ids will know what I mean. Just have an enum with the current max+1 enum value. Every time you want a new enum, use the current value and add 1 to the max value.
I have no clue what “using the enum order” even means. That sounds like disaster waiting to happen.
Yeah, we used to call this “voodoo programming” where the code is just supposed to know what to do even though everything is in flux. (Or where it was supposed to work without enough information).
The UMeta example is actually pretty solid in that you could dynamically modify the name and leave the underlying enum intact but that would impact functionality of switch statements as they work on the variable name not the UMeta as far as Im aware.
UENUM()
enum class ERigVMControllerBulkEditType : uint8
{
AddExposedPin,
RemoveExposedPin,
RenameExposedPin,
ChangeExposedPinType,
AddVariable,
RemoveVariable,
RenameVariable,
ChangeVariableType,
RemoveFunction,
Max UMETA(Hidden),
};
The values of the enum start at 0 and go up with each entry.
I’m not sure what you’re suggesting with the ID then if you don’t think it’s an issue. Something like this (extreme example, but using it to make a point) is going to be a nightmare:
UENUM()
enum class ERigVMControllerBulkEditType : uint8
{
AddExposedPin = 4,
RemoveExposedPin = 5,
RenameExposedPin = 2,
ChangeExposedPinType = 1,
AddVariable = 14,
RemoveVariable = 7,
RenameVariable = 9,
ChangeVariableType = 0,
RemoveFunction = 11,
Max UMETA(Hidden), //I don't even know what I would do with this (it would be 12 right now, by the way)
};
Yeah, I know what enum order is. I just don’t know how anyone could ever rely on it. I don’t understand how that would ever work.
As for keeping track of enums values:
enum EMyEnum
{
A = 0,
Max = 1,
};
When you add a new enum, just use the max and update it.
enum EMyEnum
{
A = 0,
B = 1,
Max = 2
};
If you delete an entry, just delete it.
It’s relatively minor bookkeeping and this is the practice that was used with MFC.
@MonsOlympus You make a good point wrt UMETA. You would indeed need to update the switch statement if you alter the meaning the enums, but if that’s the case, I think there’s a serious design issue here.
If theyre using enum order indices then its not technically being reordered just the display order is being changed. My real concern is with removing items, you can always add buffer to the enum to make sure its large enough for all possible items but removing them even shifting it to a blank and shifting it youre reordering indices. Either that or you have another list of indices to skip.
At some point the design concerns outweigh the performance benefits.
Im saying is that if you resolve the indices based on your current order through the use of a serialized name youre not technically reordering the list because the indices arnt changing.
Value0 is index 0 and Value1 is index 1. I save the string “Value0” to disc and then I change the enum to:
enum MyEnum
{
Value1,
Value0
};
Value1 is now index 0 and Value0 is index 1. I read the string “Value0” from disc and convert it to MyEnum::Value0. Its index is now correctly 1 even though it was 0 the last time I saved.