Serializing Unreal DataAssets with Python [Tutorial]

(taken from my tweet, here)


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:

  1. how do we access those properties on item using just strings?
  2. 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~ :blue_heart:
- fudgepop01

1 Like