Skip to content

Unreal Engine 4.26 and up

Niels Vaes edited this page Apr 27, 2022 · 2 revisions

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 Remote Control API.

Make sure the Remote Control API is loaded from Edit > Plugins

image

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_commands in the Python folder (/Game/Content/Python), you can just do:

    import skyhook_commands

If you want to make sure it's always available when you start the engine, add an init_unreal.py file in the Python folder and import the module from there. I've had problems with having a __pycache__ folder on engine start up, so I just deleted it from that some init_unreal.py file:

    # making sure skyhook_commands are loaded and ready to go
    import skyhook_commands
    # adding this here because it's a pain in the ass to retype every time
    from importlib import reload

    # leaving a pycache folder lying around seems to crash the editor on exit in 4.26.1,
    # so let's delete it
    pycache_folder = os.path.join(os.path.dirname(__file__), "__pycache__")
    if os.path.isdir(pycache_folder):
        shutil.rmtree(pycache_folder)

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], static=true)
    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

Whenever you want to return anything from Unreal back to your SkyHook client, add the ret keyword in the function parameters:

    @unreal.ufunction(params=[str], ret=bool, static=True)
    def does_asset_exist(asset_path=""):
        """
        Return True or False whether or not asset exists at given path

        :return: *bool*
        """
        return_value = unreal.EditorAssetLibrary.does_asset_exist(asset_path)

        return return_value

Returning "complex" items from Unreal

Let's say you want to return a dictionary from Unreal that looks like this:

my_dict = {
    "player_name": "Nisse",
    "player_health": 98.5,
    "player_lives": 3,
    "is_player_alive": True
}

Unfortunately, due to how Unreal works, you can't specify your return value as a dictionary or list with mixed values. This can be quite problematic because sometimes that's exactly what you want. To solve this you can just stringify your return value and it will be decoded into a Python object on the client side.

So in the example above, you'd do something like

    @unreal.ufunction(params=[str], ret=str, static=True)
    def my_cool_function(player_name=""):
        my_dict = {
                "player_name": "Nisse",
                "player_health": 98.5,
                "player_lives": 3,
                "is_player_alive": True
        }

        return str(my_dict)

Since "this will just work", you can basically always just return a string like that.