-
Notifications
You must be signed in to change notification settings - Fork 18
Unreal Engine 4.25
When I made the first version of SkyHook, we were using Unreal Engine 4.25 at Embark Studios, so this is where I did all my testing. As it turned out, the implementation of Python was a bit flawed as pointed out in this UDN question. This very annoying bug has been fixed in 4.26, which is excellent news. Web Remote Control also has received some updates in 4.26
I'm linking to the original documentation I wrote for 4.25 here.
Unreal is a bit of a different beast. It does support Python for editor related tasks, but seems to be starving any threads pretty quickly. That's why it's pretty much impossible to run the SkyHook server in Unreal like we're able to do so in other programs. However, as explained in the main outline, SkyHook clients don't have to necessarily connect to SkyHook servers. That's why we can use skyhook.client.UnrealClient
with Unreal Engine's built-in Web Remote Control.
Web Remote Control is a plugin you have first have to load before you can use it. It's still very much in beta right now and changing any functionality is not easy. At time of writing, using Unreal Engine 4.25, you can't even change the port it binds to (8080). After you've loaded the plugin, start it by typing WebControl.StartServer
in the default Cmd
console in the Output Log window. Or, if you want to always start with the project, enter WebControl.EnableServerOnStartup 1
.
Loading a SkyHook module in Unreal is done by just importing it like normal. Assuming the code for the SkyHook module is in a file called "skyhook" in the Python folder (/Game/Content/Python), you can just do:
import skyhook
Unreal has specific requirements to run Python code. So you have to keep that in mind when adding functionality to the SkyHook module. In order for it to be "seen" in Unreal you need to decorate your class and functions with specific Unreal decorators.
Decorating the class with unreal.uclass():
import unreal
@unreal.uclass()
class SkyHookCommands(unreal.BlueprintFunctionLibrary):
def __init__(self):
super(SkyHookCommands, self).__init__()
If you want your function to accept parameters, you need to add them in the decorator like this
import unreal
import os
@unreal.ufunction(params=[str, str])
def rename_asset(self, asset_path, new_name):
dirname = os.path.dirname(asset_path)
new_name = dirname + "/" + new_name
unreal.EditorAssetLibrary.rename_asset(asset_path, new_name)
unreal.log_warning("Renamed to %s" % new_name)
Note
You can not use Python's list in the decorator for the Unreal functions. Use unreal.Array(type), eg: unreal.Array(float).
Note
You can not use Python's dict in the decorator for the Unreal functions. Use unreal.Map(key type, value type), eg: unreal.Map(str, int)
Warning
Here be dragons!
Alright, bear with me here. As of time of writing (4.25), there seems to be a very weird bug in Unreal Engine when it comes to executing Python code through Web Remote Control. It's reported on UDN, but so far it's not been fixed. The following problem occurs:
When a Python function is executed through Web Remote Control that takes parameters AND returns a value, it bugs out. It's does something very weird where the first argument becomes None or 0 and nothing is returned. To get around that weird behavior, I'm setting a global variable as the return value of a function. That value is then fetched and returned through Web Remote Control.
Is it elegant? No!
Does it work? Yes!
Luckily, skyhook.client.UnrealClient() hides all the ugliness behind a standard execute(), so it doesn't actually look any different than a normal SkyHook client in your scripts.
Example of how this madness works:
import os
import unreal
function_return = None
@unreal.uclass()
class SkyHookCommands(unreal.BlueprintFunctionLibrary):
def __init__(self):
super(SkyHookCommands, self).__init__()
# don't need to decorate these two functions, since we're never calling them through Web Remote Control
def __set_return(self, value):
global function_return
function_return = value
def __get_reply(self):
global function_return
return function_return
# this is the function the skyhook UnrealClient will always call in every execute() to get the reply
@unreal.ufunction(ret=str)
def get_reply(self):
global function_return
reply = function_return
# reset the global variable to None, since we've gotten what we came for
function_return = None
# return the reply as a string. Even if it's actually a list or a dict, the skyhook UnrealClient will handle it
return str(reply)
@unreal.ufunction()
def list_all_assets(self):
assets = unreal.EditorAssetLibrary.list_assets("/Game/")
# instead of returning a value, we're setting the global `function_return` to the result
self.__set_return(assets)
@unreal.ufunction(params=[str, unreal.Array(str), bool])
def search_for_asset(self, root_folder="/Game/", filters=[], search_complete_path=True):
filtered_assets = []
# list all the assets
self.list_all_assets()
# read outcome from previous function from __get_reply()
assets = self.__get_reply()
# find whatever we're looking for
for asset in assets:
for filter in filters:
if not search_complete_path:
if filter.lower() in os.path.basename(asset).lower():
filtered_assets.append(asset)
else:
if filter.lower() in asset.lower():
filtered_assets.append(asset)
# don't return it, but set the global variable
self.__set_return(filtered_assets)
Ok, so since we can't run our own SkyHook server in Unreal Engine, we're relying on Web Remote Control to talk to it. The good news is that if you just use skyhook.client.UnrealClient
, it behaves exactly the same as a normal Client
. Eventhough the abovementioned weirdness. For example:
import skyhook.client
client = skyhook.client.UnrealClient()
client = skyhook.client.UnrealClient()
response = client.execute("search_for_asset", parameters={"root_folder": "/Game/", "filters": ["level"], "search_complete_path": False})
print(response)
The response is a dictionary where the "ReturnValue" key is the return of the function you called.
{'ReturnValue': ['/Game/Developers/darkopracic/Maps/ScatterDemoLevel/ScatterDemoLevelMaster.ScatterDemoLevelMaster',
'/Game/Pioneer/Core/UI/Widgets/WBP_CriticalHealthLevelVignette.WBP_CriticalHealthLevelVignette',
'/Game/Pioneer/Lighting/LUTs/RGBTable16x1_Level_01.RGBTable16x1_Level_01']}