Skip to content

Unreal Engine 4.25

Nisse edited this page Dec 17, 2020 · 2 revisions

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.

SkyHook server in Unreal

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

The SkyHook Unreal module

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)

Returning values to the SkyHook client

Warning

Here be dragons!

https://github.com/EmbarkStudios/skyhook/blob/main/wiki-images/dragon_martin_woortman.png?raw=true

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)

The SkyHook Unreal client

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']}