There have been a few times where I needed to serialize data from DataAssets using python. My previous approach was making many, many pickle functions like this:
# OLD WAY OF DOING THINGS;
def pickle_tmap(data: unreal.Map, pickler: Callable) -> dict:
out = {}
keys: List[str] = data.keys()
for key in keys:
out[str(key) if str(key.__class__.__name__) == "Name" else key] = pickler(data.get(key))
return out
def pickle_tarray(data: List, pickler: Callable) -> List[dict]:
out = []
for entry in data:
out.append(pickler(entry))
return out
def pickle_hitbox_section(data: unreal.AttackData) -> dict:
out = {}
out["hitboxes"] = pickle_tmap(data.m_hitboxes, pickle_hitbox_data)
out["hitbox_kb_data"] = pickle_tmap(data.m_hitbox_kb_data, pickle_hitbox_kb_data)
out["hitbox_creation_data"] = pickle_tarray(data.m_hitbox_creation_data, pickle_hitbox_creation_data)
return out
At a glance it may look fine - but it is very unmaintainable. There are countless different structures, so if you end up using anything that has many smaller structures, or if something changes or you mess-up writing even just one thing you may experience hours of pain trying to debug where the problem lies.
So, lets reflect: how do we fix this?
Well, luckily python is a scripting language that oftentimes has support for reflection - that is, it is able to get information about itself. We can take advantage of this using its dir()
method.
When dir()
is used it returns a List
of every property that exists on whatever gets passed into it. This remains true for most UObjects
thanks to Unreal’s reflection system. But… using it on them gives us a ton of properties that are irrelevant to what we’re trying to do. How do we get around this issue?
Unreal’s API provides a function to get the editor properties of an object, appropriately called get_editor_property()
. Its intended use appears to be to simply grab and return the value of a property if the developer knows what it is called. Otherwise it throws an Exception.
However, this can be used to our advantage to let us detect if a given string corresponds to an editor property using a try block like in the following snippet:
def is_editor_property(prop) -> bool:
try:
item.get_editor_property(prop)
return True
except Exception:
return False
This, when combined with, the dir
method from earlier, allows us to have an easy way to grab all the properties of a UObject
that are considered “editor properties.” We can do this using the following:
# assume "item" is a UObject
members = [d for d in dir(item) if is_editor_property(d)]
Ok- now we have a list of all the editor properties on an object… now what? We’re mostly there but there are still a few more questions:
- how do we access those properties on
item
using just strings? - how do we know what is a
UObject
in the first place???
To answer the first question, there is another built-in python function called getattr()
(short for “get attribute”). It takes a variable/class as its first argument and, conveniently, the member we want to access as the second in the form of a string. With this we have everything we need to filter and grab only the editor properties.
To know what exactly a UObject
is in the first place, we can use more elements of python’s reflection system. Namely, we’ll use __class__
and the issubclass
methods in the following way:
# assume "item" is anything except for "None"
if issubclass(item.__class__, unreal.Object):
# we now know that the item is a type of unreal Object
we can extend this to ensure that, if it is an UObject, then that it is not anything that isn’t a DataAsset (which is outside the scope of what I’m trying to do here):
# assume "item" is anything except for "None"
if issubclass(item.__class__, unreal.Object) and not issubclass(item.__class__, unreal.DataAsset):
# we now know that the item is NOT a DataAsset, and thus should be handled differently...
we can take things a step further and handle things like container types (Map
, Array
) and other special cases by using __name__
to simply grab the name of the item’s class as a string:
if item.__class__.__name__ == "Array" or item.__class__.__name__ == "Map":
# handle this special case
Now we have everything we need to determine what to do with a given object. When putting it all together with a bit of recursive magic, we get the final product:
# type annotation is just useful for in-editor autocompletion; it can be removed
def ue2py(item: unreal.Object):
# python understandably gets very very angry if we try to do anything with None objects
if item is None:
return "__NONE__"
name = item.__class__.__name__
if issubclass(item.__class__, unreal.Object) and not issubclass(item.__class__, unreal.DataAsset):
return item.get_name() if item != None else ""
if issubclass(item.__class__, unreal.EnumBase):
return item.name
if name == "Name":
def Name():
return str(item)
return Name()
if name == "Map":
def Map():
out = {}
data: unreal.Map = item
keys: List = data.keys()
for key in keys:
out[ue2py(key)] = ue2py(data.get(key))
return out
return Map()
if name == "Array":
def Array():
out = []
data: unreal.Array = item
for entry in data:
out.append(ue2py(entry, True))
return out
return Array()
if name == "float":
return f"f={item}"
if name == "int" or name == "str" or name == "bool":
return item
def is_editor_property(prop) -> bool:
try:
item.get_editor_property(prop)
return True
except Exception:
return False
out = {}
members = [d for d in dir(item) if is_editor_property(d)]
for member in members:
out[member] = ue2py(getattr(item, member))
return out
By calling ue2py
with a given unreal object (or almost any object for that matter), we’re able to break it down into a serialized/pickled form that we can then move into a json object and write to a file, where it can be loaded and used elsewhere!
If there are any more special cases, adjusting for that is as simple as adding another if statement. The type annotations can also be removed, if desired. I just find them useful for the in-editor autocompletion.
Thank you for reading!!
I hope you found this useful- I know I have~
- fudgepop01