Skyhook
Simple Python communication system for DCC's and Game Engines
Install / Use
/learn @EmbarkStudios/SkyhookREADME
🌴 SkyHook
Engine and DCC communication system
SkyHook was created to facilitate communication between DCCs, standalone applications, web browsers and game engines. As of right now, it’s working in Houdini, Blender, Maya, Substance Painter and Unreal Engine.
<table style="width: 100%"> <tr> <td><img src="./wiki-images/blender_logo.png" height="50" /></td> <td><img src="./wiki-images/houdinibadge.jpg" height="50" /></td> <td><img src="./wiki-images/UE_Logo_Icon_Black.png" height="50" /></td> <td><img src="./wiki-images/maya_logo.png" height="50" /></td> <td><img src="./wiki-images/substance_painter.png" height="50" /></td> </tr> </table>The current mainline version is for Python 3.6 and up.
SkyHook consist of 2 parts that can, but don’t have to, work together. There’s a client and a server. The server is just a very simple HTTP server that takes JSON requests. It parses those requests and tries to execute what was in them. The client just makes a a POST request to the server with a JSON payload. This is why you can basically use anything that’s able to do so as a client. Could be in a language other than Python, or even just webpage or mobile application.
Release 3.0
I removed dependencies on PySide altogether, since they were causing unnecessary bloat to the package.
Quick Start
Some quick start examples to help you get on your way.
Pip installing
You should be able to pip install this package like this:
pip install --upgrade git+https://github.com/EmbarkStudios/skyhook
Maya
Let's say you have a file called skyhook_commands.py that is available in sys.path inside of Maya. The following are some example functions you could have:
skyhook_commands.py
import os
import maya.cmds as cmds
import pymel.core as pm
def delete_namespace(namespace, nuke_all_contents=False):
"""
Removes all objects from the given namespace and then removes it.
If given the nuke flag, will delete all objects from the namespace too.
:param namespace: *string*
:param nuke_all_contents: *bool* whether to also delete all objects contained in namespace and their children
:return:
"""
try:
if nuke_all_contents is not True:
pm.namespace(force=True, rm=namespace, mergeNamespaceWithRoot=True)
pm.namespace (set=':')
else:
nmspc_children = cmds.namespaceInfo(namespace, listOnlyNamespaces=True, recurse=True)
if nmspc_children:
# negative element count to get them sorted from deepest to shallowest branch.
sorted_name_list = sorted(nmspc_children, key=lambda element: -element.count(":"))
for obj in sorted_name_list:
cmds.namespace(removeNamespace=obj, deleteNamespaceContent=True)
cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True)
return True
except Exception as err:
print(err)
return False
def get_scene_path(full_path=True, name_only=False, folder_only=False, extension=True):
"""
Extension of the normal pm.sceneName() with a bit more options
:param full_path: *bool* returns the full path (D:/Game/scenes/enemy.ma)
:param name_only: *bool* returns the name of the file only (enemy.ma)
:param folder_only: *bool* returns the folder of the file only (D:/Game/scenes)
:param extension: *bool* whether or not to return the name with the extension
:return: *string*
"""
if name_only:
name = os.path.basename(pm.sceneName())
if extension:
return name
return os.path.splitext(name)[0]
if folder_only:
return os.path.dirname(pm.sceneName())
if full_path:
if extension:
return pm.sceneName()
return os.path.splitext(pm.sceneName())[0]
return ""
def selection(as_strings=False, as_pynodes=False, st=False, py=False):
"""
Convenience function to easily get your selection as a string representation or as pynodes
:param as_strings: *bool* returns a list of strings
:param as_pynodes: *bool* returns a list of pynodes
:param st: *bool* same as as_strings
:param py: *bool* same as as_pynodes
:return:
"""
if as_strings or st:
return [str(uni) for uni in cmds.ls(selection=True)]
if as_pynodes or py:
return pm.selected()
Running this code inside Maya sets up a SkyHook server:
import pprint
import skyhook_commands # this is the file listed above
from skyhook import server as shs
from skyhook.constants import ServerEvents, HostPrograms
def catch_execution_signal(command, parameters):
"""
This function is ran any time the skyhook server executes a command. You can use this as a callback to do
specific things, if needed.
"""
print(f"I just ran the command {command}")
print("These were the parameters of the command:")
pprint.pprint(parameters)
if command == "SKY_SHUTDOWN":
print("The shutdown command has been sent and this server will stop accepting requests")
elif command == "get_from_list":
print("Getting this from a list!")
# Since we're running this in Maya, we want to make use of a MainThreadExecutor, so that Maya's code gets executed in the
# main (UI) thread. Otherwise Maya can get unstable and crash depending on what it is we're doing. Therefor we'll use the
# `start_executor_server_in_thread` function, pass along "maya" as a host program so that the proper MainThreadExecutor is
# started.
thread, executor, server = shs.start_executor_server_in_thread(host_program=HostPrograms.maya)
if server:
server.hotload_module("maya_mod") # this gets loaded from the skyhook.modules
server.hotload_module(skyhook_commands, is_skyhook_module=False) # passing False to is_skyhook_module, because this files comes from outside the skyhook package
server = server
executor = executor
thread = thread
server.events.connect(ServerEvents.exec_command, catch_execution_signal) # call the function `catch_execution_signal` any time this server executes a command through its MainThreadExecutor
server.events.connect(ServerEvents.command, catch_execution_signal) # call the same function any time this server exectures a command outside of its MainThreadExecutor
We'll make a MayaClient to run any of the functions that we've provided the server with. This can be run as a standalone Python program, or from another piece of software, like Blender or Substance Painter.
from skyhook import client
maya_client = client.MayaClient()
result = maya_client.execute("selection", {"st": True})
print(f"The current selection is: {result.get('ReturnValue', None)}" )
# >> The current selection is: ['actor0:Leg_ctrl.cv[14]', 'actor0:Chest_ctrl', 'actor0:bake:root']
result = maya_client.execute("get_scene_path")
print(f"The current scene path is: {result.get('ReturnValue', '')}")
# >> The current scene path is: D:/THEFINALS/MayaScenes/Weapons/AKM/AKM.ma
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

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
# cleaning up pycache folder
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(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: `un
