Unreal Engine + Python API
To those who aren't in the UE loop it might seem weird, but yes! UE4 has been offering a Python API for a while now (iirc, since end of 2019).
It also ships with a shiny Python3.7 interpreter, as recommended by the vfxplatform.
Its main purpose is to offer a way to integrate Unreal into existing workflows, and automate the heck out of it! This means that it's definitely NOT meant to be used for anything "in game". That would be crazy, performance-wise.
Just like the Python API that ship with Houdini, Nuke, Maya, etc.. you should consider it as a side dish. You can use it to prep data, create directory structures, perform validation and all other things that would otherwise be tedious, manual and error prone. You have even the chance to create GUIs using Qt for Python (in its PySide2 flavour, but more on that later).
I've been playing around with it extensively for a month or so, now, so I wanted to make a few notes since each API has got its own quirks, and it took me a bit to understand how to the UE's API where designed..
> Python.. in Unreal Engine?The bindings are magic 🧙🏻♂️
The first thing to acknowledge is that the Python API is automatically created from what it's exposed to Blueprints. This means that, virtually every function/library that you have available in the Blueprints is also available in the Python API.
It's important to understand that the API (and the python docs) are generated automatically because you'll immediately notice that there's some funkyness going around with respect to how the modules and functions are structured: they don't feel native to Python. As python peeps like to say, they're not "pythonic". They appear to be mostly designed in a OOP fashion.. yet some classes are just useless!
Case 1: Ghost Classes 👻
As a first example, the other day I needed to get the start/end frame ranges of a section of a Camera Cut Track in a Level Sequence.
So I first found the MovieSceneSection class, which has an editor property called section_range.
Hmm, that's weird, why is that exposed an editor property? Anyway, it was of type MovieSceneFrameRange
, so, at least from the sound of the name, it seemed perfect for what I wanted.
If you look at the docs tho: https://docs.unrealengine.com/en-US/PythonAPI/class/MovieSceneFrameRange.html you will notice.. nothing.
It's a bloody empty page, just describing a struct!
There's not even a mention of the properties of that class.
Ok, you can always either inspect the C++ source code, or play around with it in the Python REPL inside UE. By playing around, I ended up with something like this:
>>> import unreal
>>> level_sequence = unreal.LevelSequenceEditorBlueprintLibrary.get_current_level_sequence()
>>> cut_tracks = level_sequence.find_master_tracks_by_exact_type(unreal.MovieSceneCameraCutTrack)
>>> sections = cut_tracks[0].get_sections()[0]
>>> sections[0].get_editor_property("section_range").to_tuple()
()
What the heck! Why is it an empty tuple? My section definitely had at least a start and end frame. I was sure about that because if I used (yet) another utility function things looked good:
>>> sections[0].get_start_frame()
0
>>> sections[0].get_end_frame()
100
After chatting with their dedicated dev support, the support engineer told me that I wanted to use a utility class instead: MovieSceneSectionExtensions
That's a common pattern that you might have already noticed. They make extensive use of "helper" classes. To name just a few:
unreal.StringLibrary
unreal.AssetTools
unreal.AssetToolsHelpers
unreal.AssetRegistry
unreal.AssetRegistryHelpers
unreal.EditorAssetLibrary
unreal.MovieSceneSectionExtensions
unreal.LevelSequenceEditorBlueprintLibrary..
Generally, in OOP, you expect your classes to take of themselves. In my experience, it's quite rare to have to use utility functions to perform basic operations on them.
Case 2: Renaming.. done right 🖊
Another example of this helpers design pattern occurred to me when trying to rename a bunch of level sequences. I was trying to understand if there was a way to rename these level sequences assets without having to load them, since I had a good number of them and it would have taken probably ~5 hours to just have Unreal open all of them one after the other. The naive me, used to other DCCs, thought that there had to be a "fast" way: after all, if implemented in a certain way, renaming should be similar to just changing a label on the surface, since the internal UID of the object is what is actually referenced everywhere.
After a bit of searching on the docs, as always, I noticed that each asset has a rename method.
Awesome! A level sequence asset inherits from ObjectBase
, so I thought I would be good.
I tested it out, and it seemed to actually do the trick, plus - it didn't require to have the asset already loaded. Hooray!
But after trying to load or save the levels just renamed I noticed that UE was going crazy. As soon as I'd click on the newly renamed asset, an asset with the old name would pop up in the content browser. Then, I had to click that old one in order to load the level. The editor was prompting me to save the level, but no matter what I tried, I couldn't.
Cancel, Retry and Continue were all useless
After chatting with the support, again, I discovered that the rename method is quite low level and it doesn't handle the creation of the Redictors.
That's... interesting?
If a unreal.LevelSequence
it's overriding that method, why isn't it doing the right thing when renaming?
I don't know - I guess there's a lot of complications around how Inheritance is handled in C++ and how it's translated to Python.
Once again, the way to go was to use an "external" helper function: EditorAssetLibrary.rename_asset
..
I originally ignored that class, because the docs specifically mention:
All operations can be slow. The editor should not be in play in editor mode. It will not work on assets of the type level.
But, turns out that I shouldn't want to rename the level, but the world associated with it. How do you know that? You ask the support, since at the moment there's not a lot of docs explaining exactly where a level stops and where a world begins :P
The simplest explanation that he gave me is that a UWorld
is a container for your ULevel
s.
The end.. for now
Anytime you're tempted to just call a method on a class, try to find if there's a helper class that they want you to use instead. This a bit counter intuitive for someone used to just making an instance and calling methods on that, but.. I guess they've got their own reasons.
When in doubt.. think of the good old Python's zen.. and ignore it :)
Extract from The Zen of Python, by Tim Peters
[..]
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
[..]
Have fun scripting stuff around!